From d8ed9abebf2c182e99aa309828ade4dc703cebfd Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 22 Sep 2025 12:53:44 -0700 Subject: [PATCH 001/400] add gitignore --- .gitignore | 3 +- server/.gitignore | 3 +- server/package-lock.json | 530 ++++- server/package.json | 7 +- server/src/config/routes.js | 22 +- .../src/routes/{ => v1}/announcementsRoute.js | 0 server/src/routes/{ => v1}/authRoute.js | 6 +- server/src/routes/{ => v1}/checkRoute.js | 7 +- server/src/routes/{ => v1}/diagnosticRoute.js | 4 +- server/src/routes/{ => v1}/inviteRoute.js | 4 +- server/src/routes/{ => v1}/logRoutes.js | 2 +- .../routes/{ => v1}/maintenanceWindowRoute.js | 2 +- server/src/routes/{ => v1}/monitorRoute.js | 4 +- .../src/routes/{ => v1}/notificationRoute.js | 0 server/src/routes/{ => v1}/queueRoute.js | 2 +- server/src/routes/{ => v1}/settingsRoute.js | 2 +- server/src/routes/{ => v1}/statusPageRoute.js | 2 +- .../tests/controllers/authController.test.js | 962 --------- .../tests/controllers/checkController.test.js | 372 ---- .../tests/controllers/controllerUtils.test.js | 157 -- .../controllers/inviteController.test.js | 202 -- .../maintenanceWindowController.test.js | 422 ---- .../controllers/monitorController.test.js | 1116 ---------- .../tests/controllers/queueController.test.js | 176 -- .../controllers/settingsController.test.js | 109 - .../controllers/statusPageController.test.js | 127 -- server/tests/db/checkModule.test.js | 589 ------ server/tests/db/hardwareCheckModule.test.js | 136 -- server/tests/db/inviteModule.test.js | 103 - .../tests/db/maintenanceWindowModule.test.js | 255 --- server/tests/db/monitorModule.test.js | 1865 ----------------- server/tests/db/notificationModule.test.js | 76 - server/tests/db/pageSpeedCheckModule.test.js | 63 - server/tests/db/recoveryModule.test.js | 168 -- server/tests/db/settingsModule.test.js | 56 - server/tests/db/statusPageModule.test.js | 70 - server/tests/db/userModule.test.js | 294 --- server/tests/services/emailService.test.js | 168 -- server/tests/services/jobQueue.test.js | 813 ------- server/tests/services/networkService.test.js | 439 ---- .../services/notificationService.test.js | 285 --- server/tests/services/settingsService.test.js | 140 -- server/tests/services/statusService.test.js | 255 --- server/tests/utils/dataUtils.test.js | 61 - server/tests/utils/imageProcessing.test.js | 53 - server/tests/utils/logger.test.js | 139 -- server/tests/utils/messages.test.js | 23 - server/tests/utils/utils.test.js | 50 - server/tsconfig.json | 26 + 49 files changed, 590 insertions(+), 9780 deletions(-) rename server/src/routes/{ => v1}/announcementsRoute.js (100%) rename server/src/routes/{ => v1}/authRoute.js (85%) rename server/src/routes/{ => v1}/checkRoute.js (76%) rename server/src/routes/{ => v1}/diagnosticRoute.js (77%) rename server/src/routes/{ => v1}/inviteRoute.js (82%) rename server/src/routes/{ => v1}/logRoutes.js (85%) rename server/src/routes/{ => v1}/maintenanceWindowRoute.js (91%) rename server/src/routes/{ => v1}/monitorRoute.js (94%) rename server/src/routes/{ => v1}/notificationRoute.js (100%) rename server/src/routes/{ => v1}/queueRoute.js (93%) rename server/src/routes/{ => v1}/settingsRoute.js (90%) rename server/src/routes/{ => v1}/statusPageRoute.js (93%) delete mode 100755 server/tests/controllers/authController.test.js delete mode 100755 server/tests/controllers/checkController.test.js delete mode 100755 server/tests/controllers/controllerUtils.test.js delete mode 100755 server/tests/controllers/inviteController.test.js delete mode 100755 server/tests/controllers/maintenanceWindowController.test.js delete mode 100755 server/tests/controllers/monitorController.test.js delete mode 100755 server/tests/controllers/queueController.test.js delete mode 100755 server/tests/controllers/settingsController.test.js delete mode 100755 server/tests/controllers/statusPageController.test.js delete mode 100755 server/tests/db/checkModule.test.js delete mode 100755 server/tests/db/hardwareCheckModule.test.js delete mode 100755 server/tests/db/inviteModule.test.js delete mode 100755 server/tests/db/maintenanceWindowModule.test.js delete mode 100755 server/tests/db/monitorModule.test.js delete mode 100755 server/tests/db/notificationModule.test.js delete mode 100755 server/tests/db/pageSpeedCheckModule.test.js delete mode 100755 server/tests/db/recoveryModule.test.js delete mode 100755 server/tests/db/settingsModule.test.js delete mode 100755 server/tests/db/statusPageModule.test.js delete mode 100755 server/tests/db/userModule.test.js delete mode 100755 server/tests/services/emailService.test.js delete mode 100755 server/tests/services/jobQueue.test.js delete mode 100755 server/tests/services/networkService.test.js delete mode 100755 server/tests/services/notificationService.test.js delete mode 100755 server/tests/services/settingsService.test.js delete mode 100755 server/tests/services/statusService.test.js delete mode 100755 server/tests/utils/dataUtils.test.js delete mode 100755 server/tests/utils/imageProcessing.test.js delete mode 100755 server/tests/utils/logger.test.js delete mode 100755 server/tests/utils/messages.test.js delete mode 100755 server/tests/utils/utils.test.js create mode 100644 server/tsconfig.json diff --git a/.gitignore b/.gitignore index 600d2d33b..172702b40 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -.vscode \ No newline at end of file +.vscode +.VSCodeCounter \ No newline at end of file diff --git a/server/.gitignore b/server/.gitignore index 4bc621f83..9ae4e58ed 100755 --- a/server/.gitignore +++ b/server/.gitignore @@ -8,4 +8,5 @@ coverage .clinic node_modules .vscode/* -public \ No newline at end of file +public +dist \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index 3e1130dc4..e2affbca0 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -54,7 +54,8 @@ "mocha": "11.1.0", "nodemon": "3.1.9", "prettier": "^3.3.3", - "sinon": "19.0.2" + "sinon": "19.0.2", + "tsx": "4.20.5" } }, "node_modules/@asamuzakjp/css-color": { @@ -258,6 +259,448 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", + "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", + "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", + "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", + "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", + "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", + "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", + "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", + "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", + "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", + "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", + "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", + "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", + "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", + "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", + "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", + "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", + "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", + "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", + "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", + "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", + "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", + "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", + "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", + "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", + "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", + "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.8.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz", @@ -3163,6 +3606,48 @@ "node": ">= 0.4" } }, + "node_modules/esbuild": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", + "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.10", + "@esbuild/android-arm": "0.25.10", + "@esbuild/android-arm64": "0.25.10", + "@esbuild/android-x64": "0.25.10", + "@esbuild/darwin-arm64": "0.25.10", + "@esbuild/darwin-x64": "0.25.10", + "@esbuild/freebsd-arm64": "0.25.10", + "@esbuild/freebsd-x64": "0.25.10", + "@esbuild/linux-arm": "0.25.10", + "@esbuild/linux-arm64": "0.25.10", + "@esbuild/linux-ia32": "0.25.10", + "@esbuild/linux-loong64": "0.25.10", + "@esbuild/linux-mips64el": "0.25.10", + "@esbuild/linux-ppc64": "0.25.10", + "@esbuild/linux-riscv64": "0.25.10", + "@esbuild/linux-s390x": "0.25.10", + "@esbuild/linux-x64": "0.25.10", + "@esbuild/netbsd-arm64": "0.25.10", + "@esbuild/netbsd-x64": "0.25.10", + "@esbuild/openbsd-arm64": "0.25.10", + "@esbuild/openbsd-x64": "0.25.10", + "@esbuild/openharmony-arm64": "0.25.10", + "@esbuild/sunos-x64": "0.25.10", + "@esbuild/win32-arm64": "0.25.10", + "@esbuild/win32-ia32": "0.25.10", + "@esbuild/win32-x64": "0.25.10" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -4079,6 +4564,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/get-tsconfig": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", + "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/glob": { "version": "10.4.5", "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", @@ -7578,6 +8076,16 @@ "node": ">=4" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/responselike": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", @@ -8551,6 +9059,26 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.20.5", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.20.5.tgz", + "integrity": "sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.25.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", diff --git a/server/package.json b/server/package.json index d879776a8..c57c3a4ce 100755 --- a/server/package.json +++ b/server/package.json @@ -6,7 +6,9 @@ "type": "module", "scripts": { "test": "c8 mocha", - "dev": "nodemon src/index.js", + "dev": "nodemon --exec tsx src/index.js", + "start": "nodemon ./dist/index.js", + "build": "tsc", "lint": "eslint .", "lint-fix": "eslint --fix .", "format": "prettier --write .", @@ -61,6 +63,7 @@ "mocha": "11.1.0", "nodemon": "3.1.9", "prettier": "^3.3.3", - "sinon": "19.0.2" + "sinon": "19.0.2", + "tsx": "4.20.5" } } diff --git a/server/src/config/routes.js b/server/src/config/routes.js index 7056393f9..2ec93a2dc 100644 --- a/server/src/config/routes.js +++ b/server/src/config/routes.js @@ -1,17 +1,17 @@ import { verifyJWT } from "../middleware/verifyJWT.js"; import { authApiLimiter } from "../middleware/rateLimiter.js"; -import AuthRoutes from "../routes/authRoute.js"; -import InviteRoutes from "../routes/inviteRoute.js"; -import MonitorRoutes from "../routes/monitorRoute.js"; -import CheckRoutes from "../routes/checkRoute.js"; -import SettingsRoutes from "../routes/settingsRoute.js"; -import MaintenanceWindowRoutes from "../routes/maintenanceWindowRoute.js"; -import StatusPageRoutes from "../routes/statusPageRoute.js"; -import QueueRoutes from "../routes/queueRoute.js"; -import LogRoutes from "../routes/logRoutes.js"; -import DiagnosticRoutes from "../routes/diagnosticRoute.js"; -import NotificationRoutes from "../routes/notificationRoute.js"; +import AuthRoutes from "../routes/v1/authRoute.js"; +import InviteRoutes from "../routes/v1//inviteRoute.js"; +import MonitorRoutes from "../routes/v1/monitorRoute.js"; +import CheckRoutes from "../routes/v1/checkRoute.js"; +import SettingsRoutes from "../routes/v1/settingsRoute.js"; +import MaintenanceWindowRoutes from "../routes/v1/maintenanceWindowRoute.js"; +import StatusPageRoutes from "../routes/v1/statusPageRoute.js"; +import QueueRoutes from "../routes/v1/queueRoute.js"; +import LogRoutes from "../routes/v1/logRoutes.js"; +import DiagnosticRoutes from "../routes/v1//diagnosticRoute.js"; +import NotificationRoutes from "../routes/v1/notificationRoute.js"; export const setupRoutes = (app, controllers) => { const authRoutes = new AuthRoutes(controllers.authController); diff --git a/server/src/routes/announcementsRoute.js b/server/src/routes/v1/announcementsRoute.js similarity index 100% rename from server/src/routes/announcementsRoute.js rename to server/src/routes/v1/announcementsRoute.js diff --git a/server/src/routes/authRoute.js b/server/src/routes/v1/authRoute.js similarity index 85% rename from server/src/routes/authRoute.js rename to server/src/routes/v1/authRoute.js index e790c60b0..f69f94f43 100755 --- a/server/src/routes/authRoute.js +++ b/server/src/routes/v1/authRoute.js @@ -1,9 +1,7 @@ import { Router } from "express"; -import { verifyJWT } from "../middleware/verifyJWT.js"; -import { verifyOwnership } from "../middleware/verifyOwnership.js"; -import { isAllowed } from "../middleware/isAllowed.js"; +import { verifyJWT } from "../../middleware/verifyJWT.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; import multer from "multer"; -import User from "../db/models/User.js"; const upload = multer(); diff --git a/server/src/routes/checkRoute.js b/server/src/routes/v1/checkRoute.js similarity index 76% rename from server/src/routes/checkRoute.js rename to server/src/routes/v1/checkRoute.js index 8bdb2d4fb..673fe51e0 100755 --- a/server/src/routes/checkRoute.js +++ b/server/src/routes/v1/checkRoute.js @@ -1,9 +1,6 @@ import { Router } from "express"; -import { verifyOwnership } from "../middleware/verifyOwnership.js"; -import { verifyTeamAccess } from "../middleware/verifyTeamAccess.js"; -import { isAllowed } from "../middleware/isAllowed.js"; -import Monitor from "../db/models/Monitor.js"; -import Check from "../db/models/Check.js"; + +import { isAllowed } from "../../middleware/isAllowed.js"; class CheckRoutes { constructor(checkController) { diff --git a/server/src/routes/diagnosticRoute.js b/server/src/routes/v1/diagnosticRoute.js similarity index 77% rename from server/src/routes/diagnosticRoute.js rename to server/src/routes/v1/diagnosticRoute.js index a15f5a038..1b7cc06be 100755 --- a/server/src/routes/diagnosticRoute.js +++ b/server/src/routes/v1/diagnosticRoute.js @@ -1,6 +1,6 @@ import { Router } from "express"; -import { verifyJWT } from "../middleware/verifyJWT.js"; -import { isAllowed } from "../middleware/isAllowed.js"; +import { verifyJWT } from "../../middleware/verifyJWT.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; class DiagnosticRoutes { constructor(diagnosticController) { diff --git a/server/src/routes/inviteRoute.js b/server/src/routes/v1/inviteRoute.js similarity index 82% rename from server/src/routes/inviteRoute.js rename to server/src/routes/v1/inviteRoute.js index d495c5476..74fcfeb91 100755 --- a/server/src/routes/inviteRoute.js +++ b/server/src/routes/v1/inviteRoute.js @@ -1,6 +1,6 @@ import { Router } from "express"; -import { verifyJWT } from "../middleware/verifyJWT.js"; -import { isAllowed } from "../middleware/isAllowed.js"; +import { verifyJWT } from "../../middleware/verifyJWT.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; class InviteRoutes { constructor(inviteController) { diff --git a/server/src/routes/logRoutes.js b/server/src/routes/v1/logRoutes.js similarity index 85% rename from server/src/routes/logRoutes.js rename to server/src/routes/v1/logRoutes.js index 867826dde..feabe669a 100755 --- a/server/src/routes/logRoutes.js +++ b/server/src/routes/v1/logRoutes.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../middleware/isAllowed.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; class LogRoutes { constructor(logController) { this.router = Router(); diff --git a/server/src/routes/maintenanceWindowRoute.js b/server/src/routes/v1/maintenanceWindowRoute.js similarity index 91% rename from server/src/routes/maintenanceWindowRoute.js rename to server/src/routes/v1/maintenanceWindowRoute.js index 774434ed6..622e98f88 100755 --- a/server/src/routes/maintenanceWindowRoute.js +++ b/server/src/routes/v1/maintenanceWindowRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; +import MaintenanceWindow from "../../db/models/MaintenanceWindow.js"; class MaintenanceWindowRoutes { constructor(maintenanceWindowController) { this.router = Router(); diff --git a/server/src/routes/monitorRoute.js b/server/src/routes/v1/monitorRoute.js similarity index 94% rename from server/src/routes/monitorRoute.js rename to server/src/routes/v1/monitorRoute.js index 043d361d1..f8f219b83 100755 --- a/server/src/routes/monitorRoute.js +++ b/server/src/routes/v1/monitorRoute.js @@ -1,7 +1,7 @@ import { Router } from "express"; -import { isAllowed } from "../middleware/isAllowed.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; import multer from "multer"; -import { fetchMonitorCertificate } from "../controllers/controllerUtils.js"; +import { fetchMonitorCertificate } from "../../controllers/controllerUtils.js"; const upload = multer({ storage: multer.memoryStorage(), // Store file in memory as Buffer }); diff --git a/server/src/routes/notificationRoute.js b/server/src/routes/v1/notificationRoute.js similarity index 100% rename from server/src/routes/notificationRoute.js rename to server/src/routes/v1/notificationRoute.js diff --git a/server/src/routes/queueRoute.js b/server/src/routes/v1/queueRoute.js similarity index 93% rename from server/src/routes/queueRoute.js rename to server/src/routes/v1/queueRoute.js index 55e829b91..59e1cb744 100755 --- a/server/src/routes/queueRoute.js +++ b/server/src/routes/v1/queueRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../middleware/isAllowed.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; class QueueRoutes { constructor(queueController) { this.router = Router(); diff --git a/server/src/routes/settingsRoute.js b/server/src/routes/v1/settingsRoute.js similarity index 90% rename from server/src/routes/settingsRoute.js rename to server/src/routes/v1/settingsRoute.js index 88eb67e65..76556e923 100755 --- a/server/src/routes/settingsRoute.js +++ b/server/src/routes/v1/settingsRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../middleware/isAllowed.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; class SettingsRoutes { constructor(settingsController) { diff --git a/server/src/routes/statusPageRoute.js b/server/src/routes/v1/statusPageRoute.js similarity index 93% rename from server/src/routes/statusPageRoute.js rename to server/src/routes/v1/statusPageRoute.js index 53eb21fba..8300066a8 100755 --- a/server/src/routes/statusPageRoute.js +++ b/server/src/routes/v1/statusPageRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { verifyJWT } from "../middleware/verifyJWT.js"; +import { verifyJWT } from "../../middleware/verifyJWT.js"; import multer from "multer"; const upload = multer(); diff --git a/server/tests/controllers/authController.test.js b/server/tests/controllers/authController.test.js deleted file mode 100755 index 341c8eda6..000000000 --- a/server/tests/controllers/authController.test.js +++ /dev/null @@ -1,962 +0,0 @@ -import { - issueToken, - registerUser, - loginUser, - editUser, - checkSuperadminExists, - requestRecovery, - validateRecovery, - resetPassword, - deleteUser, - getAllUsers, -} from "../../controllers/authController.js"; -import jwt from "jsonwebtoken"; -import { errorMessages, successMessages } from "../../utils/messages.js"; -import sinon from "sinon"; -import { tokenType } from "../../utils/utils.js"; -import logger from "../../utils/logger.js"; - -describe("Auth Controller - issueToken", function () { - let stub; - - afterEach(function () { - sinon.restore(); // Restore stubs after each test - }); - - it("should reject with an error if jwt.sign fails", function () { - const error = new Error("jwt.sign error"); - stub = sinon.stub(jwt, "sign").throws(error); - const payload = { id: "123" }; - const appSettings = { jwtSecret: "my_secret" }; - expect(() => issueToken(payload, tokenType.ACCESS_TOKEN, appSettings)).to.throw(error); - }); - - it("should return a token if jwt.sign is successful and appSettings.jwtTTL is not defined", function () { - const payload = { id: "123" }; - const appSettings = { jwtSecret: "my_secret" }; - const expectedToken = "mockToken"; - - stub = sinon.stub(jwt, "sign").returns(expectedToken); - const token = issueToken(payload, tokenType.ACCESS_TOKEN, appSettings); - expect(token).to.equal(expectedToken); - }); - - it("should return a token if jwt.sign is successful and appSettings.jwtTTL is defined", function () { - const payload = { id: "123" }; - const appSettings = { jwtSecret: "my_secret", jwtTTL: "1s" }; - const expectedToken = "mockToken"; - - stub = sinon.stub(jwt, "sign").returns(expectedToken); - const token = issueToken(payload, tokenType.ACCESS_TOKEN, appSettings); - expect(token).to.equal(expectedToken); - }); - - it("should return a refresh token if jwt.sign is successful and appSettings.refreshTokenTTL is not defined", function () { - const payload = {}; - const appSettings = { refreshTokenSecret: "my_refresh_secret" }; - const expectedToken = "mockRefreshToken"; - - stub = sinon.stub(jwt, "sign").returns(expectedToken); - const token = issueToken(payload, tokenType.REFRESH_TOKEN, appSettings); - expect(token).to.equal(expectedToken); - }); - - it("should return a refresh token if jwt.sign is successful and appSettings.refreshTokenTTL is defined", function () { - const payload = {}; - const appSettings = { - refreshTokenSecret: "my_refresh_secret", - refreshTokenTTL: "7d", - }; - const expectedToken = "mockRefreshToken"; - - stub = sinon.stub(jwt, "sign").returns(expectedToken); - const token = issueToken(payload, tokenType.REFRESH_TOKEN, appSettings); - expect(token).to.equal(expectedToken); - }); -}); - -describe("Auth Controller - registerUser", function () { - let req, res, next; - - beforeEach(function () { - req = { - body: { - firstName: "firstname", - lastName: "lastname", - email: "test@test.com", - password: "Uptime1!", - role: ["admin"], - teamId: "123", - inviteToken: "invite", - }, - db: { - checkSuperadmin: sinon.stub(), - getInviteTokenAndDelete: sinon.stub(), - updateAppSettings: sinon.stub(), - insertUser: sinon.stub(), - }, - settingsService: { - getSettings: sinon.stub().resolves({ - jwtSecret: "my_secret", - refreshTokenSecret: "my_refresh_secret", - }), - }, - emailService: { - buildAndSendEmail: sinon.stub(), - }, - file: {}, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - sinon.stub(logger, "error"); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if body validation fails", async function () { - req.body = {}; - await registerUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if checkSuperadmin fails", async function () { - req.db.checkSuperadmin.throws(new Error("checkSuperadmin error")); - await registerUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("checkSuperadmin error"); - }); - - it("should reject with an error if getInviteTokenAndDelete fails", async function () { - req.db.checkSuperadmin.returns(true); - req.db.getInviteTokenAndDelete.throws(new Error("getInviteTokenAndDelete error")); - await registerUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("getInviteTokenAndDelete error"); - }); - - it("should reject with an error if updateAppSettings fails", async function () { - req.db.checkSuperadmin.returns(false); - req.db.updateAppSettings.throws(new Error("updateAppSettings error")); - await registerUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("updateAppSettings error"); - }); - - it("should reject with an error if insertUser fails", async function () { - req.db.checkSuperadmin.resolves(false); - req.db.updateAppSettings.resolves(); - req.db.insertUser.rejects(new Error("insertUser error")); - await registerUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("insertUser error"); - }); - - it("should reject with an error if settingsService.getSettings fails", async function () { - req.db.checkSuperadmin.resolves(false); - req.db.updateAppSettings.resolves(); - req.db.insertUser.resolves({ _id: "123" }); - req.settingsService.getSettings.rejects(new Error("settingsService.getSettings error")); - await registerUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("settingsService.getSettings error"); - }); - - it("should log an error if emailService.buildAndSendEmail fails", async function () { - req.db.checkSuperadmin.resolves(false); - req.db.updateAppSettings.resolves(); - req.db.insertUser.returns({ _id: "123" }); - req.settingsService.getSettings.returns({ - jwtSecret: "my_secret", - refreshTokenSecret: "my_secret", - }); - req.emailService.buildAndSendEmail.rejects(new Error("emailService error")); - await registerUser(req, res, next); - expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal("emailService error"); - }); - - it("should return a success message and data if all operations are successful", async function () { - const user = { _id: "123" }; - req.db.checkSuperadmin.resolves(false); - req.db.updateAppSettings.resolves(); - req.db.insertUser.returns(user); - req.settingsService.getSettings.returns({ - jwtSecret: "my_secret", - refreshTokenSecret: "my_secret", - }); - req.emailService.buildAndSendEmail.resolves("message-id"); - await registerUser(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: successMessages.AUTH_CREATE_USER, - data: { user, token: sinon.match.string, refreshToken: sinon.match.string }, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); - - it("should return a success message and data if all operations are successful and superAdmin true", async function () { - const user = { _id: "123" }; - req.db.checkSuperadmin.resolves(true); - req.db.updateAppSettings.resolves(); - req.db.insertUser.returns(user); - req.settingsService.getSettings.returns({ - jwtSecret: "my_secret", - refreshTokenSecret: "my_secret", - }); - req.emailService.buildAndSendEmail.resolves("message-id"); - await registerUser(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: successMessages.AUTH_CREATE_USER, - data: { user, token: sinon.match.string, refreshToken: sinon.match.string }, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); -}); - -describe("Auth Controller - loginUser", function () { - let req, res, next, user; - - beforeEach(function () { - req = { - body: { email: "test@example.com", password: "Password123!" }, - db: { - getUserByEmail: sinon.stub(), - }, - settingsService: { - getSettings: sinon.stub().resolves({ - jwtSecret: "my_secret", - refreshTokenSecret: "my_refresh_token", - }), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - user = { - _doc: { - email: "test@example.com", - }, - comparePassword: sinon.stub(), - }; - }); - - it("should reject with an error if validation fails", async function () { - req.body = {}; - await loginUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if getUserByEmail fails", async function () { - req.db.getUserByEmail.rejects(new Error("getUserByEmail error")); - await loginUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("getUserByEmail error"); - }); - - it("should login user successfully", async function () { - req.db.getUserByEmail.resolves(user); - user.comparePassword.resolves(true); - await loginUser(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: successMessages.AUTH_LOGIN_USER, - data: { - user: { - email: "test@example.com", - avatarImage: undefined, - }, - token: sinon.match.string, - refreshToken: sinon.match.string, - }, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); - - it("should reject a user with an incorrect password", async function () { - req.body = { - email: "test@test.com", - password: "Password123!", - }; - req.db.getUserByEmail.resolves(user); - user.comparePassword.resolves(false); - await loginUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal(errorMessages.AUTH_INCORRECT_PASSWORD); - }); -}); - -describe("Auth Controller - refreshAuthToken", function () { - let req, res, next, issueTokenStub; - - beforeEach(function () { - req = { - headers: { - "x-refresh-token": "valid_refresh_token", - authorization: "Bearer old_auth_token", - }, - settingsService: { - getSettings: sinon.stub().resolves({ - jwtSecret: "my_secret", - refreshTokenSecret: "my_refresh_secret", - }), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - sinon.stub(jwt, "verify"); - - issueTokenStub = sinon.stub().returns("new_auth_token"); - sinon.replace({ issueToken }, "issueToken", issueTokenStub); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject if no refresh token is provided", async function () { - delete req.headers["x-refresh-token"]; - await refreshAuthToken(req, res, next); - - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal(errorMessages.NO_REFRESH_TOKEN); - expect(next.firstCall.args[0].status).to.equal(401); - }); - - it("should reject if the refresh token is invalid", async function () { - jwt.verify.yields(new Error("invalid token")); - await refreshAuthToken(req, res, next); - - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal(errorMessages.INVALID_REFRESH_TOKEN); - expect(next.firstCall.args[0].status).to.equal(401); - }); - - it("should reject if the refresh token is expired", async function () { - const error = new Error("Token expired"); - error.name = "TokenExpiredError"; - jwt.verify.yields(error); - await refreshAuthToken(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal(errorMessages.EXPIRED_REFRESH_TOKEN); - expect(next.firstCall.args[0].status).to.equal(401); - }); - - it("should reject if settingsService.getSettings fails", async function () { - req.settingsService.getSettings.rejects(new Error("settingsService.getSettings error")); - await refreshAuthToken(req, res, next); - - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("settingsService.getSettings error"); - }); - - it("should generate a new auth token if the refresh token is valid", async function () { - const decodedPayload = { expiresIn: "60" }; - jwt.verify.callsFake(() => { - return decodedPayload; - }); - await refreshAuthToken(req, res, next); - - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: successMessages.AUTH_TOKEN_REFRESHED, - data: { - user: decodedPayload, - token: sinon.match.string, - refreshToken: "valid_refresh_token", - }, - }) - ).to.be.true; - }); -}); - -describe("Auth Controller - editUser", function () { - let req, res, next, stub, user; - - beforeEach(function () { - req = { - params: { userId: "123" }, - body: { password: "Password1!", newPassword: "Password2!" }, - headers: { authorization: "Bearer token" }, - user: { _id: "123" }, - settingsService: { - getSettings: sinon.stub().returns({ jwtSecret: "my_secret" }), - }, - db: { - getUserByEmail: sinon.stub(), - updateUser: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - stub = sinon.stub(jwt, "verify").returns({ email: "test@example.com" }); - }); - - afterEach(function () { - sinon.restore(); - stub.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - req.params = {}; - await editUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if body validation fails", async function () { - req.body = { invalid: 1 }; - await editUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if param.userId !== req.user._id", async function () { - req.params = { userId: "456" }; - await editUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(401); - }); - - it("should reject with an error if !req.body.password and getUserByEmail fails", async function () { - req.db.getUserByEmail.rejects(new Error("getUserByEmail error")); - await editUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("getUserByEmail error"); - }); - - it("should reject with an error if user.comparePassword fails", async function () { - req.db.getUserByEmail.returns({ - comparePassword: sinon.stub().rejects(new Error("Bad Password Match")), - }); - await editUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Bad Password Match"); - }); - - it("should reject with an error if user.comparePassword returns false", async function () { - req.db.getUserByEmail.returns({ - comparePassword: sinon.stub().returns(false), - }); - await editUser(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(401); - expect(next.firstCall.args[0].message).to.equal(errorMessages.AUTH_INCORRECT_PASSWORD); - }); - - it("should edit a user if it receives a proper request", async function () { - const user = { - comparePassword: sinon.stub().resolves(true), - }; - req.db.getUserByEmail.resolves(user); - req.db.updateUser.resolves({ email: "test@example.com" }); - - await editUser(req, res, next); - - expect(req.db.getUserByEmail.calledOnce).to.be.true; - expect(req.db.updateUser.calledOnce).to.be.true; - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: successMessages.AUTH_UPDATE_USER, - data: { email: "test@example.com" }, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); - - it("should edit a user if it receives a proper request and both password fields are undefined", async function () { - req.body.password = undefined; - req.body.newPassword = undefined; - req.db.getUserByEmail.resolves(user); - req.db.updateUser.resolves({ email: "test@example.com" }); - - await editUser(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: successMessages.AUTH_UPDATE_USER, - data: { email: "test@example.com" }, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); - - it("should reject an edit request if password format is incorrect", async function () { - req.body = { password: "bad_password", newPassword: "bad_password" }; - const user = { - comparePassword: sinon.stub().resolves(true), - }; - req.db.getUserByEmail.resolves(user); - - await editUser(req, res, next); - - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); -}); - -describe("Auth Controller - checkSuperadminExists", function () { - let req, res, next; - - beforeEach(function () { - req = { - db: { - checkSuperadmin: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - it("should reject with an error if checkSuperadmin fails", async function () { - req.db.checkSuperadmin.rejects(new Error("checkSuperadmin error")); - await checkSuperadminExists(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("checkSuperadmin error"); - }); - - it("should return true if a superadmin exists", async function () { - req.db.checkSuperadmin.resolves(true); - await checkSuperadminExists(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: successMessages.AUTH_SUPERADMIN_EXISTS, - data: true, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); - - it("should return false if a superadmin does not exist", async function () { - req.db.checkSuperadmin.resolves(false); - await checkSuperadminExists(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: successMessages.AUTH_SUPERADMIN_EXISTS, - data: false, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); -}); - -describe("Auth Controller - requestRecovery", function () { - let req, res, next; - - beforeEach(function () { - req = { - body: { email: "test@test.com" }, - db: { - getUserByEmail: sinon.stub(), - requestRecoveryToken: sinon.stub(), - }, - settingsService: { - getSettings: sinon.stub().returns({ clientHost: "http://localhost" }), - }, - emailService: { - buildAndSendEmail: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - it("should reject with an error if validation fails", async function () { - req.body = {}; - await requestRecovery(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if getUserByEmail fails", async function () { - req.db.getUserByEmail.rejects(new Error("getUserByEmail error")); - await requestRecovery(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("getUserByEmail error"); - }); - - it("should throw an error if the user is not found", async function () { - req.db.getUserByEmail.resolves(null); - await requestRecovery(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - // expect(next.firstCall.args[0].message).to.equal( - // errorMessages.FRIENDLY_ERROR - // ); - }); - - it("should throw an error if the email is not provided", async function () { - req.body = {}; - await requestRecovery(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should return a success message if the email is provided", async function () { - const user = { firstName: "John" }; - const recoveryToken = { token: "recovery-token" }; - const msgId = "message-id"; - req.db.getUserByEmail.resolves(user); - req.db.requestRecoveryToken.resolves(recoveryToken); - req.emailService.buildAndSendEmail.resolves(msgId); - await requestRecovery(req, res, next); - expect(req.db.getUserByEmail.calledOnceWith("test@test.com")).to.be.true; - expect(req.db.requestRecoveryToken.calledOnceWith(req, res)).to.be.true; - expect( - req.emailService.buildAndSendEmail.calledOnceWith( - "passwordResetTemplate", - { - name: "John", - email: "test@test.com", - url: "http://localhost/set-new-password/recovery-token", - }, - "test@test.com", - "Checkmate Password Reset" - ) - ).to.be.true; - expect(res.status.calledOnceWith(200)).to.be.true; - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.AUTH_CREATE_RECOVERY_TOKEN, - data: msgId, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); -}); - -describe("Auth Controller - validateRecovery", function () { - let req, res, next; - - beforeEach(function () { - req = { - body: { recoveryToken: "recovery-token" }, - db: { - validateRecoveryToken: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - it("should reject with an error if validation fails", async function () { - req.body = {}; - await validateRecovery(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if validateRecoveryToken fails", async function () { - req.db.validateRecoveryToken.rejects(new Error("validateRecoveryToken error")); - await validateRecovery(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("validateRecoveryToken error"); - }); - - it("should return a success message if the token is valid", async function () { - req.db.validateRecoveryToken.resolves(); - await validateRecovery(req, res, next); - expect(res.status.calledOnceWith(200)).to.be.true; - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.AUTH_VERIFY_RECOVERY_TOKEN, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); -}); - -describe("Auth Controller - resetPassword", function () { - let req, res, next, newPasswordValidation, handleValidationError, handleError; - - beforeEach(function () { - req = { - body: { - recoveryToken: "recovery-token", - password: "Password1!", - }, - db: { - resetPassword: sinon.stub(), - }, - settingsService: { - getSettings: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - newPasswordValidation = { - validateAsync: sinon.stub(), - }; - handleValidationError = sinon.stub(); - handleError = sinon.stub(); - }); - - it("should reject with an error if validation fails", async function () { - req.body = { password: "bad_password" }; - await resetPassword(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if resetPassword fails", async function () { - const error = new Error("resetPassword error"); - newPasswordValidation.validateAsync.resolves(); - req.db.resetPassword.rejects(error); - await resetPassword(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("resetPassword error"); - }); - - it("should reset password successfully", async function () { - const user = { _doc: {} }; - const appSettings = { jwtSecret: "my_secret" }; - const token = "token"; - - newPasswordValidation.validateAsync.resolves(); - req.db.resetPassword.resolves(user); - req.settingsService.getSettings.resolves(appSettings); - - await resetPassword(req, res, next); - - expect(req.db.resetPassword.calledOnceWith(req, res)).to.be.true; - expect(req.settingsService.getSettings.calledOnce).to.be.true; - expect(res.status.calledOnceWith(200)).to.be.true; - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.AUTH_RESET_PASSWORD, - data: { user: sinon.match.object, token: sinon.match.string }, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); -}); - -describe("Auth Controller - deleteUser", function () { - let req, res, next, handleError; - - beforeEach(function () { - req = { - headers: { - authorization: "Bearer token", - }, - db: { - getUserByEmail: sinon.stub(), - getMonitorsByTeamId: sinon.stub(), - deleteJob: sinon.stub(), - deleteChecks: sinon.stub(), - deletePageSpeedChecksByMonitorId: sinon.stub(), - deleteNotificationsByMonitorId: sinon.stub(), - deleteTeam: sinon.stub(), - deleteAllOtherUsers: sinon.stub(), - deleteMonitorsByUserId: sinon.stub(), - deleteUser: sinon.stub(), - }, - jobQueue: { - deleteJob: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - - sinon.stub(jwt, "decode"); - - handleError = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should throw an error if user is not found", async function () { - jwt.decode.returns({ email: "test@example.com" }); - req.db.getUserByEmail.throws(new Error(errorMessages.DB_USER_NOT_FOUND)); - - await deleteUser(req, res, next); - - expect(req.db.getUserByEmail.calledOnceWith("test@example.com")).to.be.true; - expect(next.calledOnce).to.be.true; - expect(next.firstCall.args[0].message).to.equal(errorMessages.DB_USER_NOT_FOUND); - expect(res.status.notCalled).to.be.true; - expect(res.json.notCalled).to.be.true; - }); - - it("should delete user and associated data if user is superadmin", async function () { - const user = { - _id: "user_id", - email: "test@example.com", - role: ["superadmin"], - teamId: "team_id", - }; - const monitors = [{ _id: "monitor_id" }]; - - jwt.decode.returns({ email: "test@example.com" }); - req.db.getUserByEmail.resolves(user); - req.db.getMonitorsByTeamId.resolves({ monitors }); - - await deleteUser(req, res, next); - - expect(req.db.getUserByEmail.calledOnceWith("test@example.com")).to.be.true; - expect( - req.db.getMonitorsByTeamId.calledOnceWith({ - params: { teamId: "team_id" }, - }) - ).to.be.true; - expect(req.jobQueue.deleteJob.calledOnceWith(monitors[0])).to.be.true; - expect(req.db.deleteChecks.calledOnceWith("monitor_id")).to.be.true; - expect(req.db.deletePageSpeedChecksByMonitorId.calledOnceWith("monitor_id")).to.be.true; - expect(req.db.deleteNotificationsByMonitorId.calledOnceWith("monitor_id")).to.be.true; - expect(req.db.deleteTeam.calledOnceWith("team_id")).to.be.true; - expect(req.db.deleteAllOtherUsers.calledOnce).to.be.true; - expect(req.db.deleteMonitorsByUserId.calledOnceWith("user_id")).to.be.true; - expect(req.db.deleteUser.calledOnceWith("user_id")).to.be.true; - expect(res.status.calledOnceWith(200)).to.be.true; - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.AUTH_DELETE_USER, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); - - it("should delete user if user is not superadmin", async function () { - const user = { - _id: "user_id", - email: "test@example.com", - role: ["user"], - teamId: "team_id", - }; - - jwt.decode.returns({ email: "test@example.com" }); - req.db.getUserByEmail.resolves(user); - - await deleteUser(req, res, next); - - expect(req.db.getUserByEmail.calledOnceWith("test@example.com")).to.be.true; - expect( - req.db.getMonitorsByTeamId.calledOnceWith({ - params: { teamId: "team_id" }, - }) - ).to.be.true; - expect(req.db.deleteUser.calledOnceWith("user_id")).to.be.true; - expect(res.status.calledOnceWith(200)).to.be.true; - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.AUTH_DELETE_USER, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); - - it("should handle errors", async function () { - const error = new Error("Something went wrong"); - const SERVICE_NAME = "AuthController"; - jwt.decode.returns({ email: "test@example.com" }); - req.db.getUserByEmail.rejects(error); - await deleteUser(req, res, next); - expect(next.calledOnce).to.be.true; - expect(next.firstCall.args[0].message).to.equal("Something went wrong"); - expect(res.status.notCalled).to.be.true; - expect(res.json.notCalled).to.be.true; - }); -}); - -describe("Auth Controller - getAllUsers", function () { - let req, res, next; - - beforeEach(function () { - req = { - db: { - getAllUsers: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); // Restore the original methods after each test - }); - - it("should return 200 and all users", async function () { - const allUsers = [{ id: 1, name: "John Doe" }]; - req.db.getAllUsers.resolves(allUsers); - - await getAllUsers(req, res, next); - - expect(req.db.getAllUsers.calledOnce).to.be.true; - expect(res.status.calledOnceWith(200)).to.be.true; - expect( - res.json.calledOnceWith({ - success: true, - msg: "Got all users", - data: allUsers, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); - - it("should call next with error when an exception occurs", async function () { - const error = new Error("Something went wrong"); - req.db.getAllUsers.rejects(error); - await getAllUsers(req, res, next); - expect(req.db.getAllUsers.calledOnce).to.be.true; - expect(next.calledOnce).to.be.true; - expect(res.status.notCalled).to.be.true; - expect(res.json.notCalled).to.be.true; - }); -}); diff --git a/server/tests/controllers/checkController.test.js b/server/tests/controllers/checkController.test.js deleted file mode 100755 index c5fb57d50..000000000 --- a/server/tests/controllers/checkController.test.js +++ /dev/null @@ -1,372 +0,0 @@ -import { createCheck, getChecks, getTeamChecks, deleteChecks, deleteChecksByTeamId, updateChecksTTL } from "../../controllers/checkController.js"; -import jwt from "jsonwebtoken"; -import { errorMessages, successMessages } from "../../utils/messages.js"; -import sinon from "sinon"; -describe("Check Controller - createCheck", function () { - let req, res, next, handleError; - - beforeEach(function () { - req = { - params: {}, - body: {}, - db: { - createCheck: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - handleError = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); // Restore the original methods after each test - }); - - it("should reject with a validation if params are invalid", async function () { - await createCheck(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with a validation error if body is invalid", async function () { - req.params = { - monitorId: "monitorId", - }; - await createCheck(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should call next with error if data retrieval fails", async function () { - req.params = { - monitorId: "monitorId", - }; - req.body = { - monitorId: "monitorId", - status: true, - responseTime: 100, - statusCode: 200, - message: "message", - }; - req.db.createCheck.rejects(new Error("error")); - await createCheck(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - }); - - it("should return a success message if check is created", async function () { - req.params = { - monitorId: "monitorId", - }; - req.db.createCheck.resolves({ id: "123" }); - req.body = { - monitorId: "monitorId", - status: true, - responseTime: 100, - statusCode: 200, - message: "message", - }; - await createCheck(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: successMessages.CHECK_CREATE, - data: { id: "123" }, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); -}); - -describe("Check Controller - getChecks", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: {}, - query: {}, - db: { - getChecks: sinon.stub(), - getChecksCount: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with a validation error if params are invalid", async function () { - await getChecks(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should return a success message if checks are found", async function () { - req.params = { - monitorId: "monitorId", - }; - req.db.getChecks.resolves([{ id: "123" }]); - req.db.getChecksCount.resolves(1); - await getChecks(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: successMessages.CHECK_GET, - data: { checksCount: 1, checks: [{ id: "123" }] }, - }) - ).to.be.true; - expect(next.notCalled).to.be.true; - }); - - it("should call next with error if data retrieval fails", async function () { - req.params = { - monitorId: "monitorId", - }; - req.db.getChecks.rejects(new Error("error")); - await getChecks(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - }); -}); - -describe("Check Controller - getTeamChecks", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: {}, - query: {}, - db: { - getTeamChecks: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with a validation error if params are invalid", async function () { - await getTeamChecks(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should return 200 and check data on successful validation and data retrieval", async function () { - req.params = { teamId: "1" }; - const checkData = [{ id: 1, name: "Check 1" }]; - req.db.getTeamChecks.resolves(checkData); - - await getTeamChecks(req, res, next); - expect(req.db.getTeamChecks.calledOnceWith(req)).to.be.true; - expect(res.status.calledOnceWith(200)).to.be.true; - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.CHECK_GET, - data: checkData, - }) - ).to.be.true; - }); - - it("should call next with error if data retrieval fails", async function () { - req.params = { teamId: "1" }; - req.db.getTeamChecks.rejects(new Error("Retrieval Error")); - await getTeamChecks(req, res, next); - expect(req.db.getTeamChecks.calledOnceWith(req)).to.be.true; - expect(next.firstCall.args[0]).to.be.an("error"); - expect(res.status.notCalled).to.be.true; - expect(res.json.notCalled).to.be.true; - }); -}); - -describe("Check Controller - deleteChecks", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: {}, - db: { - deleteChecks: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - await deleteChecks(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should call next with error if data retrieval fails", async function () { - req.params = { monitorId: "1" }; - req.db.deleteChecks.rejects(new Error("Deletion Error")); - await deleteChecks(req, res, next); - expect(req.db.deleteChecks.calledOnceWith(req.params.monitorId)).to.be.true; - expect(next.firstCall.args[0]).to.be.an("error"); - expect(res.status.notCalled).to.be.true; - expect(res.json.notCalled).to.be.true; - }); - - it("should delete checks successfully", async function () { - req.params = { monitorId: "123" }; - req.db.deleteChecks.resolves(1); - await deleteChecks(req, res, next); - expect(req.db.deleteChecks.calledOnceWith(req.params.monitorId)).to.be.true; - expect(res.status.calledOnceWith(200)).to.be.true; - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.CHECK_DELETE, - data: { deletedCount: 1 }, - }) - ).to.be.true; - }); -}); - -describe("Check Controller - deleteChecksByTeamId", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: {}, - db: { - deleteChecksByTeamId: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - await deleteChecksByTeamId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should call next with error if data retrieval fails", async function () { - req.params = { teamId: "1" }; - req.db.deleteChecksByTeamId.rejects(new Error("Deletion Error")); - await deleteChecksByTeamId(req, res, next); - expect(req.db.deleteChecksByTeamId.calledOnceWith(req.params.teamId)).to.be.true; - expect(next.firstCall.args[0]).to.be.an("error"); - expect(res.status.notCalled).to.be.true; - expect(res.json.notCalled).to.be.true; - }); - - it("should delete checks successfully", async function () { - req.params = { teamId: "123" }; - req.db.deleteChecksByTeamId.resolves(1); - await deleteChecksByTeamId(req, res, next); - expect(req.db.deleteChecksByTeamId.calledOnceWith(req.params.teamId)).to.be.true; - expect(res.status.calledOnceWith(200)).to.be.true; - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.CHECK_DELETE, - data: { deletedCount: 1 }, - }) - ).to.be.true; - }); -}); - -describe("Check Controller - updateCheckTTL", function () { - let stub, req, res, next; - - beforeEach(function () { - stub = sinon.stub(jwt, "verify").callsFake(() => { - return { teamId: "123" }; - }); - - req = { - body: {}, - headers: { authorization: "Bearer token" }, - settingsService: { - getSettings: sinon.stub().returns({ jwtSecret: "my_secret" }), - }, - db: { - updateChecksTTL: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - stub.restore(); - }); - - it("should reject if body validation fails", async function () { - await updateChecksTTL(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should throw a JwtError if verification fails", async function () { - stub.restore(); - req.body = { - ttl: 1, - }; - await updateChecksTTL(req, res, next); - expect(next.firstCall.args[0]).to.be.instanceOf(jwt.JsonWebTokenError); - }); - - it("should call next with error if data retrieval fails", async function () { - req.body = { - ttl: 1, - }; - req.db.updateChecksTTL.rejects(new Error("Update Error")); - await updateChecksTTL(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - }); - - it("should update TTL successfully", async function () { - req.body = { - ttl: 1, - }; - req.db.updateChecksTTL.resolves(); - await updateChecksTTL(req, res, next); - expect(req.db.updateChecksTTL.calledOnceWith("123", 1 * 86400)).to.be.true; - expect(res.status.calledOnceWith(200)).to.be.true; - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.CHECK_UPDATE_TTL, - }) - ).to.be.true; - }); -}); diff --git a/server/tests/controllers/controllerUtils.test.js b/server/tests/controllers/controllerUtils.test.js deleted file mode 100755 index a55318857..000000000 --- a/server/tests/controllers/controllerUtils.test.js +++ /dev/null @@ -1,157 +0,0 @@ -import sinon from "sinon"; - -import { handleValidationError, handleError, fetchMonitorCertificate } from "../../controllers/controllerUtils.js"; -import { expect } from "chai"; -import sslChecker from "ssl-checker"; -import { afterEach } from "node:test"; -import exp from "constants"; - -describe("controllerUtils - handleValidationError", function () { - it("should set status to 422", function () { - const error = {}; - const serviceName = "TestService"; - const result = handleValidationError(error, serviceName); - expect(result.status).to.equal(422); - }); - - it("should set service to the provided serviceName", function () { - const error = {}; - const serviceName = "TestService"; - const result = handleValidationError(error, serviceName); - expect(result.service).to.equal(serviceName); - }); - - it("should set message to error.details[0].message if present", function () { - const error = { - details: [{ message: "Detail message" }], - }; - const serviceName = "TestService"; - const result = handleValidationError(error, serviceName); - expect(result.message).to.equal("Detail message"); - }); - - it("should set message to error.message if error.details is not present", function () { - const error = { - message: "Error message", - }; - const serviceName = "TestService"; - const result = handleValidationError(error, serviceName); - expect(result.message).to.equal("Error message"); - }); - - it('should set message to "Validation Error" if neither error.details nor error.message is present', function () { - const error = {}; - const serviceName = "TestService"; - const result = handleValidationError(error, serviceName); - expect(result.message).to.equal("Validation Error"); - }); -}); - -describe("controllerUtils - handleError", function () { - it("should set stats to the provided status if error.code is undefined", function () { - const error = {}; - const serviceName = "TestService"; - const method = "testMethod"; - const status = 400; - const result = handleError(error, serviceName, method, status); - expect(result.status).to.equal(status); - }); - - it("should not overwrite error.code if it is already defined", function () { - const error = { status: 404 }; - const serviceName = "TestService"; - const method = "testMethod"; - const status = 400; - const result = handleError(error, serviceName, method, status); - expect(result.status).to.equal(404); - }); - - it("should set service to the provided serviceName if error.service is undefined", function () { - const error = {}; - const serviceName = "TestService"; - const method = "testMethod"; - const result = handleError(error, serviceName, method); - expect(result.service).to.equal(serviceName); - }); - - it("should not overwrite error.service if it is already defined", function () { - const error = { service: "ExistingService" }; - const serviceName = "TestService"; - const method = "testMethod"; - const result = handleError(error, serviceName, method); - expect(result.service).to.equal("ExistingService"); - }); - - it("should set method to the provided method if error.method is undefined", function () { - const error = {}; - const serviceName = "TestService"; - const method = "testMethod"; - const result = handleError(error, serviceName, method); - expect(result.method).to.equal(method); - }); - - it("should not overwrite error.method if it is already defined", function () { - const error = { method: "existingMethod" }; - const serviceName = "TestService"; - const method = "testMethod"; - const result = handleError(error, serviceName, method); - expect(result.method).to.equal("existingMethod"); - }); - - it("should set code to 500 if error.code is undefined and no code is provided", function () { - const error = {}; - const serviceName = "TestService"; - const method = "testMethod"; - const result = handleError(error, serviceName, method); - expect(result.status).to.equal(500); - }); -}); - -describe("controllerUtils - fetchMonitorCertificate", function () { - let sslChecker, monitor; - - beforeEach(function () { - monitor = { - url: "https://www.google.com", - }; - sslChecker = sinon.stub(); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should reject with an error if a URL does not parse", async function () { - monitor.url = "invalidurl"; - try { - await fetchMonitorCertificate(sslChecker, monitor); - } catch (error) { - expect(error).to.be.an("error"); - expect(error.message).to.equal("Invalid URL"); - } - }); - - it("should reject with an error if sslChecker throws an error", async function () { - sslChecker.rejects(new Error("Test error")); - try { - await fetchMonitorCertificate(sslChecker, monitor); - } catch (error) { - expect(error).to.be.an("error"); - expect(error.message).to.equal("Test error"); - } - }); - - it("should return a certificate if sslChecker resolves", async function () { - sslChecker.resolves({ validTo: "2022-01-01" }); - const result = await fetchMonitorCertificate(sslChecker, monitor); - expect(result).to.deep.equal({ validTo: "2022-01-01" }); - }); - - it("should throw an error if a ssl-checker returns null", async function () { - sslChecker.returns(null); - await fetchMonitorCertificate(sslChecker, monitor).catch((error) => { - expect(error).to.be.an("error"); - expect(error.message).to.equal("Certificate not found"); - }); - }); -}); diff --git a/server/tests/controllers/inviteController.test.js b/server/tests/controllers/inviteController.test.js deleted file mode 100755 index 635bdf653..000000000 --- a/server/tests/controllers/inviteController.test.js +++ /dev/null @@ -1,202 +0,0 @@ -import { issueInvitation, inviteVerifyController } from "../../controllers/inviteController.js"; -import jwt from "jsonwebtoken"; -import sinon from "sinon"; -import joi from "joi"; -describe("inviteController - issueInvitation", function () { - let req, res, next, stub; - - beforeEach(function () { - req = { - headers: { authorization: "Bearer token" }, - body: { - email: "test@test.com", - role: ["admin"], - teamId: "123", - }, - db: { requestInviteToken: sinon.stub() }, - settingsService: { getSettings: sinon.stub() }, - emailService: { buildAndSendEmail: sinon.stub() }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if role validation fails", async function () { - stub = sinon.stub(jwt, "decode").callsFake(() => { - return { role: ["bad_role"], firstname: "first_name", teamId: "1" }; - }); - await issueInvitation(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0]).to.be.instanceOf(joi.ValidationError); - expect(next.firstCall.args[0].status).to.equal(422); - stub.restore(); - }); - - it("should reject with an error if body validation fails", async function () { - stub = sinon.stub(jwt, "decode").callsFake(() => { - return { role: ["admin"], firstname: "first_name", teamId: "1" }; - }); - req.body = {}; - await issueInvitation(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - stub.restore(); - }); - - it("should reject with an error if DB operations fail", async function () { - stub = sinon.stub(jwt, "decode").callsFake(() => { - return { role: ["admin"], firstname: "first_name", teamId: "1" }; - }); - req.db.requestInviteToken.throws(new Error("DB error")); - await issueInvitation(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - stub.restore(); - }); - - it("should send an invite successfully", async function () { - const token = "token"; - const decodedToken = { - role: "admin", - firstname: "John", - teamId: "team123", - }; - const inviteToken = { token: "inviteToken" }; - const clientHost = "http://localhost"; - - stub = sinon.stub(jwt, "decode").callsFake(() => { - return decodedToken; - }); - req.db.requestInviteToken.resolves(inviteToken); - req.settingsService.getSettings.returns({ clientHost }); - req.emailService.buildAndSendEmail.resolves(); - await issueInvitation(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: "Invite sent", - data: inviteToken, - }) - ).to.be.true; - stub.restore(); - }); - - it("should send an email successfully", async function () { - const token = "token"; - const decodedToken = { - role: "admin", - firstname: "John", - teamId: "team123", - }; - const inviteToken = { token: "inviteToken" }; - const clientHost = "http://localhost"; - - stub = sinon.stub(jwt, "decode").callsFake(() => { - return decodedToken; - }); - req.db.requestInviteToken.resolves(inviteToken); - req.settingsService.getSettings.returns({ clientHost }); - req.emailService.buildAndSendEmail.resolves(); - - await issueInvitation(req, res, next); - expect(req.emailService.buildAndSendEmail.calledOnce).to.be.true; - expect( - req.emailService.buildAndSendEmail.calledWith( - "employeeActivationTemplate", - { - name: "John", - link: "http://localhost/register/inviteToken", - }, - "test@test.com", - "Welcome to Uptime Monitor" - ) - ).to.be.true; - stub.restore(); - }); - - it("should continue executing if sending an email fails", async function () { - const token = "token"; - req.emailService.buildAndSendEmail.rejects(new Error("Email error")); - const decodedToken = { - role: "admin", - firstname: "John", - teamId: "team123", - }; - const inviteToken = { token: "inviteToken" }; - const clientHost = "http://localhost"; - - stub = sinon.stub(jwt, "decode").callsFake(() => { - return decodedToken; - }); - req.db.requestInviteToken.resolves(inviteToken); - req.settingsService.getSettings.returns({ clientHost }); - await issueInvitation(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - msg: "Invite sent", - data: inviteToken, - }) - ).to.be.true; - stub.restore(); - }); -}); - -describe("inviteController - inviteVerifyController", function () { - let req, res, next; - - beforeEach(function () { - req = { - body: { token: "token" }, - db: { - getInviteToken: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if body validation fails", async function () { - req.body = {}; - await inviteVerifyController(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if DB operations fail", async function () { - req.db.getInviteToken.throws(new Error("DB error")); - await inviteVerifyController(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return 200 and invite data when validation and invite retrieval are successful", async function () { - req.db.getInviteToken.resolves({ invite: "data" }); - await inviteVerifyController(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - status: "success", - msg: "Invite verified", - data: { invite: "data" }, - }) - ).to.be.true; - expect(next.called).to.be.false; - }); -}); diff --git a/server/tests/controllers/maintenanceWindowController.test.js b/server/tests/controllers/maintenanceWindowController.test.js deleted file mode 100755 index 0ea07b389..000000000 --- a/server/tests/controllers/maintenanceWindowController.test.js +++ /dev/null @@ -1,422 +0,0 @@ -import { - createMaintenanceWindows, - getMaintenanceWindowById, - getMaintenanceWindowsByTeamId, - getMaintenanceWindowsByMonitorId, - deleteMaintenanceWindow, - editMaintenanceWindow, -} from "../../controllers/maintenanceWindowController.js"; - -import jwt from "jsonwebtoken"; -import { successMessages } from "../../utils/messages.js"; -import sinon from "sinon"; - -describe("maintenanceWindowController - createMaintenanceWindows", function () { - let req, res, next, stub; - - beforeEach(function () { - req = { - body: { - monitors: ["66ff52e7c5911c61698ac724"], - name: "window", - active: true, - start: "2024-10-11T05:27:13.747Z", - end: "2024-10-11T05:27:14.747Z", - repeat: "123", - }, - headers: { - authorization: "Bearer token", - }, - settingsService: { - getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }), - }, - db: { - createMaintenanceWindow: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if body validation fails", async function () { - stub = sinon.stub(jwt, "verify").callsFake(() => { - return { teamId: "123" }; - }); - req.body = {}; - await createMaintenanceWindows(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - stub.restore(); - }); - - it("should reject with an error if jwt.verify fails", async function () { - stub = sinon.stub(jwt, "verify").throws(new jwt.JsonWebTokenError()); - await createMaintenanceWindows(req, res, next); - expect(next.firstCall.args[0]).to.be.instanceOf(jwt.JsonWebTokenError); - stub.restore(); - }); - - it("should reject with an error DB operations fail", async function () { - stub = sinon.stub(jwt, "verify").callsFake(() => { - return { teamId: "123" }; - }); - req.db.createMaintenanceWindow.throws(new Error("DB error")); - await createMaintenanceWindows(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - stub.restore(); - }); - - it("should return success message if all operations are successful", async function () { - stub = sinon.stub(jwt, "verify").callsFake(() => { - return { teamId: "123" }; - }); - await createMaintenanceWindows(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(201); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MAINTENANCE_WINDOW_CREATE, - }) - ).to.be.true; - stub.restore(); - }); - - it("should return success message if all operations are successful with active set to undefined", async function () { - req.body.active = undefined; - stub = sinon.stub(jwt, "verify").callsFake(() => { - return { teamId: "123" }; - }); - await createMaintenanceWindows(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(201); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MAINTENANCE_WINDOW_CREATE, - }) - ).to.be.true; - stub.restore(); - }); -}); - -describe("maintenanceWindowController - getMaintenanceWindowById", function () { - let req, res, next; - - beforeEach(function () { - req = { - body: {}, - params: { - id: "123", - }, - headers: { - authorization: "Bearer token", - }, - settingsService: { - getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }), - }, - db: { - getMaintenanceWindowById: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - it("should reject if param validation fails", async function () { - req.params = {}; - await getMaintenanceWindowById(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject if DB operations fail", async function () { - req.db.getMaintenanceWindowById.throws(new Error("DB error")); - await getMaintenanceWindowById(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return success message with data if all operations are successful", async function () { - req.db.getMaintenanceWindowById.returns({ id: "123" }); - await getMaintenanceWindowById(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MAINTENANCE_WINDOW_GET_BY_ID, - data: { id: "123" }, - }) - ).to.be.true; - }); -}); - -describe("maintenanceWindowController - getMaintenanceWindowsByTeamId", function () { - let req, res, next, stub; - - beforeEach(function () { - req = { - body: {}, - params: {}, - query: {}, - headers: { - authorization: "Bearer token", - }, - settingsService: { - getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }), - }, - db: { - getMaintenanceWindowsByTeamId: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - it("should reject if query validation fails", async function () { - req.query = { - invalid: 1, - }; - await getMaintenanceWindowsByTeamId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject if jwt.verify fails", async function () { - stub = sinon.stub(jwt, "verify").throws(new jwt.JsonWebTokenError()); - await getMaintenanceWindowsByTeamId(req, res, next); - expect(next.firstCall.args[0]).to.be.instanceOf(jwt.JsonWebTokenError); - stub.restore(); - }); - - it("should reject with an error if DB operations fail", async function () { - stub = sinon.stub(jwt, "verify").callsFake(() => { - return { teamId: "123" }; - }); - req.db.getMaintenanceWindowsByTeamId.throws(new Error("DB error")); - await getMaintenanceWindowsByTeamId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - stub.restore(); - }); - - it("should return success message with data if all operations are successful", async function () { - stub = sinon.stub(jwt, "verify").callsFake(() => { - return { teamId: "123" }; - }); - req.db.getMaintenanceWindowsByTeamId.returns([{ id: "123" }]); - await getMaintenanceWindowsByTeamId(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MAINTENANCE_WINDOW_GET_BY_TEAM, - data: [{ id: jwt.verify().teamId }], - }) - ).to.be.true; - stub.restore(); - }); -}); - -describe("maintenanceWindowController - getMaintenanceWindowsByMonitorId", function () { - let req, res, next; - - beforeEach(function () { - req = { - body: {}, - params: { - monitorId: "123", - }, - query: {}, - headers: { - authorization: "Bearer token", - }, - settingsService: { - getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }), - }, - db: { - getMaintenanceWindowsByMonitorId: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject if param validation fails", async function () { - req.params = {}; - await getMaintenanceWindowsByMonitorId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if DB operations fail", async function () { - req.db.getMaintenanceWindowsByMonitorId.throws(new Error("DB error")); - await getMaintenanceWindowsByMonitorId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return success message with data if all operations are successful", async function () { - const data = [{ monitorId: "123" }]; - req.db.getMaintenanceWindowsByMonitorId.returns(data); - await getMaintenanceWindowsByMonitorId(req, res, next); - expect(req.db.getMaintenanceWindowsByMonitorId.calledOnceWith(req.params.monitorId)); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MAINTENANCE_WINDOW_GET_BY_MONITOR, - data: data, - }) - ).to.be.true; - }); -}); - -describe("maintenanceWindowController - deleteMaintenanceWindow", function () { - let req, res, next; - - beforeEach(function () { - req = { - body: {}, - params: { - id: "123", - }, - query: {}, - headers: { - authorization: "Bearer token", - }, - settingsService: { - getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }), - }, - db: { - deleteMaintenanceWindowById: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject if param validation fails", async function () { - req.params = {}; - await deleteMaintenanceWindow(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if DB operations fail", async function () { - req.db.deleteMaintenanceWindowById.throws(new Error("DB error")); - await deleteMaintenanceWindow(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return success message if all operations are successful", async function () { - await deleteMaintenanceWindow(req, res, next); - expect(req.db.deleteMaintenanceWindowById.calledOnceWith(req.params.id)); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MAINTENANCE_WINDOW_DELETE, - }) - ).to.be.true; - }); -}); - -describe("maintenanceWindowController - editMaintenanceWindow", function () { - let req, res, next; - - beforeEach(function () { - req = { - body: { - active: true, - name: "test", - }, - params: { - id: "123", - }, - query: {}, - headers: { - authorization: "Bearer token", - }, - settingsService: { - getSettings: sinon.stub().returns({ jwtSecret: "jwtSecret" }), - }, - db: { - editMaintenanceWindowById: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject if param validation fails", async function () { - req.params = {}; - await editMaintenanceWindow(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject if body validation fails", async function () { - req.body = { invalid: 1 }; - await editMaintenanceWindow(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if DB operations fail", async function () { - req.db.editMaintenanceWindowById.throws(new Error("DB error")); - await editMaintenanceWindow(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return success message with data if all operations are successful", async function () { - const data = { id: "123" }; - req.db.editMaintenanceWindowById.returns(data); - - await editMaintenanceWindow(req, res, next); - expect(req.db.editMaintenanceWindowById.calledOnceWith(req.params.id, req.body)); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MAINTENANCE_WINDOW_EDIT, - data: data, - }) - ).to.be.true; - }); -}); diff --git a/server/tests/controllers/monitorController.test.js b/server/tests/controllers/monitorController.test.js deleted file mode 100755 index 963862579..000000000 --- a/server/tests/controllers/monitorController.test.js +++ /dev/null @@ -1,1116 +0,0 @@ -import { - getAllMonitors, - getAllMonitorsWithUptimeStats, - getMonitorStatsById, - getMonitorCertificate, - getMonitorById, - getMonitorsAndSummaryByTeamId, - getMonitorsByTeamId, - createMonitor, - checkEndpointResolution, - deleteMonitor, - deleteAllMonitors, - editMonitor, - pauseMonitor, - addDemoMonitors, -} from "../../controllers/monitorController.js"; -import jwt from "jsonwebtoken"; -import sinon from "sinon"; -import { successMessages } from "../../utils/messages.js"; -import logger from "../../utils/logger.js"; -import axios from "axios"; -const SERVICE_NAME = "monitorController"; - -describe("Monitor Controller - getAllMonitors", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: {}, - query: {}, - body: {}, - db: { - getAllMonitors: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if DB operations fail", async function () { - req.db.getAllMonitors.throws(new Error("DB error")); - await getAllMonitors(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return success message and data if all operations succeed", async function () { - const data = [{ monitor: "data" }]; - req.db.getAllMonitors.returns(data); - await getAllMonitors(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_GET_ALL, - data: data, - }) - ).to.be.true; - }); -}); -describe("Monitor Controller - getAllMonitorsWithUptimeStats", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: {}, - query: {}, - body: {}, - db: { - getAllMonitorsWithUptimeStats: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if DB operations fail", async function () { - req.db.getAllMonitorsWithUptimeStats.throws(new Error("DB error")); - await getAllMonitorsWithUptimeStats(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return success message and data if all operations succeed", async function () { - const data = [{ monitor: "data" }]; - req.db.getAllMonitorsWithUptimeStats.returns(data); - await getAllMonitorsWithUptimeStats(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_GET_ALL, - data: data, - }) - ).to.be.true; - }); -}); - -describe("Monitor Controller - getMonitorStatsById", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: { - monitorId: "123", - }, - query: {}, - body: {}, - db: { - getMonitorStatsById: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - req.params = {}; - await getMonitorStatsById(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if query validation fails", async function () { - req.query = { invalid: 1 }; - await getMonitorStatsById(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if DB operations fail", async function () { - req.db.getMonitorStatsById.throws(new Error("DB error")); - await getMonitorStatsById(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return success message and data if all operations succeed", async function () { - const data = [{ monitorStats: "data" }]; - req.db.getMonitorStatsById.returns(data); - await getMonitorStatsById(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_STATS_BY_ID, - data: data, - }) - ).to.be.true; - }); -}); - -describe("Monitor Controller - getMonitorCertificate", function () { - let req, res, next, fetchMonitorCertificate; - - beforeEach(function () { - req = { - params: { - monitorId: "123", - }, - query: {}, - body: {}, - db: { - getMonitorById: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - fetchMonitorCertificate = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - req.params = {}; - await getMonitorCertificate(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if getMonitorById operation fails", async function () { - req.db.getMonitorById.throws(new Error("DB error")); - await getMonitorCertificate(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return success message and data if all operations succeed with a valid cert", async function () { - req.db.getMonitorById.returns({ url: "https://www.google.com" }); - const data = { certificate: "cert", validTo: "2024/08/08" }; - fetchMonitorCertificate.returns(data); - await getMonitorCertificate(req, res, next, fetchMonitorCertificate); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_CERTIFICATE, - data: { certificateDate: new Date(data.validTo) }, - }) - ).to.be.true; - }); - - it("should return an error if fetchMonitorCertificate fails", async function () { - req.db.getMonitorById.returns({ url: "https://www.google.com" }); - fetchMonitorCertificate.throws(new Error("Certificate error")); - await getMonitorCertificate(req, res, next, fetchMonitorCertificate); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Certificate error"); - }); -}); - -describe("Monitor Controller - getMonitorById", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: { - monitorId: "123", - }, - query: {}, - body: {}, - db: { - getMonitorById: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - req.params = {}; - await getMonitorById(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if query param validation fails", async function () { - req.query = { invalid: 1 }; - await getMonitorById(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if DB operations fail", async function () { - req.db.getMonitorById.throws(new Error("DB error")); - await getMonitorById(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return 404 if a monitor is not found", async function () { - const error = new Error("Monitor not found"); - error.status = 404; - req.db.getMonitorById.throws(error); - await getMonitorById(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(404); - }); - - it("should return success message and data if all operations succeed", async function () { - const data = { monitor: "data" }; - req.db.getMonitorById.returns(data); - await getMonitorById(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_GET_BY_ID, - data: data, - }) - ).to.be.true; - }); -}); - -describe("Monitor Controller - getMonitorsAndSummaryByTeamId", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: { - teamId: "123", - }, - query: {}, - body: {}, - db: { - getMonitorsAndSummaryByTeamId: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - req.params = {}; - await getMonitorsAndSummaryByTeamId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if query validation fails", async function () { - req.query = { invalid: 1 }; - await getMonitorsAndSummaryByTeamId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if DB operations fail", async function () { - req.db.getMonitorsAndSummaryByTeamId.throws(new Error("DB error")); - await getMonitorsAndSummaryByTeamId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return success message and data if all operations succeed", async function () { - const data = { monitors: "data", summary: "data" }; - req.db.getMonitorsAndSummaryByTeamId.returns(data); - await getMonitorsAndSummaryByTeamId(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_GET_BY_USER_ID(req.params.teamId), - data: data, - }) - ).to.be.true; - }); -}); - -describe("Monitor Controller - getMonitorsByTeamId", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: { - teamId: "123", - }, - query: {}, - body: {}, - db: { - getMonitorsByTeamId: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - req.params = {}; - await getMonitorsByTeamId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if query validation fails", async function () { - req.query = { invalid: 1 }; - await getMonitorsByTeamId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if DB operations fail", async function () { - req.db.getMonitorsByTeamId.throws(new Error("DB error")); - await getMonitorsByTeamId(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should return success message and data if all operations succeed", async function () { - const data = { monitors: "data" }; - req.db.getMonitorsByTeamId.returns(data); - await getMonitorsByTeamId(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_GET_BY_USER_ID(req.params.teamId), - data: data, - }) - ).to.be.true; - }); -}); - -describe("Monitor Controller - createMonitor", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: {}, - query: {}, - body: { - userId: "123", - teamId: "123", - name: "test_monitor", - description: "test_monitor_desc", - type: "http", - url: "https://example.com", - notifications: [{ email: "example@example.com" }], - }, - db: { - createMonitor: sinon.stub(), - createNotification: sinon.stub(), - }, - jobQueue: { - addJob: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if body validation fails", async function () { - req.body = {}; - await createMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if DB createMonitor operation fail", async function () { - req.db.createMonitor.throws(new Error("DB error")); - await createMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should reject with an error if DB createNotification operation fail", async function () { - req.db.createNotification.throws(new Error("DB error")); - req.db.createMonitor.returns({ _id: "123" }); - await createMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should reject with an error if monitor.save operation fail", async function () { - req.db.createMonitor.returns({ - _id: "123", - save: sinon.stub().throws(new Error("Monitor save error")), - }); - await createMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Monitor save error"); - }); - - it("should throw an error if addJob operation fails", async function () { - req.db.createMonitor.returns({ _id: "123", save: sinon.stub() }); - req.jobQueue.addJob.throws(new Error("Job error")); - await createMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Job error"); - }); - - it("should return success message and data if all operations succeed", async function () { - const monitor = { _id: "123", save: sinon.stub() }; - req.db.createMonitor.returns(monitor); - await createMonitor(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(201); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_CREATE, - data: monitor, - }) - ).to.be.true; - }); -}); - -describe("Monitor Controller - checkEndpointResolution", function () { - let req, res, next, axiosGetStub; - - beforeEach(function () { - req = { query: { monitorURL: "https://example.com" } }; - res = { status: sinon.stub().returnsThis(), json: sinon.stub() }; - next = sinon.stub(); - axiosGetStub = sinon.stub(axios, "get"); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should resolve the URL successfully", async function () { - axiosGetStub.resolves({ status: 200, statusText: "OK" }); - await checkEndpointResolution(req, res, next); - expect(res.status.calledWith(200)).to.be.true; - expect( - res.json.calledWith({ - success: true, - code: 200, - statusText: "OK", - msg: "URL resolved successfully", - }) - ).to.be.true; - expect(next.called).to.be.false; - }); - - it("should return an error if endpoint resolution fails", async function () { - const axiosError = new Error("resolution failed"); - axiosError.code = "ENOTFOUND"; - axiosGetStub.rejects(axiosError); - await checkEndpointResolution(req, res, next); - expect(next.calledOnce).to.be.true; - const errorPassedToNext = next.getCall(0).args[0]; - expect(errorPassedToNext).to.be.an.instanceOf(Error); - expect(errorPassedToNext.message).to.include("resolution failed"); - expect(errorPassedToNext.code).to.equal("ENOTFOUND"); - expect(errorPassedToNext.status).to.equal(500); - }); - - it("should reject with an error if query validation fails", async function () { - req.query.monitorURL = "invalid-url"; - await checkEndpointResolution(req, res, next); - expect(next.calledOnce).to.be.true; - const error = next.getCall(0).args[0]; - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - expect(error.message).to.equal('"monitorURL" must be a valid uri'); - }); -}); - -describe("Monitor Controller - deleteMonitor", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: { - monitorId: "123", - }, - query: {}, - body: {}, - db: { - deleteMonitor: sinon.stub(), - deleteChecks: sinon.stub(), - deletePageSpeedChecksByMonitorId: sinon.stub(), - deleteNotificationsByMonitorId: sinon.stub(), - }, - jobQueue: { - deleteJob: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - sinon.stub(logger, "error"); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - req.params = {}; - await deleteMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if DB deleteMonitor operation fail", async function () { - req.db.deleteMonitor.throws(new Error("DB error")); - await deleteMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should log an error if deleteJob throws an error", async function () { - const error = new Error("Job error"); - const monitor = { name: "test_monitor", _id: "123" }; - req.db.deleteMonitor.returns(monitor); - req.jobQueue.deleteJob.rejects(error); - await deleteMonitor(req, res, next); - expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal(`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`); - }); - - it("should log an error if deleteChecks throws an error", async function () { - const error = new Error("Checks error"); - const monitor = { name: "test_monitor", _id: "123" }; - req.db.deleteMonitor.returns(monitor); - req.db.deleteChecks.rejects(error); - await deleteMonitor(req, res, next); - expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal(`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`); - }); - - it("should log an error if deletePageSpeedChecksByMonitorId throws an error", async function () { - const error = new Error("PageSpeed error"); - const monitor = { name: "test_monitor", _id: "123" }; - req.db.deleteMonitor.returns(monitor); - req.db.deletePageSpeedChecksByMonitorId.rejects(error); - await deleteMonitor(req, res, next); - expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal(`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`); - }); - - it("should log an error if deleteNotificationsByMonitorId throws an error", async function () { - const error = new Error("Notifications error"); - const monitor = { name: "test_monitor", _id: "123" }; - req.db.deleteMonitor.returns(monitor); - req.db.deleteNotificationsByMonitorId.rejects(error); - await deleteMonitor(req, res, next); - expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal(`Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`); - }); - - it("should return success message if all operations succeed", async function () { - const monitor = { name: "test_monitor", _id: "123" }; - req.db.deleteMonitor.returns(monitor); - await deleteMonitor(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_DELETE, - }) - ).to.be.true; - }); -}); - -describe("Monitor Controller - deleteAllMonitors", function () { - let req, res, next, stub; - - beforeEach(function () { - stub = sinon.stub(jwt, "verify").callsFake(() => { - return { teamId: "123" }; - }); - req = { - headers: { - authorization: "Bearer token", - }, - params: { - monitorId: "123", - }, - query: {}, - body: {}, - db: { - deleteAllMonitors: sinon.stub(), - deleteChecks: sinon.stub(), - deletePageSpeedChecksByMonitorId: sinon.stub(), - deleteNotificationsByMonitorId: sinon.stub(), - }, - jobQueue: { - deleteJob: sinon.stub(), - }, - settingsService: { - getSettings: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - sinon.stub(logger, "error"); - }); - - afterEach(function () { - sinon.restore(); - stub.restore(); - }); - - it("should reject with an error if getTokenFromHeaders throws an error", async function () { - req.headers = {}; - await deleteAllMonitors(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("No auth headers"); - expect(next.firstCall.args[0].status).to.equal(500); - }); - - it("should reject with an error if token validation fails", async function () { - stub.restore(); - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); - await deleteAllMonitors(req, res, next); - expect(next.firstCall.args[0]).to.be.instanceOf(jwt.JsonWebTokenError); - }); - - it("should reject with an error if DB deleteAllMonitors operation fail", async function () { - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); - req.db.deleteAllMonitors.throws(new Error("DB error")); - await deleteAllMonitors(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should log an error if deleteChecks throws an error", async function () { - const monitors = [{ name: "test_monitor", _id: "123" }]; - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); - req.db.deleteAllMonitors.returns({ monitors, deletedCount: 1 }); - const error = new Error("Check error"); - req.db.deleteChecks.rejects(error); - await deleteAllMonitors(req, res, next); - expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal( - `Error deleting associated records for monitor ${monitors[0]._id} with name ${monitors[0].name}` - ); - }); - - it("should log an error if deletePageSpeedChecksByMonitorId throws an error", async function () { - const monitors = [{ name: "test_monitor", _id: "123" }]; - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); - req.db.deleteAllMonitors.returns({ monitors, deletedCount: 1 }); - const error = new Error("Pagespeed Check error"); - req.db.deletePageSpeedChecksByMonitorId.rejects(error); - await deleteAllMonitors(req, res, next); - expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal( - `Error deleting associated records for monitor ${monitors[0]._id} with name ${monitors[0].name}` - ); - }); - - it("should log an error if deleteNotificationsByMonitorId throws an error", async function () { - const monitors = [{ name: "test_monitor", _id: "123" }]; - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); - req.db.deleteAllMonitors.returns({ monitors, deletedCount: 1 }); - const error = new Error("Notifications Check error"); - req.db.deleteNotificationsByMonitorId.rejects(error); - await deleteAllMonitors(req, res, next); - expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal( - `Error deleting associated records for monitor ${monitors[0]._id} with name ${monitors[0].name}` - ); - }); - - it("should return success message if all operations succeed", async function () { - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); - req.db.deleteAllMonitors.returns({ - monitors: [{ name: "test_monitor", _id: "123" }], - deletedCount: 1, - }); - await deleteAllMonitors(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: "Deleted 1 monitors", - }) - ).to.be.true; - }); -}); - -describe("Monitor Controller - editMonitor", function () { - let req, res, next; - - beforeEach(function () { - req = { - headers: {}, - params: { - monitorId: "123", - }, - query: {}, - body: { - notifications: [{ email: "example@example.com" }], - }, - db: { - getMonitorById: sinon.stub(), - editMonitor: sinon.stub(), - deleteNotificationsByMonitorId: sinon.stub(), - createNotification: sinon.stub(), - }, - jobQueue: { - deleteJob: sinon.stub(), - addJob: sinon.stub(), - }, - settingsService: { - getSettings: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - req.params = {}; - await editMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if body validation fails", async function () { - req.body = { invalid: 1 }; - await editMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if getMonitorById operation fails", async function () { - req.db.getMonitorById.throws(new Error("DB error")); - await editMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should reject with an error if editMonitor operation fails", async function () { - req.db.getMonitorById.returns({ teamId: "123" }); - req.db.editMonitor.throws(new Error("DB error")); - await editMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should reject with an error if deleteNotificationsByMonitorId operation fails", async function () { - req.db.getMonitorById.returns({ teamId: "123" }); - req.db.editMonitor.returns({ _id: "123" }); - req.db.deleteNotificationsByMonitorId.throws(new Error("DB error")); - await editMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should reject with an error if createNotification operation fails", async function () { - req.db.getMonitorById.returns({ teamId: "123" }); - req.db.editMonitor.returns({ _id: "123" }); - req.db.createNotification.throws(new Error("DB error")); - await editMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should reject with an error if deleteJob operation fails", async function () { - req.db.getMonitorById.returns({ teamId: "123" }); - req.db.editMonitor.returns({ _id: "123" }); - req.jobQueue.deleteJob.throws(new Error("Job error")); - await editMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Job error"); - }); - - it("should reject with an error if addJob operation fails", async function () { - req.db.getMonitorById.returns({ teamId: "123" }); - req.db.editMonitor.returns({ _id: "123" }); - req.jobQueue.addJob.throws(new Error("Add Job error")); - await editMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Add Job error"); - }); - - it("should return success message with data if all operations succeed", async function () { - const monitor = { _id: "123" }; - req.db.getMonitorById.returns({ teamId: "123" }); - req.db.editMonitor.returns(monitor); - await editMonitor(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_EDIT, - data: monitor, - }) - ).to.be.true; - }); -}); - -describe("Monitor Controller - pauseMonitor", function () { - let req, res, next; - - beforeEach(function () { - req = { - headers: {}, - params: { - monitorId: "123", - }, - query: {}, - body: {}, - db: { - getMonitorById: sinon.stub(), - }, - jobQueue: { - deleteJob: sinon.stub(), - addJob: sinon.stub(), - }, - settingsService: { - getSettings: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should reject with an error if param validation fails", async function () { - req.params = {}; - await pauseMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if getMonitorById operation fails", async function () { - req.db.getMonitorById.throws(new Error("DB error")); - await pauseMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should reject with an error if deleteJob operation fails", async function () { - const monitor = { _id: req.params.monitorId, isActive: true }; - req.db.getMonitorById.returns(monitor); - req.jobQueue.deleteJob.throws(new Error("Delete Job error")); - await pauseMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Delete Job error"); - }); - - it("should reject with an error if addJob operation fails", async function () { - const monitor = { _id: req.params.monitorId, isActive: false }; - req.db.getMonitorById.returns(monitor); - req.jobQueue.addJob.throws(new Error("Add Job error")); - await pauseMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Add Job error"); - }); - - it("should reject with an error if monitor.save operation fails", async function () { - const monitor = { - _id: req.params.monitorId, - active: false, - save: sinon.stub().throws(new Error("Save error")), - }; - req.db.getMonitorById.returns(monitor); - await pauseMonitor(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Save error"); - }); - - it("should return success pause message with data if all operations succeed with inactive monitor", async function () { - const monitor = { - _id: req.params.monitorId, - isActive: false, - save: sinon.stub().resolves(), - }; - req.db.getMonitorById.returns(monitor); - await pauseMonitor(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_PAUSE, - data: monitor, - }) - ).to.be.true; - }); - - it("should return success resume message with data if all operations succeed with active monitor", async function () { - const monitor = { - _id: req.params.monitorId, - isActive: true, - save: sinon.stub().resolves(), - }; - req.db.getMonitorById.returns(monitor); - await pauseMonitor(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_RESUME, - data: monitor, - }) - ).to.be.true; - }); -}); - -describe("Monitor Controller - addDemoMonitors", function () { - let req, res, next, stub; - - beforeEach(function () { - stub = sinon.stub(jwt, "verify").callsFake(() => { - return { _id: "123", teamId: "123" }; - }); - req = { - headers: { - authorization: "Bearer token", - }, - params: {}, - query: {}, - body: {}, - db: { - addDemoMonitors: sinon.stub(), - }, - settingsService: { - getSettings: sinon.stub(), - }, - jobQueue: { - addJob: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - stub.restore(); - }); - - it("should reject with an error if getTokenFromHeaders fails", async function () { - req.headers = {}; - await addDemoMonitors(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("No auth headers"); - expect(next.firstCall.args[0].status).to.equal(500); - }); - - it("should reject with an error if getting settings fails", async function () { - req.settingsService.getSettings.throws(new Error("Settings error")); - await addDemoMonitors(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Settings error"); - }); - - it("should reject with an error if JWT validation fails", async function () { - stub.restore(); - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); - await addDemoMonitors(req, res, next); - expect(next.firstCall.args[0]).to.be.instanceOf(jwt.JsonWebTokenError); - }); - - it("should reject with an error if addDemoMonitors operation fails", async function () { - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); - req.db.addDemoMonitors.throws(new Error("DB error")); - await addDemoMonitors(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("DB error"); - }); - - it("should reject with an error if addJob operation fails", async function () { - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); - req.db.addDemoMonitors.returns([{ _id: "123" }]); - req.jobQueue.addJob.throws(new Error("Add Job error")); - await addDemoMonitors(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("Add Job error"); - }); - - it("should return success message with data if all operations succeed", async function () { - const monitors = [{ _id: "123" }]; - req.settingsService.getSettings.returns({ jwtSecret: "my_secret" }); - req.db.addDemoMonitors.returns(monitors); - await addDemoMonitors(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect( - res.json.calledOnceWith({ - success: true, - msg: successMessages.MONITOR_DEMO_ADDED, - data: monitors.length, - }) - ).to.be.true; - }); -}); diff --git a/server/tests/controllers/queueController.test.js b/server/tests/controllers/queueController.test.js deleted file mode 100755 index 6201bdb5a..000000000 --- a/server/tests/controllers/queueController.test.js +++ /dev/null @@ -1,176 +0,0 @@ -import { afterEach } from "node:test"; -import { getMetrics, getJobs, addJob, obliterateQueue } from "../../controllers/queueController.js"; -import { successMessages } from "../../utils/messages.js"; -import sinon from "sinon"; - -describe("Queue Controller - getMetrics", function () { - let req, res, next; - - beforeEach(function () { - req = { - headers: {}, - params: {}, - body: {}, - db: {}, - jobQueue: { - getMetrics: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should throw an error if getMetrics throws an error", async function () { - req.jobQueue.getMetrics.throws(new Error("getMetrics error")); - await getMetrics(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("getMetrics error"); - }); - - it("should return a success message and data if getMetrics is successful", async function () { - const data = { data: "metrics" }; - req.jobQueue.getMetrics.returns(data); - await getMetrics(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect(res.json.firstCall.args[0]).to.deep.equal({ - success: true, - msg: successMessages.QUEUE_GET_METRICS, - data, - }); - }); -}); - -describe("Queue Controller - getJobs", function () { - let req, res, next; - - beforeEach(function () { - req = { - headers: {}, - params: {}, - body: {}, - db: {}, - jobQueue: { - getJobStats: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should reject with an error if getJobs throws an error", async function () { - req.jobQueue.getJobStats.throws(new Error("getJobs error")); - await getJobs(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("getJobs error"); - }); - - it("should return a success message and data if getJobs is successful", async function () { - const data = { data: "jobs" }; - req.jobQueue.getJobStats.returns(data); - await getJobs(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect(res.json.firstCall.args[0]).to.deep.equal({ - success: true, - msg: successMessages.QUEUE_GET_METRICS, - data, - }); - }); -}); - -describe("Queue Controller - addJob", function () { - let req, res, next; - - beforeEach(function () { - req = { - headers: {}, - params: {}, - body: {}, - db: {}, - jobQueue: { - addJob: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should reject with an error if addJob throws an error", async function () { - req.jobQueue.addJob.throws(new Error("addJob error")); - await addJob(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("addJob error"); - }); - - it("should return a success message if addJob is successful", async function () { - req.jobQueue.addJob.resolves(); - await addJob(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect(res.json.firstCall.args[0]).to.deep.equal({ - success: true, - msg: successMessages.QUEUE_ADD_JOB, - }); - }); -}); - -describe("Queue Controller - obliterateQueue", function () { - let req, res, next; - - beforeEach(function () { - req = { - headers: {}, - params: {}, - body: {}, - db: {}, - jobQueue: { - obliterate: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should reject with an error if obliterateQueue throws an error", async function () { - req.jobQueue.obliterate.throws(new Error("obliterateQueue error")); - await obliterateQueue(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("obliterateQueue error"); - }); - - it("should return a success message if obliterateQueue is successful", async function () { - req.jobQueue.obliterate.resolves(); - await obliterateQueue(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect(res.json.firstCall.args[0]).to.deep.equal({ - success: true, - msg: successMessages.QUEUE_OBLITERATE, - }); - }); -}); diff --git a/server/tests/controllers/settingsController.test.js b/server/tests/controllers/settingsController.test.js deleted file mode 100755 index 721c41c02..000000000 --- a/server/tests/controllers/settingsController.test.js +++ /dev/null @@ -1,109 +0,0 @@ -import { afterEach } from "node:test"; -import { getAppSettings, updateAppSettings } from "../../controllers/settingsController.js"; - -import { successMessages } from "../../utils/messages.js"; -import sinon from "sinon"; - -describe("Settings Controller - getAppSettings", function () { - let req, res, next; - - beforeEach(function () { - req = { - headers: {}, - params: {}, - body: {}, - db: {}, - settingsService: { - getSettings: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should throw an error if getSettings throws an error", async function () { - req.settingsService.getSettings.throws(new Error("getSettings error")); - await getAppSettings(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("getSettings error"); - }); - - it("should return a success message and data if getSettings is successful", async function () { - const data = { data: "settings" }; - req.settingsService.getSettings.returns(data); - await getAppSettings(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect(res.json.firstCall.args[0]).to.deep.equal({ - success: true, - msg: successMessages.GET_APP_SETTINGS, - data, - }); - }); -}); - -describe("Settings Controller - updateAppSettings", function () { - let req, res, next; - - beforeEach(function () { - req = { - headers: {}, - params: {}, - body: {}, - db: { - updateAppSettings: sinon.stub(), - }, - settingsService: { - reloadSettings: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should reject with an error if body validation fails", async function () { - req.body = { invalid: 1 }; - await updateAppSettings(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].status).to.equal(422); - }); - - it("should reject with an error if updateAppSettings throws an error", async function () { - req.db.updateAppSettings.throws(new Error("updateAppSettings error")); - await updateAppSettings(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("updateAppSettings error"); - }); - - it("should reject with an error if reloadSettings throws an error", async function () { - req.settingsService.reloadSettings.throws(new Error("reloadSettings error")); - await updateAppSettings(req, res, next); - expect(next.firstCall.args[0]).to.be.an("error"); - expect(next.firstCall.args[0].message).to.equal("reloadSettings error"); - }); - - it("should return a success message and data if updateAppSettings is successful", async function () { - const data = { data: "settings" }; - req.settingsService.reloadSettings.returns(data); - await updateAppSettings(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect(res.json.firstCall.args[0]).to.deep.equal({ - success: true, - msg: successMessages.UPDATE_APP_SETTINGS, - data, - }); - }); -}); diff --git a/server/tests/controllers/statusPageController.test.js b/server/tests/controllers/statusPageController.test.js deleted file mode 100755 index cada662e0..000000000 --- a/server/tests/controllers/statusPageController.test.js +++ /dev/null @@ -1,127 +0,0 @@ -import sinon from "sinon"; -import { createStatusPage, getStatusPageByUrl } from "../../controllers/statusPageController.js"; - -describe("statusPageController", function () { - let req, res, next; - - beforeEach(function () { - req = { - params: {}, - body: {}, - db: { - createStatusPage: sinon.stub(), - getStatusPageByUrl: sinon.stub(), - }, - }; - res = { - status: sinon.stub().returnsThis(), - json: sinon.stub(), - }; - next = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("createStatusPage", function () { - beforeEach(function () { - req.body = { - companyName: "Test Company", - url: "123456", - timezone: "America/Toronto", - color: "#000000", - theme: "light", - monitors: ["67309ca673788e808884c8ac", "67309ca673788e808884c8ac"], - }; - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should handle a validation error", async function () { - req.body = { - // Invalid data that will trigger validation error - companyName: "", - url: "", - timezone: "", - color: "invalid", - theme: "invalid", - monitors: ["invalid-id"], - }; - try { - await createStatusPage(req, res, next); - } catch (error) { - expect(error).to.be.an.instanceOf(Error); - expect(error.message).to.equal("Validation error"); - } - }); - - it("should handle a db error", async function () { - const err = new Error("DB error"); - req.db.createStatusPage.throws(err); - - try { - await createStatusPage(req, res, next); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - - it("should insert a properly formatted status page", async function () { - const result = await createStatusPage(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect(res.json.firstCall.args[0].success).to.be.true; - }); - }); - - describe("getStatusPageByUrl", function () { - beforeEach(function () { - req.params = { - url: "123456", - }; - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should handle a validation error", async function () { - req.params = { - url: "", - }; - - try { - await getStatusPageByUrl(req, res, next); - } catch (error) { - expect(error).to.be.an.instanceOf(Error); - expect(error.message).to.equal("Validation error"); - } - }); - - it("should handle a DB error", async function () { - const err = new Error("DB error"); - req.db.getStatusPageByUrl.throws(err); - - try { - await getStatusPageByUrl(req, res, next); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - - it("should return a status page", async function () { - const statusPage = { - _id: "123456", - companyName: "Test Company", - url: "123456", - }; - req.db.getStatusPageByUrl.resolves(statusPage); - const result = await getStatusPageByUrl(req, res, next); - expect(res.status.firstCall.args[0]).to.equal(200); - expect(res.json.firstCall.args[0].success).to.be.true; - expect(res.json.firstCall.args[0].data).to.deep.equal(statusPage); - }); - }); -}); diff --git a/server/tests/db/checkModule.test.js b/server/tests/db/checkModule.test.js deleted file mode 100755 index 6eef12445..000000000 --- a/server/tests/db/checkModule.test.js +++ /dev/null @@ -1,589 +0,0 @@ -import sinon from "sinon"; -import { - createCheck, - getChecksCount, - getChecks, - getTeamChecks, - deleteChecks, - deleteChecksByTeamId, - updateChecksTTL, -} from "../../db/mongo/modules/checkModule.js"; -import Check from "../../db/models/Check.js"; -import Monitor from "../../db/models/Monitor.js"; -import User from "../../db/models/User.js"; -import logger from "../../utils/logger.js"; - -describe("checkModule", function () { - describe("createCheck", function () { - let checkCountDocumentsStub, checkSaveStub, monitorFindByIdStub, monitorSaveStub; - const mockMonitor = { - _id: "123", - uptimePercentage: 0.5, - status: true, - save: () => this, - }; - const mockCheck = { active: true }; - - beforeEach(function () { - checkSaveStub = sinon.stub(Check.prototype, "save"); - checkCountDocumentsStub = sinon.stub(Check, "countDocuments"); - monitorFindByIdStub = sinon.stub(Monitor, "findById"); - monitorSaveStub = sinon.stub(Monitor.prototype, "save"); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return undefined early if no monitor is found", async function () { - monitorFindByIdStub.returns(null); - const check = await createCheck({ monitorId: "123" }); - expect(check).to.be.undefined; - }); - - it("should return a check", async function () { - monitorFindByIdStub.returns(mockMonitor); - checkSaveStub.returns(mockCheck); - monitorSaveStub.returns(mockMonitor); - const check = await createCheck({ monitorId: "123", status: true }); - expect(check).to.deep.equal(mockCheck); - }); - - it("should return a check if status is down", async function () { - mockMonitor.status = false; - monitorFindByIdStub.returns(mockMonitor); - checkSaveStub.returns(mockCheck); - monitorSaveStub.returns(mockMonitor); - const check = await createCheck({ monitorId: "123", status: false }); - expect(check).to.deep.equal(mockCheck); - }); - - it("should return a check if uptimePercentage is undefined", async function () { - mockMonitor.uptimePercentage = undefined; - monitorFindByIdStub.returns(mockMonitor); - checkSaveStub.returns(mockCheck); - monitorSaveStub.returns(mockMonitor); - const check = await createCheck({ monitorId: "123", status: true }); - expect(check).to.deep.equal(mockCheck); - }); - - it("should return a check if uptimePercentage is undefined and status is down", async function () { - mockMonitor.uptimePercentage = undefined; - monitorFindByIdStub.returns(mockMonitor); - checkSaveStub.returns(mockCheck); - monitorSaveStub.returns(mockMonitor); - const check = await createCheck({ monitorId: "123", status: false }); - expect(check).to.deep.equal(mockCheck); - }); - - it("should monitor save error", async function () { - const err = new Error("Save Error"); - monitorSaveStub.throws(err); - try { - await createCheck({ monitorId: "123" }); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - - it("should handle errors", async function () { - const err = new Error("DB Error"); - checkCountDocumentsStub.throws(err); - try { - await createCheck({ monitorId: "123" }); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("getChecksCount", function () { - let checkCountDocumentStub; - - beforeEach(function () { - checkCountDocumentStub = sinon.stub(Check, "countDocuments"); - }); - - afterEach(function () { - checkCountDocumentStub.restore(); - }); - - it("should return count with basic monitorId query", async function () { - const req = { - params: { monitorId: "test123" }, - query: {}, - }; - checkCountDocumentStub.resolves(5); - - const result = await getChecksCount(req); - - expect(result).to.equal(5); - expect(checkCountDocumentStub.calledOnce).to.be.true; - expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({ - monitorId: "test123", - }); - }); - - it("should include dateRange in query when provided", async function () { - const req = { - params: { monitorId: "test123" }, - query: { dateRange: "day" }, - }; - checkCountDocumentStub.resolves(3); - - const result = await getChecksCount(req); - - expect(result).to.equal(3); - expect(checkCountDocumentStub.firstCall.args[0]).to.have.property("createdAt"); - expect(checkCountDocumentStub.firstCall.args[0].createdAt).to.have.property("$gte"); - }); - - it('should handle "all" filter correctly', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "all" }, - }; - checkCountDocumentStub.resolves(2); - - const result = await getChecksCount(req); - - expect(result).to.equal(2); - expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({ - monitorId: "test123", - status: false, - }); - }); - - it('should handle "down" filter correctly', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "down" }, - }; - checkCountDocumentStub.resolves(2); - - const result = await getChecksCount(req); - - expect(result).to.equal(2); - expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({ - monitorId: "test123", - status: false, - }); - }); - - it('should handle "resolve" filter correctly', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "resolve" }, - }; - checkCountDocumentStub.resolves(1); - - const result = await getChecksCount(req); - - expect(result).to.equal(1); - expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({ - monitorId: "test123", - status: false, - statusCode: 5000, - }); - }); - - it("should handle unknown filter correctly", async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "unknown" }, - }; - checkCountDocumentStub.resolves(1); - - const result = await getChecksCount(req); - - expect(result).to.equal(1); - expect(checkCountDocumentStub.firstCall.args[0]).to.deep.equal({ - monitorId: "test123", - status: false, - }); - }); - - it("should combine dateRange and filter in query", async function () { - const req = { - params: { monitorId: "test123" }, - query: { - dateRange: "week", - filter: "down", - }, - }; - checkCountDocumentStub.resolves(4); - - const result = await getChecksCount(req); - - expect(result).to.equal(4); - expect(checkCountDocumentStub.firstCall.args[0]).to.have.all.keys("monitorId", "createdAt", "status"); - }); - }); - - describe("getChecks", function () { - let checkFindStub, monitorFindStub; - - beforeEach(function () { - checkFindStub = sinon.stub(Check, "find").returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns([{ id: 1 }, { id: 2 }]), - }), - }), - }); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return checks with basic monitorId query", async function () { - const req = { - params: { monitorId: "test123" }, - query: {}, - }; - - const result = await getChecks(req); - - expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); - }); - - it("should return checks with limit query", async function () { - const req = { - params: { monitorId: "test123" }, - query: { limit: 10 }, - }; - - const result = await getChecks(req); - - expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); - }); - - it("should handle pagination correctly", async function () { - const req = { - params: { monitorId: "test123" }, - query: { - page: 2, - rowsPerPage: 10, - }, - }; - - const result = await getChecks(req); - - expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); - }); - - it("should handle dateRange filter", async function () { - const req = { - params: { monitorId: "test123" }, - query: { dateRange: "week" }, - }; - const result = await getChecks(req); - - expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); - }); - - it('should handle "all" filter', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "all" }, - }; - - await getChecks(req); - const result = await getChecks(req); - expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); - }); - - it('should handle "down" filter', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "down" }, - }; - - await getChecks(req); - const result = await getChecks(req); - expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); - }); - - it('should handle "resolve" filter', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "resolve" }, - }; - - await getChecks(req); - const result = await getChecks(req); - expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); - }); - - it('should handle "unknown" filter', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "unknown" }, - }; - - await getChecks(req); - const result = await getChecks(req); - expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); - }); - - it("should handle ascending sort order", async function () { - const req = { - params: { monitorId: "test123" }, - query: { sortOrder: "asc" }, - }; - - await getChecks(req); - const result = await getChecks(req); - expect(result).to.deep.equal([{ id: 1 }, { id: 2 }]); - }); - - it("should handle error case", async function () { - const req = { - params: { monitorId: "test123" }, - query: {}, - }; - - checkFindStub.throws(new Error("Database error")); - - try { - await getChecks(req); - } catch (error) { - expect(error.message).to.equal("Database error"); - expect(error.service).to.equal("checkModule"); - expect(error.method).to.equal("getChecks"); - } - }); - }); - - describe("getTeamChecks", function () { - let checkFindStub, checkCountDocumentsStub, monitorFindStub; - const mockMonitors = [{ _id: "123" }]; - - beforeEach(function () { - monitorFindStub = sinon.stub(Monitor, "find").returns({ - select: sinon.stub().returns(mockMonitors), - }); - checkCountDocumentsStub = sinon.stub(Check, "countDocuments").returns(2); - checkFindStub = sinon.stub(Check, "find").returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns({ - select: sinon.stub().returns([{ id: 1 }, { id: 2 }]), - }), - }), - }), - }); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return checks with basic monitorId query", async function () { - const req = { - params: { teamId: "test123" }, - query: {}, - }; - - const result = await getTeamChecks(req); - expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); - }); - - it("should handle pagination correctly", async function () { - const req = { - params: { monitorId: "test123" }, - query: { limit: 1, page: 2, rowsPerPage: 10 }, - }; - - const result = await getTeamChecks(req); - expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); - }); - - it("should handle dateRange filter", async function () { - const req = { - params: { monitorId: "test123" }, - query: { dateRange: "week" }, - }; - const result = await getTeamChecks(req); - expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); - }); - - it('should handle "all" filter', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "all" }, - }; - - await getChecks(req); - const result = await getTeamChecks(req); - expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); - }); - - it('should handle "down" filter', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "down" }, - }; - - await getChecks(req); - const result = await getTeamChecks(req); - expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); - }); - - it('should handle "resolve" filter', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "resolve" }, - }; - - await getChecks(req); - const result = await getTeamChecks(req); - expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); - }); - - it('should handle "unknown" filter', async function () { - const req = { - params: { monitorId: "test123" }, - query: { filter: "unknown" }, - }; - - await getChecks(req); - const result = await getTeamChecks(req); - expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); - }); - - it("should handle ascending sort order", async function () { - const req = { - params: { monitorId: "test123" }, - query: { sortOrder: "asc" }, - }; - - await getChecks(req); - const result = await getTeamChecks(req); - expect(result).to.deep.equal({ checksCount: 2, checks: [{ id: 1 }, { id: 2 }] }); - }); - - it("should handle error case", async function () { - const req = { - params: { monitorId: "test123" }, - query: {}, - }; - - checkFindStub.throws(new Error("Database error")); - - try { - await getTeamChecks(req); - } catch (error) { - expect(error.message).to.equal("Database error"); - expect(error.service).to.equal("checkModule"); - expect(error.method).to.equal("getTeamChecks"); - } - }); - }); - - describe("deleteChecks", function () { - let checkDeleteManyStub; - - beforeEach(function () { - checkDeleteManyStub = sinon.stub(Check, "deleteMany").resolves({ deletedCount: 1 }); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return a value if a check is deleted", async function () { - const result = await deleteChecks("123"); - expect(result).to.equal(1); - }); - - it("should handle an error", async function () { - checkDeleteManyStub.throws(new Error("Database error")); - try { - await deleteChecks("123"); - } catch (error) { - expect(error.message).to.equal("Database error"); - expect(error.method).to.equal("deleteChecks"); - } - }); - }); - - describe("deleteChecksByTeamId", function () { - let mockMonitors = [{ _id: 123, save: () => this }]; - let monitorFindStub, monitorSaveStub, checkDeleteManyStub; - - beforeEach(function () { - monitorSaveStub = sinon.stub(Monitor.prototype, "save"); - monitorFindStub = sinon.stub(Monitor, "find").returns(mockMonitors); - checkDeleteManyStub = sinon.stub(Check, "deleteMany").resolves({ deletedCount: 1 }); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return a deleted count", async function () { - const result = await deleteChecksByTeamId("123"); - expect(result).to.equal(1); - }); - - it("should handle errors", async function () { - const err = new Error("DB Error"); - monitorFindStub.throws(err); - try { - const result = await deleteChecksByTeamId("123"); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("updateChecksTTL", function () { - let userUpdateManyStub; - let loggerStub; - - beforeEach(function () { - loggerStub = sinon.stub(logger, "error"); - userUpdateManyStub = sinon.stub(User, "updateMany"); - Check.collection = { dropIndex: sinon.stub(), createIndex: sinon.stub() }; - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return undefined", async function () { - const result = await updateChecksTTL("123", 10); - expect(result).to.be.undefined; - }); - - it("should log an error if dropIndex throws an error", async function () { - const err = new Error("Drop Index Error"); - Check.collection.dropIndex.throws(err); - await updateChecksTTL("123", 10); - expect(loggerStub.calledOnce).to.be.true; - expect(loggerStub.firstCall.args[0].message).to.equal(err.message); - }); - - it("should throw an error if createIndex throws an error", async function () { - const err = new Error("Create Index Error"); - Check.collection.createIndex.throws(err); - try { - await updateChecksTTL("123", 10); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - - it("should throw an error if User.updateMany throws an error", async function () { - const err = new Error("Update Many Error"); - userUpdateManyStub.throws(err); - try { - await updateChecksTTL("123", 10); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); -}); diff --git a/server/tests/db/hardwareCheckModule.test.js b/server/tests/db/hardwareCheckModule.test.js deleted file mode 100755 index 20ce197fb..000000000 --- a/server/tests/db/hardwareCheckModule.test.js +++ /dev/null @@ -1,136 +0,0 @@ -import sinon from "sinon"; -import HardwareCheck from "../../db/models/HardwareCheck.js"; -import { createHardwareCheck } from "../../db/mongo/modules/hardwareCheckModule.js"; -import Monitor from "../../db/models/Monitor.js"; -import logger from "../../utils/logger.js"; - -const mockHardwareCheck = { - data: { - cpu: { - physical_core: 4, - logical_core: 8, - frequency: 4800, - current_frequency: 1411, - temperature: [45, 50, 46, 47, 45, 50, 46, 47], - free_percent: 0.8552990910595134, - usage_percent: 0.14470090894048657, - }, - memory: { - total_bytes: 16467628032, - available_bytes: 7895044096, - used_bytes: 6599561216, - usage_percent: 0.4008, - }, - disk: [ - { - read_speed_bytes: null, - write_speed_bytes: null, - total_bytes: 931258499072, - free_bytes: 737097256960, - usage_percent: 0.1661, - }, - ], - host: { - os: "linux", - platform: "ubuntu", - kernel_version: "6.8.0-48-generic", - }, - }, - errors: [ - { - metric: ["cpu.temperature"], - err: "unable to read CPU temperature", - }, - ], -}; - -const mockMonitor = { - _id: "123", - uptimePercentage: 1, - status: true, - save: () => this, -}; - -describe("HardwareCheckModule", function () { - let hardwareCheckSaveStub, hardwareCheckCountDocumentsStub, monitorFindByIdStub, loggerStub; - - beforeEach(function () { - loggerStub = sinon.stub(logger, "error"); - hardwareCheckSaveStub = sinon.stub(HardwareCheck.prototype, "save"); - monitorFindByIdStub = sinon.stub(Monitor, "findById"); - hardwareCheckCountDocumentsStub = sinon.stub(HardwareCheck, "countDocuments"); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("createHardwareCheck", function () { - it("should return a hardware check", async function () { - hardwareCheckSaveStub.resolves(mockHardwareCheck); - monitorFindByIdStub.resolves(mockMonitor); - hardwareCheckCountDocumentsStub.resolves(1); - const hardwareCheck = await createHardwareCheck({ status: true }); - expect(hardwareCheck).to.exist; - expect(hardwareCheck).to.deep.equal(mockHardwareCheck); - }); - - it("should return a hardware check for a check with status false", async function () { - hardwareCheckSaveStub.resolves(mockHardwareCheck); - monitorFindByIdStub.resolves(mockMonitor); - hardwareCheckCountDocumentsStub.resolves(1); - const hardwareCheck = await createHardwareCheck({ status: false }); - expect(hardwareCheck).to.exist; - expect(hardwareCheck).to.deep.equal(mockHardwareCheck); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - monitorFindByIdStub.resolves(mockMonitor); - hardwareCheckSaveStub.rejects(err); - try { - await createHardwareCheck({}); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - - it("should log an error if a monitor is not found", async function () { - monitorFindByIdStub.resolves(null); - const res = await createHardwareCheck({}); - expect(loggerStub.calledOnce).to.be.true; - expect(res).to.be.null; - }); - - it("should handle a monitor with undefined uptimePercentage", async function () { - monitorFindByIdStub.resolves({ ...mockMonitor, uptimePercentage: undefined }); - hardwareCheckSaveStub.resolves(mockHardwareCheck); - hardwareCheckCountDocumentsStub.resolves(1); - const res = await createHardwareCheck({}); - expect(res).to.exist; - }); - - it("should handle a monitor with undefined uptimePercentage and true status", async function () { - monitorFindByIdStub.resolves({ - ...mockMonitor, - uptimePercentage: undefined, - }); - hardwareCheckSaveStub.resolves(mockHardwareCheck); - hardwareCheckCountDocumentsStub.resolves(1); - const res = await createHardwareCheck({ status: true }); - expect(res).to.exist; - }); - - it("should handle a monitor with undefined uptimePercentage and false status", async function () { - monitorFindByIdStub.resolves({ - ...mockMonitor, - uptimePercentage: undefined, - }); - hardwareCheckSaveStub.resolves(mockHardwareCheck); - hardwareCheckCountDocumentsStub.resolves(1); - const res = await createHardwareCheck({ status: false }); - expect(res).to.exist; - }); - }); -}); diff --git a/server/tests/db/inviteModule.test.js b/server/tests/db/inviteModule.test.js deleted file mode 100755 index e8913c983..000000000 --- a/server/tests/db/inviteModule.test.js +++ /dev/null @@ -1,103 +0,0 @@ -import sinon from "sinon"; -import InviteToken from "../../db/models/InviteToken.js"; -import { requestInviteToken, getInviteToken, getInviteTokenAndDelete } from "../../db/mongo/modules/inviteModule.js"; -import { errorMessages } from "../../utils/messages.js"; - -describe("Invite Module", function () { - const mockUserData = { - email: "test@test.com", - teamId: "123", - role: ["admin"], - token: "123", - }; - const mockInviteToken = { _id: 123, time: 123 }; - let inviteTokenDeleteManyStub, inviteTokenSaveStub, inviteTokenFindOneStub, inviteTokenFindOneAndDeleteStub; - - beforeEach(function () { - inviteTokenDeleteManyStub = sinon.stub(InviteToken, "deleteMany"); - inviteTokenSaveStub = sinon.stub(InviteToken.prototype, "save"); - inviteTokenFindOneStub = sinon.stub(InviteToken, "findOne"); - inviteTokenFindOneAndDeleteStub = sinon.stub(InviteToken, "findOneAndDelete"); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("requestInviteToken", function () { - it("should return a new invite token", async function () { - inviteTokenDeleteManyStub.resolves(); - inviteTokenSaveStub.resolves(); - const inviteToken = await requestInviteToken(mockUserData); - expect(inviteToken.email).to.equal(mockUserData.email); - expect(inviteToken.role).to.deep.equal(mockUserData.role); - expect(inviteToken.token).to.exist; - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - inviteTokenDeleteManyStub.rejects(err); - try { - await requestInviteToken(mockUserData); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("getInviteToken", function () { - it("should return an invite token", async function () { - inviteTokenFindOneStub.resolves(mockInviteToken); - const inviteToken = await getInviteToken(mockUserData.token); - expect(inviteToken).to.deep.equal(mockInviteToken); - }); - - it("should handle a token not found", async function () { - inviteTokenFindOneStub.resolves(null); - try { - await getInviteToken(mockUserData.token); - } catch (error) { - expect(error.message).to.equal(errorMessages.AUTH_INVITE_NOT_FOUND); - } - }); - - it("should handle DB errors", async function () { - const err = new Error("test error"); - inviteTokenFindOneStub.rejects(err); - try { - await getInviteToken(mockUserData.token); - } catch (error) { - expect(error).to.deep.equal(err); - expect(error.method).to.equal("getInviteToken"); - } - }); - }); - - describe("getInviteTokenAndDelete", function () { - it("should return a deleted invite", async function () { - inviteTokenFindOneAndDeleteStub.resolves(mockInviteToken); - const deletedInvite = await getInviteTokenAndDelete(mockUserData.token); - expect(deletedInvite).to.deep.equal(mockInviteToken); - }); - - it("should handle a token not found", async function () { - inviteTokenFindOneAndDeleteStub.resolves(null); - try { - await getInviteTokenAndDelete(mockUserData.token); - } catch (error) { - expect(error.message).to.equal(errorMessages.AUTH_INVITE_NOT_FOUND); - } - }); - - it("should handle DB errors", async function () { - const err = new Error("test error"); - inviteTokenFindOneAndDeleteStub.rejects(err); - try { - await getInviteTokenAndDelete(mockUserData.token); - } catch (error) { - expect(error).to.deep.equal(err); - expect(error.method).to.equal("getInviteTokenAndDelete"); - } - }); - }); -}); diff --git a/server/tests/db/maintenanceWindowModule.test.js b/server/tests/db/maintenanceWindowModule.test.js deleted file mode 100755 index 576c97f84..000000000 --- a/server/tests/db/maintenanceWindowModule.test.js +++ /dev/null @@ -1,255 +0,0 @@ -import sinon from "sinon"; -import MaintenanceWindow from "../../db/models/MaintenanceWindow.js"; -import { - createMaintenanceWindow, - getMaintenanceWindowById, - getMaintenanceWindowsByTeamId, - getMaintenanceWindowsByMonitorId, - deleteMaintenanceWindowById, - deleteMaintenanceWindowByMonitorId, - deleteMaintenanceWindowByUserId, - editMaintenanceWindowById, -} from "../../db/mongo/modules/maintenanceWindowModule.js"; - -describe("MaintenanceWindow Module", function () { - const mockMaintenanceWindow = { - monitorId: "123", - active: true, - oneTime: true, - start: 1, - end: 20000, - }; - - let mockMaintenanceWindows = [mockMaintenanceWindow]; - let maintenanceWindowSaveStub, - maintenanceWindowFindByIdStub, - maintenanceWindowCountDocumentsStub, - maintenanceWindowFindStub, - maintenanceWindowFindByIdAndDeleteStub, - maintenanceWindowDeleteManyStub, - maintenanceWindowFindByIdAndUpdateStub; - - beforeEach(function () { - maintenanceWindowSaveStub = sinon.stub(MaintenanceWindow.prototype, "save"); - maintenanceWindowFindByIdStub = sinon.stub(MaintenanceWindow, "findById"); - maintenanceWindowCountDocumentsStub = sinon.stub(MaintenanceWindow, "countDocuments"); - maintenanceWindowFindStub = sinon.stub(MaintenanceWindow, "find").returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns(mockMaintenanceWindows), - }), - }), - }); - maintenanceWindowFindByIdAndDeleteStub = sinon.stub(MaintenanceWindow, "findByIdAndDelete"); - maintenanceWindowDeleteManyStub = sinon.stub(MaintenanceWindow, "deleteMany"); - maintenanceWindowFindByIdAndUpdateStub = sinon.stub(MaintenanceWindow, "findByIdAndUpdate"); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("createMaintenanceWindow", function () { - it("should save a new maintenance window", async function () { - maintenanceWindowSaveStub.resolves(mockMaintenanceWindow); - const result = await createMaintenanceWindow(mockMaintenanceWindow); - expect(result).to.deep.equal(mockMaintenanceWindow); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - maintenanceWindowSaveStub.rejects(err); - try { - await createMaintenanceWindow(mockMaintenanceWindow); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("getMaintenanceWindowById", function () { - it("should return a maintenance window", async function () { - maintenanceWindowFindByIdStub.resolves(mockMaintenanceWindow); - const result = await getMaintenanceWindowById(mockMaintenanceWindow.id); - expect(result).to.deep.equal(mockMaintenanceWindow); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - maintenanceWindowFindByIdStub.rejects(err); - try { - await getMaintenanceWindowById(mockMaintenanceWindow.id); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("getMaintenanceWindowsByTeamId", function () { - let query; - - beforeEach(function () { - query = { - active: true, - page: 1, - rowsPerPage: 10, - field: "name", - order: "asc", - }; - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return a list of maintenance windows and count", async function () { - maintenanceWindowCountDocumentsStub.resolves(1); - const result = await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query); - expect(result).to.deep.equal({ - maintenanceWindows: mockMaintenanceWindows, - maintenanceWindowCount: 1, - }); - }); - - it("should return a list of maintenance windows and count with empty query", async function () { - query = undefined; - maintenanceWindowCountDocumentsStub.resolves(1); - const result = await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query); - expect(result).to.deep.equal({ - maintenanceWindows: mockMaintenanceWindows, - maintenanceWindowCount: 1, - }); - }); - - it("should return a list of maintenance windows and count with no pagination provided", async function () { - query.page = undefined; - query.rowsPerPage = undefined; - maintenanceWindowCountDocumentsStub.resolves(1); - const result = await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query); - expect(result).to.deep.equal({ - maintenanceWindows: mockMaintenanceWindows, - maintenanceWindowCount: 1, - }); - }); - - it("should return a list of maintenance windows and count with field and desc order", async function () { - query.order = "desc"; - maintenanceWindowCountDocumentsStub.resolves(1); - const result = await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query); - expect(result).to.deep.equal({ - maintenanceWindows: mockMaintenanceWindows, - maintenanceWindowCount: 1, - }); - }); - - it("should return a list of maintenance windows and count no field", async function () { - query.field = undefined; - maintenanceWindowCountDocumentsStub.resolves(1); - const result = await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query); - expect(result).to.deep.equal({ - maintenanceWindows: mockMaintenanceWindows, - maintenanceWindowCount: 1, - }); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - maintenanceWindowCountDocumentsStub.rejects(err); - try { - await getMaintenanceWindowsByTeamId(mockMaintenanceWindow.teamId, query); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("getMaintenanceWindowsByMonitorId", function () { - it("should return a list of maintenance windows", async function () { - maintenanceWindowFindStub.resolves(mockMaintenanceWindows); - const result = await getMaintenanceWindowsByMonitorId(mockMaintenanceWindow.monitorId); - expect(result).to.deep.equal(mockMaintenanceWindows); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - maintenanceWindowFindStub.rejects(err); - try { - await getMaintenanceWindowsByMonitorId(mockMaintenanceWindow.monitorId); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("deleteMaintenanceWindowById", function () { - it("should delete a maintenance window", async function () { - maintenanceWindowFindByIdAndDeleteStub.resolves(mockMaintenanceWindow); - const result = await deleteMaintenanceWindowById(mockMaintenanceWindow.id); - expect(result).to.deep.equal(mockMaintenanceWindow); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - maintenanceWindowFindByIdAndDeleteStub.rejects(err); - try { - await deleteMaintenanceWindowById(mockMaintenanceWindow.id); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("deleteMaintenanceWindowByMonitorId", function () { - it("should return the number of documents deleted", async function () { - maintenanceWindowDeleteManyStub.resolves({ deletedCount: 1 }); - const result = await deleteMaintenanceWindowByMonitorId(mockMaintenanceWindow.monitorId); - expect(result).to.deep.equal({ deletedCount: 1 }); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - maintenanceWindowDeleteManyStub.rejects(err); - try { - await deleteMaintenanceWindowByMonitorId(mockMaintenanceWindow.monitorId); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("deleteMaintenanceWindowByUserId", function () { - it("should return the number of documents deleted", async function () { - maintenanceWindowDeleteManyStub.resolves({ deletedCount: 1 }); - const result = await deleteMaintenanceWindowByUserId(mockMaintenanceWindow.userId); - expect(result).to.deep.equal({ deletedCount: 1 }); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - maintenanceWindowDeleteManyStub.rejects(err); - try { - await deleteMaintenanceWindowByUserId(mockMaintenanceWindow.userId); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("editMaintenanceWindowById", function () { - it("should return the updated maintenance window", async function () { - maintenanceWindowFindByIdAndUpdateStub.resolves(mockMaintenanceWindow); - const result = await editMaintenanceWindowById(mockMaintenanceWindow.id, mockMaintenanceWindow); - expect(result).to.deep.equal(mockMaintenanceWindow); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - maintenanceWindowFindByIdAndUpdateStub.rejects(err); - try { - await editMaintenanceWindowById(mockMaintenanceWindow.id, mockMaintenanceWindow); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); -}); diff --git a/server/tests/db/monitorModule.test.js b/server/tests/db/monitorModule.test.js deleted file mode 100755 index e59c352cf..000000000 --- a/server/tests/db/monitorModule.test.js +++ /dev/null @@ -1,1865 +0,0 @@ -import sinon from "sinon"; -import Monitor from "../../db/models/Monitor.js"; -import Check from "../../db/models/Check.js"; -import PageSpeedCheck from "../../db/models/PageSpeedCheck.js"; -import HardwareCheck from "../../db/models/HardwareCheck.js"; -import Notification from "../../db/models/Notification.js"; - -import { errorMessages } from "../../utils/messages.js"; -import { - getAllMonitors, - getAllMonitorsWithUptimeStats, - getMonitorStatsById, - getMonitorById, - getMonitorsAndSummaryByTeamId, - getMonitorsByTeamId, - createMonitor, - deleteMonitor, - deleteAllMonitors, - deleteMonitorsByUserId, - editMonitor, - addDemoMonitors, - calculateUptimeDuration, - getLastChecked, - getLatestResponseTime, - getAverageResponseTime, - getUptimePercentage, - getIncidents, - getMonitorChecks, - processChecksForDisplay, - groupChecksByTime, - calculateGroupStats, -} from "../../db/mongo/modules/monitorModule.js"; - -describe("monitorModule", function () { - let monitorFindStub, - monitorFindByIdStub, - monitorFindByIdAndUpdateStub, - monitorFindByIdAndDeleteStub, - monitorDeleteManyStub, - monitorCountStub, - monitorInsertManyStub, - checkFindStub, - pageSpeedCheckFindStub, - hardwareCheckFindStub; - - beforeEach(function () { - monitorFindStub = sinon.stub(Monitor, "find"); - monitorFindByIdStub = sinon.stub(Monitor, "findById"); - monitorFindByIdAndUpdateStub = sinon.stub(Monitor, "findByIdAndUpdate"); - monitorFindByIdAndDeleteStub = sinon.stub(Monitor, "findByIdAndDelete"); - monitorDeleteManyStub = sinon.stub(Monitor, "deleteMany"); - monitorCountStub = sinon.stub(Monitor, "countDocuments"); - - monitorInsertManyStub = sinon.stub(Monitor, "insertMany"); - checkFindStub = sinon.stub(Check, "find").returns({ - sort: sinon.stub(), - }); - pageSpeedCheckFindStub = sinon.stub(PageSpeedCheck, "find").returns({ - sort: sinon.stub(), - }); - hardwareCheckFindStub = sinon.stub(HardwareCheck, "find").returns({ - sort: sinon.stub(), - }); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("getAllMonitors", function () { - it("should return all monitors", async function () { - const mockMonitors = [ - { _id: "1", name: "Monitor 1", url: "test1.com" }, - { _id: "2", name: "Monitor 2", url: "test2.com" }, - ]; - monitorFindStub.returns(mockMonitors); - const result = await getAllMonitors(); - - expect(result).to.deep.equal(mockMonitors); - expect(monitorFindStub.calledOnce).to.be.true; - expect(monitorFindStub.firstCall.args).to.deep.equal([]); - }); - - it("should handle empty results", async function () { - monitorFindStub.returns([]); - const result = await getAllMonitors(); - expect(result).to.be.an("array").that.is.empty; - }); - - it("should throw error when database fails", async function () { - // Arrange - const error = new Error("Database error"); - error.service = "MonitorModule"; - error.method = "getAllMonitors"; - monitorFindStub.rejects(error); - // Act & Assert - try { - await getAllMonitors(); - expect.fail("Should have thrown an error"); - } catch (err) { - expect(err).to.equal(error); - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("getAllMonitors"); - } - }); - }); - - describe("getAllMonitorsWithUptimeStats", function () { - it("should return monitors with uptime stats for different time periods", async function () { - // Mock data - const mockMonitors = [ - { - _id: "monitor1", - type: "http", - toObject: () => ({ - _id: "monitor1", - type: "http", - name: "Test Monitor", - }), - }, - ]; - - const mockChecks = [{ status: true }, { status: true }, { status: false }, { status: true }]; - - monitorFindStub.resolves(mockMonitors); - checkFindStub.resolves(mockChecks); - - const result = await getAllMonitorsWithUptimeStats(); - - expect(result).to.be.an("array"); - expect(result).to.have.lengthOf(1); - - const monitor = result[0]; - expect(monitor).to.have.property("_id", "monitor1"); - expect(monitor).to.have.property("name", "Test Monitor"); - - // Check uptime percentages exist for all time periods - expect(monitor).to.have.property("1"); - expect(monitor).to.have.property("7"); - expect(monitor).to.have.property("30"); - expect(monitor).to.have.property("90"); - - // Verify uptime percentage calculation (3 successful out of 4 = 75%) - expect(monitor["1"]).to.equal(75); - expect(monitor["7"]).to.equal(75); - expect(monitor["30"]).to.equal(75); - expect(monitor["90"]).to.equal(75); - }); - - it("should return monitors with stats for pagespeed type", async function () { - // Mock data - const mockMonitors = [ - { - _id: "monitor1", - type: "pagespeed", - toObject: () => ({ - _id: "monitor1", - type: "pagespeed", - name: "Test Monitor", - }), - }, - ]; - - const mockChecks = [{ status: true }, { status: true }, { status: false }, { status: true }]; - - monitorFindStub.resolves(mockMonitors); - pageSpeedCheckFindStub.resolves(mockChecks); - - const result = await getAllMonitorsWithUptimeStats(); - - expect(result).to.be.an("array"); - expect(result).to.have.lengthOf(1); - - const monitor = result[0]; - expect(monitor).to.have.property("_id", "monitor1"); - expect(monitor).to.have.property("name", "Test Monitor"); - - // Check uptime percentages exist for all time periods - expect(monitor).to.have.property("1"); - expect(monitor).to.have.property("7"); - expect(monitor).to.have.property("30"); - expect(monitor).to.have.property("90"); - - // Verify uptime percentage calculation (3 successful out of 4 = 75%) - expect(monitor["1"]).to.equal(75); - expect(monitor["7"]).to.equal(75); - expect(monitor["30"]).to.equal(75); - expect(monitor["90"]).to.equal(75); - }); - - it("should return monitors with stats for hardware type", async function () { - // Mock data - const mockMonitors = [ - { - _id: "monitor1", - type: "hardware", - toObject: () => ({ - _id: "monitor1", - type: "hardware", - name: "Test Monitor", - }), - }, - ]; - - const mockChecks = [{ status: true }, { status: true }, { status: false }, { status: true }]; - - monitorFindStub.resolves(mockMonitors); - hardwareCheckFindStub.resolves(mockChecks); - - const result = await getAllMonitorsWithUptimeStats(); - - expect(result).to.be.an("array"); - expect(result).to.have.lengthOf(1); - - const monitor = result[0]; - expect(monitor).to.have.property("_id", "monitor1"); - expect(monitor).to.have.property("name", "Test Monitor"); - - // Check uptime percentages exist for all time periods - expect(monitor).to.have.property("1"); - expect(monitor).to.have.property("7"); - expect(monitor).to.have.property("30"); - expect(monitor).to.have.property("90"); - - // Verify uptime percentage calculation (3 successful out of 4 = 75%) - expect(monitor["1"]).to.equal(75); - expect(monitor["7"]).to.equal(75); - expect(monitor["30"]).to.equal(75); - expect(monitor["90"]).to.equal(75); - }); - - it("should handle errors appropriately", async function () { - // Setup stub to throw error - monitorFindStub.rejects(new Error("Database error")); - - try { - await getAllMonitorsWithUptimeStats(); - } catch (error) { - expect(error).to.be.an("error"); - expect(error.message).to.equal("Database error"); - expect(error.service).to.equal("monitorModule"); - expect(error.method).to.equal("getAllMonitorsWithUptimeStats"); - } - }); - - it("should handle empty monitor list", async function () { - monitorFindStub.resolves([]); - - const result = await getAllMonitorsWithUptimeStats(); - - expect(result).to.be.an("array"); - expect(result).to.have.lengthOf(0); - }); - - it("should handle monitor with no checks", async function () { - const mockMonitors = [ - { - _id: "monitor1", - type: "http", - toObject: () => ({ - _id: "monitor1", - type: "http", - name: "Test Monitor", - }), - }, - ]; - - monitorFindStub.resolves(mockMonitors); - checkFindStub.resolves([]); - - const result = await getAllMonitorsWithUptimeStats(); - - expect(result[0]).to.have.property("1", 0); - expect(result[0]).to.have.property("7", 0); - expect(result[0]).to.have.property("30", 0); - expect(result[0]).to.have.property("90", 0); - }); - }); - - describe("calculateUptimeDuration", function () { - let clock; - const NOW = new Date("2024-01-01T12:00:00Z").getTime(); - - beforeEach(function () { - // Fix the current time - clock = sinon.useFakeTimers(NOW); - }); - - afterEach(function () { - clock.restore(); - }); - - it("should return 0 when checks array is empty", function () { - expect(calculateUptimeDuration([])).to.equal(0); - }); - - it("should return 0 when checks array is null", function () { - expect(calculateUptimeDuration(null)).to.equal(0); - }); - - it("should calculate uptime from last down check to most recent check", function () { - const checks = [ - { status: true, createdAt: "2024-01-01T11:00:00Z" }, // Most recent - { status: true, createdAt: "2024-01-01T10:00:00Z" }, - { status: false, createdAt: "2024-01-01T09:00:00Z" }, // Last down - { status: true, createdAt: "2024-01-01T08:00:00Z" }, - ]; - - // Expected: 2 hours (from 09:00 to 11:00) = 7200000ms - expect(calculateUptimeDuration(checks)).to.equal(7200000); - }); - - it("should calculate uptime from first check when no down checks exist", function () { - const checks = [ - { status: true, createdAt: "2024-01-01T11:00:00Z" }, - { status: true, createdAt: "2024-01-01T10:00:00Z" }, - { status: true, createdAt: "2024-01-01T09:00:00Z" }, - ]; - - // Expected: Current time (12:00) - First check (09:00) = 3 hours = 10800000ms - expect(calculateUptimeDuration(checks)).to.equal(10800000); - }); - }); - - describe("getLastChecked", function () { - let clock; - const NOW = new Date("2024-01-01T12:00:00Z").getTime(); - - beforeEach(function () { - // Fix the current time - clock = sinon.useFakeTimers(NOW); - }); - - afterEach(function () { - clock.restore(); - }); - - it("should return 0 when checks array is empty", function () { - expect(getLastChecked([])).to.equal(0); - }); - - it("should return 0 when checks array is null", function () { - expect(getLastChecked(null)).to.equal(0); - }); - - it("should return time difference between now and most recent check", function () { - const checks = [ - { createdAt: "2024-01-01T11:30:00Z" }, // 30 minutes ago - { createdAt: "2024-01-01T11:00:00Z" }, - { createdAt: "2024-01-01T10:30:00Z" }, - ]; - - // Expected: 30 minutes = 1800000ms - expect(getLastChecked(checks)).to.equal(1800000); - }); - - it("should handle checks from different days", function () { - const checks = [ - { createdAt: "2023-12-31T12:00:00Z" }, // 24 hours ago - { createdAt: "2023-12-30T12:00:00Z" }, - ]; - - // Expected: 24 hours = 86400000ms - expect(getLastChecked(checks)).to.equal(86400000); - }); - }); - - describe("getLatestResponseTime", function () { - it("should return 0 when checks array is empty", function () { - expect(getLatestResponseTime([])).to.equal(0); - }); - - it("should return 0 when checks array is null", function () { - expect(getLatestResponseTime(null)).to.equal(0); - }); - - it("should return response time from most recent check", function () { - const checks = [ - { responseTime: 150, createdAt: "2024-01-01T11:30:00Z" }, // Most recent - { responseTime: 200, createdAt: "2024-01-01T11:00:00Z" }, - { responseTime: 250, createdAt: "2024-01-01T10:30:00Z" }, - ]; - - expect(getLatestResponseTime(checks)).to.equal(150); - }); - - it("should handle missing responseTime in checks", function () { - const checks = [{ createdAt: "2024-01-01T11:30:00Z" }, { responseTime: 200, createdAt: "2024-01-01T11:00:00Z" }]; - - expect(getLatestResponseTime(checks)).to.equal(0); - }); - }); - - describe("getAverageResponseTime", function () { - it("should return 0 when checks array is empty", function () { - expect(getAverageResponseTime([])).to.equal(0); - }); - - it("should return 0 when checks array is null", function () { - expect(getAverageResponseTime(null)).to.equal(0); - }); - - it("should calculate average response time from all checks", function () { - const checks = [ - { responseTime: 100, createdAt: "2024-01-01T11:30:00Z" }, - { responseTime: 200, createdAt: "2024-01-01T11:00:00Z" }, - { responseTime: 300, createdAt: "2024-01-01T10:30:00Z" }, - ]; - - // Average: (100 + 200 + 300) / 3 = 200 - expect(getAverageResponseTime(checks)).to.equal(200); - }); - - it("should handle missing responseTime in some checks", function () { - const checks = [ - { responseTime: 100, createdAt: "2024-01-01T11:30:00Z" }, - { createdAt: "2024-01-01T11:00:00Z" }, - { responseTime: 300, createdAt: "2024-01-01T10:30:00Z" }, - ]; - - // Average: (100 + 300) / 2 = 200 - expect(getAverageResponseTime(checks)).to.equal(200); - }); - - it("should return 0 when no checks have responseTime", function () { - const checks = [{ createdAt: "2024-01-01T11:30:00Z" }, { createdAt: "2024-01-01T11:00:00Z" }]; - - expect(getAverageResponseTime(checks)).to.equal(0); - }); - }); - - describe("getUptimePercentage", function () { - it("should return 0 when checks array is empty", function () { - expect(getUptimePercentage([])).to.equal(0); - }); - - it("should return 0 when checks array is null", function () { - expect(getUptimePercentage(null)).to.equal(0); - }); - - it("should return 100 when all checks are up", function () { - const checks = [{ status: true }, { status: true }, { status: true }]; - expect(getUptimePercentage(checks)).to.equal(100); - }); - - it("should return 0 when all checks are down", function () { - const checks = [{ status: false }, { status: false }, { status: false }]; - expect(getUptimePercentage(checks)).to.equal(0); - }); - - it("should calculate correct percentage for mixed status checks", function () { - const checks = [{ status: true }, { status: false }, { status: true }, { status: true }]; - // 3 up out of 4 total = 75% - expect(getUptimePercentage(checks)).to.equal(75); - }); - - it("should handle undefined status values", function () { - const checks = [{ status: true }, { status: undefined }, { status: true }]; - // 2 up out of 3 total ≈ 66.67% - expect(getUptimePercentage(checks)).to.equal((2 / 3) * 100); - }); - }); - - describe("getIncidents", function () { - it("should return 0 when checks array is empty", function () { - expect(getIncidents([])).to.equal(0); - }); - - it("should return 0 when checks array is null", function () { - expect(getIncidents(null)).to.equal(0); - }); - - it("should return 0 when all checks are up", function () { - const checks = [{ status: true }, { status: true }, { status: true }]; - expect(getIncidents(checks)).to.equal(0); - }); - - it("should count all incidents when all checks are down", function () { - const checks = [{ status: false }, { status: false }, { status: false }]; - expect(getIncidents(checks)).to.equal(3); - }); - - it("should count correct number of incidents for mixed status checks", function () { - const checks = [{ status: true }, { status: false }, { status: true }, { status: false }, { status: true }]; - expect(getIncidents(checks)).to.equal(2); - }); - - it("should handle undefined status values", function () { - const checks = [{ status: true }, { status: undefined }, { status: false }, { status: false }]; - // Only counts explicit false values - expect(getIncidents(checks)).to.equal(2); - }); - }); - - describe("getMonitorChecks", function () { - let mockModel; - - beforeEach(function () { - // Create a mock model with chainable methods - const mockChecks = [ - { monitorId: "123", createdAt: new Date("2024-01-01") }, - { monitorId: "123", createdAt: new Date("2024-01-02") }, - ]; - - mockModel = { - find: sinon.stub().returns({ - sort: sinon.stub().returns(mockChecks), - }), - }; - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return all checks and date-ranged checks", async function () { - // Arrange - const monitorId = "123"; - const dateRange = { - start: new Date("2024-01-01"), - end: new Date("2024-01-02"), - }; - const sortOrder = -1; - - // Act - const result = await getMonitorChecks(monitorId, mockModel, dateRange, sortOrder); - - // Assert - expect(result).to.have.keys(["checksAll", "checksForDateRange"]); - - // Verify find was called with correct parameters - expect(mockModel.find.firstCall.args[0]).to.deep.equal({ monitorId }); - expect(mockModel.find.secondCall.args[0]).to.deep.equal({ - monitorId, - createdAt: { $gte: dateRange.start, $lte: dateRange.end }, - }); - - // Verify sort was called with correct parameters - const sortCalls = mockModel.find().sort.getCalls(); - sortCalls.forEach((call) => { - expect(call.args[0]).to.deep.equal({ createdAt: sortOrder }); - }); - }); - - it("should handle empty results", async function () { - // Arrange - const emptyModel = { - find: sinon.stub().returns({ - sort: sinon.stub().returns([]), - }), - }; - - // Act - const result = await getMonitorChecks( - "123", - emptyModel, - { - start: new Date(), - end: new Date(), - }, - -1 - ); - - // Assert - expect(result.checksAll).to.be.an("array").that.is.empty; - expect(result.checksForDateRange).to.be.an("array").that.is.empty; - }); - - it("should maintain sort order", async function () { - // Arrange - const sortedChecks = [ - { monitorId: "123", createdAt: new Date("2024-01-02") }, - { monitorId: "123", createdAt: new Date("2024-01-01") }, - ]; - - const sortedModel = { - find: sinon.stub().returns({ - sort: sinon.stub().returns(sortedChecks), - }), - }; - - // Act - const result = await getMonitorChecks( - "123", - sortedModel, - { - start: new Date("2024-01-01"), - end: new Date("2024-01-02"), - }, - -1 - ); - - // Assert - expect(result.checksAll[0].createdAt).to.be.greaterThan(result.checksAll[1].createdAt); - expect(result.checksForDateRange[0].createdAt).to.be.greaterThan(result.checksForDateRange[1].createdAt); - }); - }); - - describe("processChecksForDisplay", function () { - let normalizeStub; - - beforeEach(function () { - normalizeStub = sinon.stub(); - }); - - it("should return original checks when numToDisplay is not provided", function () { - const checks = [1, 2, 3, 4, 5]; - const result = processChecksForDisplay(normalizeStub, checks); - expect(result).to.deep.equal(checks); - }); - - it("should return original checks when numToDisplay is greater than checks length", function () { - const checks = [1, 2, 3]; - const result = processChecksForDisplay(normalizeStub, checks, 5); - expect(result).to.deep.equal(checks); - }); - - it("should filter checks based on numToDisplay", function () { - const checks = [1, 2, 3, 4, 5, 6]; - const result = processChecksForDisplay(normalizeStub, checks, 3); - // Should return [1, 3, 5] as n = ceil(6/3) = 2 - expect(result).to.deep.equal([1, 3, 5]); - }); - - it("should handle empty checks array", function () { - const checks = []; - const result = processChecksForDisplay(normalizeStub, checks, 3); - expect(result).to.be.an("array").that.is.empty; - }); - - it("should call normalizeData when normalize is true", function () { - const checks = [1, 2, 3]; - normalizeStub.returns([10, 20, 30]); - - const result = processChecksForDisplay(normalizeStub, checks, null, true); - - expect(normalizeStub.args[0]).to.deep.equal([checks, 1, 100]); - expect(result).to.deep.equal([10, 20, 30]); - }); - - it("should handle both filtering and normalization", function () { - const checks = [1, 2, 3, 4, 5, 6]; - normalizeStub.returns([10, 30, 50]); - - const result = processChecksForDisplay(normalizeStub, checks, 3, true); - - expect(normalizeStub.args[0][0]).to.deep.equal([1, 3, 5]); - expect(result).to.deep.equal([10, 30, 50]); - }); - }); - - describe("groupChecksByTime", function () { - const mockChecks = [ - { createdAt: "2024-01-15T10:30:45Z" }, - { createdAt: "2024-01-15T10:45:15Z" }, - { createdAt: "2024-01-15T11:15:00Z" }, - { createdAt: "2024-01-16T10:30:00Z" }, - ]; - - it("should group checks by hour when dateRange is 'day'", function () { - const result = groupChecksByTime(mockChecks, "day"); - - // Get timestamps for 10:00 and 11:00 on Jan 15 - const time1 = new Date("2024-01-15T10:00:00Z").getTime(); - const time2 = new Date("2024-01-15T11:00:00Z").getTime(); - const time3 = new Date("2024-01-16T10:00:00Z").getTime(); - - expect(Object.keys(result)).to.have.lengthOf(3); - - expect(result[time1].checks).to.have.lengthOf(2); - expect(result[time2].checks).to.have.lengthOf(1); - expect(result[time3].checks).to.have.lengthOf(1); - }); - - it("should group checks by day when dateRange is not 'day'", function () { - const result = groupChecksByTime(mockChecks, "week"); - - expect(Object.keys(result)).to.have.lengthOf(2); - expect(result["2024-01-15"].checks).to.have.lengthOf(3); - expect(result["2024-01-16"].checks).to.have.lengthOf(1); - }); - - it("should handle empty checks array", function () { - const result = groupChecksByTime([], "day"); - expect(result).to.deep.equal({}); - }); - - it("should handle single check", function () { - const singleCheck = [{ createdAt: "2024-01-15T10:30:45Z" }]; - const result = groupChecksByTime(singleCheck, "day"); - - const expectedTime = new Date("2024-01-15T10:00:00Z").getTime(); - expect(Object.keys(result)).to.have.lengthOf(1); - expect(result[expectedTime].checks).to.have.lengthOf(1); - }); - - it("should skip invalid dates and process valid ones", function () { - const checksWithInvalidDate = [ - { createdAt: "invalid-date" }, - { createdAt: "2024-01-15T10:30:45Z" }, - { createdAt: null }, - { createdAt: undefined }, - { createdAt: "" }, - ]; - - const result = groupChecksByTime(checksWithInvalidDate, "day"); - - const expectedTime = new Date("2024-01-15T10:00:00Z").getTime(); - expect(Object.keys(result)).to.have.lengthOf(1); - expect(result[expectedTime].checks).to.have.lengthOf(1); - expect(result[expectedTime].checks[0].createdAt).to.equal("2024-01-15T10:30:45Z"); - }); - - it("should handle checks in same time group", function () { - const checksInSameHour = [{ createdAt: "2024-01-15T10:15:00Z" }, { createdAt: "2024-01-15T10:45:00Z" }]; - - const result = groupChecksByTime(checksInSameHour, "day"); - - const expectedTime = new Date("2024-01-15T10:00:00Z").getTime(); - expect(Object.keys(result)).to.have.lengthOf(1); - expect(result[expectedTime].checks).to.have.lengthOf(2); - }); - }); - - describe("calculateGroupStats", function () { - // Mock getUptimePercentage function - let uptimePercentageStub; - - beforeEach(function () { - uptimePercentageStub = sinon.stub(); - uptimePercentageStub.returns(95); // Default return value - }); - - it("should calculate stats correctly for a group of checks", function () { - const mockGroup = { - time: "2024-01-15", - checks: [ - { status: true, responseTime: 100 }, - { status: false, responseTime: 200 }, - { status: true, responseTime: 300 }, - ], - }; - - const result = calculateGroupStats(mockGroup, uptimePercentageStub); - - expect(result).to.deep.equal({ - time: "2024-01-15", - uptimePercentage: (2 / 3) * 100, - totalChecks: 3, - totalIncidents: 1, - avgResponseTime: 200, // (100 + 200 + 300) / 3 - }); - }); - - it("should handle empty checks array", function () { - const mockGroup = { - time: "2024-01-15", - checks: [], - }; - - const result = calculateGroupStats(mockGroup, uptimePercentageStub); - - expect(result).to.deep.equal({ - time: "2024-01-15", - uptimePercentage: 0, - totalChecks: 0, - totalIncidents: 0, - avgResponseTime: 0, - }); - }); - - it("should handle missing responseTime values", function () { - const mockGroup = { - time: "2024-01-15", - checks: [{ status: true }, { status: false, responseTime: 200 }, { status: true, responseTime: undefined }], - }; - - const result = calculateGroupStats(mockGroup, uptimePercentageStub); - - expect(result).to.deep.equal({ - time: "2024-01-15", - uptimePercentage: (2 / 3) * 100, - totalChecks: 3, - totalIncidents: 1, - avgResponseTime: 200, // 200 / 1 - }); - }); - - it("should handle all checks with status false", function () { - const mockGroup = { - time: "2024-01-15", - checks: [ - { status: false, responseTime: 100 }, - { status: false, responseTime: 200 }, - { status: false, responseTime: 300 }, - ], - }; - - const result = calculateGroupStats(mockGroup, uptimePercentageStub); - - expect(result).to.deep.equal({ - time: "2024-01-15", - uptimePercentage: 0, - totalChecks: 3, - totalIncidents: 3, - avgResponseTime: 200, - }); - }); - - it("should handle all checks with status true", function () { - const mockGroup = { - time: "2024-01-15", - checks: [ - { status: true, responseTime: 100 }, - { status: true, responseTime: 200 }, - { status: true, responseTime: 300 }, - ], - }; - - const result = calculateGroupStats(mockGroup, uptimePercentageStub); - - expect(result).to.deep.equal({ - time: "2024-01-15", - uptimePercentage: 100, - totalChecks: 3, - totalIncidents: 0, - avgResponseTime: 200, - }); - }); - }); - - describe("getMonitorStatsById", function () { - const now = new Date(); - const oneHourAgo = new Date(now - 3600000); - const twoHoursAgo = new Date(now - 7200000); - - const mockMonitor = { - _id: "monitor123", - type: "http", - name: "Test Monitor", - url: "https://test.com", - toObject: () => ({ - _id: "monitor123", - type: "http", - name: "Test Monitor", - url: "https://test.com", - }), - }; - - const mockMonitorPing = { - _id: "monitor123", - type: "ping", - name: "Test Monitor", - url: "https://test.com", - toObject: () => ({ - _id: "monitor123", - type: "http", - name: "Test Monitor", - url: "https://test.com", - }), - }; - const mockMonitorDocker = { - _id: "monitor123", - type: "docker", - name: "Test Monitor", - url: "https://test.com", - toObject: () => ({ - _id: "monitor123", - type: "http", - name: "Test Monitor", - url: "https://test.com", - }), - }; - - const checkDocs = [ - { - monitorId: "monitor123", - status: true, - responseTime: 100, - createdAt: new Date("2024-01-01T12:00:00Z"), - toObject: function () { - return { - monitorId: this.monitorId, - status: this.status, - responseTime: this.responseTime, - createdAt: this.createdAt, - }; - }, - }, - { - monitorId: "monitor123", - status: true, - responseTime: 150, - createdAt: new Date("2024-01-01T11:00:00Z"), - toObject: function () { - return { - monitorId: this.monitorId, - status: this.status, - responseTime: this.responseTime, - createdAt: this.createdAt, - }; - }, - }, - { - monitorId: "monitor123", - status: false, - responseTime: 200, - createdAt: new Date("2024-01-01T10:00:00Z"), - toObject: function () { - return { - monitorId: this.monitorId, - status: this.status, - responseTime: this.responseTime, - createdAt: this.createdAt, - }; - }, - }, - ]; - const req = { - params: { monitorId: "monitor123" }, - query: { - dateRange: "day", - sortOrder: "desc", - numToDisplay: 10, - normalize: true, - }, - }; - - beforeEach(function () { - checkFindStub.returns({ - sort: () => checkDocs, - }); - monitorFindByIdStub.returns(mockMonitor); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return monitor stats with calculated values, sort order desc", async function () { - req.query.sortOrder = "desc"; - const result = await getMonitorStatsById(req); - expect(result).to.include.keys([ - "_id", - "type", - "name", - "url", - "uptimeDuration", - "lastChecked", - "latestResponseTime", - "periodIncidents", - "periodTotalChecks", - "periodAvgResponseTime", - "periodUptime", - "aggregateData", - ]); - expect(result.latestResponseTime).to.equal(100); - expect(result.periodTotalChecks).to.equal(3); - expect(result.periodIncidents).to.equal(1); - expect(result.periodUptime).to.be.a("number"); - expect(result.aggregateData).to.be.an("array"); - }); - - it("should return monitor stats with calculated values, ping type", async function () { - monitorFindByIdStub.returns(mockMonitorPing); - req.query.sortOrder = "desc"; - const result = await getMonitorStatsById(req); - expect(result).to.include.keys([ - "_id", - "type", - "name", - "url", - "uptimeDuration", - "lastChecked", - "latestResponseTime", - "periodIncidents", - "periodTotalChecks", - "periodAvgResponseTime", - "periodUptime", - "aggregateData", - ]); - expect(result.latestResponseTime).to.equal(100); - expect(result.periodTotalChecks).to.equal(3); - expect(result.periodIncidents).to.equal(1); - expect(result.periodUptime).to.be.a("number"); - expect(result.aggregateData).to.be.an("array"); - }); - - it("should return monitor stats with calculated values, docker type", async function () { - monitorFindByIdStub.returns(mockMonitorDocker); - req.query.sortOrder = "desc"; - const result = await getMonitorStatsById(req); - expect(result).to.include.keys([ - "_id", - "type", - "name", - "url", - "uptimeDuration", - "lastChecked", - "latestResponseTime", - "periodIncidents", - "periodTotalChecks", - "periodAvgResponseTime", - "periodUptime", - "aggregateData", - ]); - expect(result.latestResponseTime).to.equal(100); - expect(result.periodTotalChecks).to.equal(3); - expect(result.periodIncidents).to.equal(1); - expect(result.periodUptime).to.be.a("number"); - expect(result.aggregateData).to.be.an("array"); - }); - - it("should return monitor stats with calculated values", async function () { - req.query.sortOrder = "asc"; - const result = await getMonitorStatsById(req); - expect(result).to.include.keys([ - "_id", - "type", - "name", - "url", - "uptimeDuration", - "lastChecked", - "latestResponseTime", - "periodIncidents", - "periodTotalChecks", - "periodAvgResponseTime", - "periodUptime", - "aggregateData", - ]); - expect(result.latestResponseTime).to.equal(100); - expect(result.periodTotalChecks).to.equal(3); - expect(result.periodIncidents).to.equal(1); - expect(result.periodUptime).to.be.a("number"); - expect(result.aggregateData).to.be.an("array"); - }); - - it("should throw error when monitor is not found", async function () { - monitorFindByIdStub.returns(Promise.resolve(null)); - - const req = { - params: { monitorId: "nonexistent" }, - }; - - try { - await getMonitorStatsById(req); - expect.fail("Should have thrown an error"); - } catch (error) { - expect(error).to.be.an("Error"); - expect(error.service).to.equal("monitorModule"); - expect(error.method).to.equal("getMonitorStatsById"); - } - }); - }); - - describe("getMonitorById", function () { - let notificationFindStub; - let monitorSaveStub; - - beforeEach(function () { - // Create stubs - notificationFindStub = sinon.stub(Notification, "find"); - monitorSaveStub = sinon.stub().resolves(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return monitor with notifications when found", async function () { - // Arrange - const monitorId = "123"; - const mockMonitor = { - _id: monitorId, - name: "Test Monitor", - save: monitorSaveStub, - }; - const mockNotifications = [ - { _id: "notif1", message: "Test notification 1" }, - { _id: "notif2", message: "Test notification 2" }, - ]; - - monitorFindByIdStub.resolves(mockMonitor); - notificationFindStub.resolves(mockNotifications); - - const result = await getMonitorById(monitorId); - expect(result._id).to.equal(monitorId); - expect(result.name).to.equal("Test Monitor"); - expect(monitorFindByIdStub.calledWith(monitorId)).to.be.true; - expect(notificationFindStub.calledWith({ monitorId })).to.be.true; - expect(monitorSaveStub.calledOnce).to.be.true; - }); - - it("should throw 404 error when monitor not found", async function () { - // Arrange - const monitorId = "nonexistent"; - monitorFindByIdStub.resolves(null); - - // Act & Assert - try { - await getMonitorById(monitorId); - expect.fail("Should have thrown an error"); - } catch (error) { - expect(error.message).to.equal(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId)); - expect(error.status).to.equal(404); - expect(error.service).to.equal("monitorModule"); - expect(error.method).to.equal("getMonitorById"); - } - }); - - it("should handle database errors properly", async function () { - // Arrange - const monitorId = "123"; - const dbError = new Error("Database connection failed"); - monitorFindByIdStub.rejects(dbError); - - // Act & Assert - try { - await getMonitorById(monitorId); - expect.fail("Should have thrown an error"); - } catch (error) { - expect(error.service).to.equal("monitorModule"); - expect(error.method).to.equal("getMonitorById"); - expect(error.message).to.equal("Database connection failed"); - } - }); - - it("should handle notification fetch errors", async function () { - // Arrange - const monitorId = "123"; - const mockMonitor = { - _id: monitorId, - name: "Test Monitor", - save: monitorSaveStub, - }; - const notificationError = new Error("Notification fetch failed"); - - monitorFindByIdStub.resolves(mockMonitor); - notificationFindStub.rejects(notificationError); - - // Act & Assert - try { - await getMonitorById(monitorId); - expect.fail("Should have thrown an error"); - } catch (error) { - expect(error.service).to.equal("monitorModule"); - expect(error.method).to.equal("getMonitorById"); - expect(error.message).to.equal("Notification fetch failed"); - } - }); - - it("should handle monitor save errors", async function () { - // Arrange - const monitorId = "123"; - const mockMonitor = { - _id: monitorId, - name: "Test Monitor", - save: sinon.stub().rejects(new Error("Save failed")), - }; - const mockNotifications = []; - - monitorFindByIdStub.resolves(mockMonitor); - notificationFindStub.resolves(mockNotifications); - - // Act & Assert - try { - await getMonitorById(monitorId); - expect.fail("Should have thrown an error"); - } catch (error) { - expect(error.service).to.equal("monitorModule"); - expect(error.method).to.equal("getMonitorById"); - expect(error.message).to.equal("Save failed"); - } - }); - }); - - describe("getMonitorsAndSummaryByTeamId", function () { - it("should return monitors and correct summary counts", async function () { - // Arrange - const teamId = "team123"; - const type = "http"; - const mockMonitors = [ - { teamId, type, status: true, isActive: true }, // up - { teamId, type, status: false, isActive: true }, // down - { teamId, type, status: null, isActive: false }, // paused - { teamId, type, status: true, isActive: true }, // up - ]; - monitorFindStub.resolves(mockMonitors); - - // Act - const result = await getMonitorsAndSummaryByTeamId(teamId, type); - - // Assert - expect(result.monitors).to.have.lengthOf(4); - expect(result.monitorCounts).to.deep.equal({ - up: 2, - down: 1, - paused: 1, - total: 4, - }); - expect(monitorFindStub.calledOnceWith({ teamId, type })).to.be.true; - }); - - it("should return empty results for non-existent team", async function () { - // Arrange - monitorFindStub.resolves([]); - - // Act - const result = await getMonitorsAndSummaryByTeamId("nonexistent", "http"); - - // Assert - expect(result.monitors).to.have.lengthOf(0); - expect(result.monitorCounts).to.deep.equal({ - up: 0, - down: 0, - paused: 0, - total: 0, - }); - }); - - it("should handle database errors", async function () { - // Arrange - const error = new Error("Database error"); - error.service = "MonitorModule"; - error.method = "getMonitorsAndSummaryByTeamId"; - monitorFindStub.rejects(error); - - // Act & Assert - try { - await getMonitorsAndSummaryByTeamId("team123", "http"); - expect.fail("Should have thrown an error"); - } catch (err) { - expect(err).to.equal(error); - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("getMonitorsAndSummaryByTeamId"); - } - }); - }); - - describe("getMonitorsByTeamId", function () { - beforeEach(function () { - // Chain stubs for Monitor.find().skip().limit().sort() - - // Stub for CHECK_MODEL_LOOKUP model find - checkFindStub.returns({ - sort: sinon.stub().returns({ - limit: sinon.stub().returns([]), - }), - }); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should return monitors with basic query parameters", async function () { - const mockMonitors = [ - { _id: "1", type: "http", toObject: () => ({ _id: "1", type: "http" }) }, - { _id: "2", type: "ping", toObject: () => ({ _id: "2", type: "ping" }) }, - ]; - monitorFindStub.returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns(mockMonitors), - }), - }), - }); - - const req = { - params: { teamId: "team123" }, - query: { - type: "http", - page: 0, - rowsPerPage: 10, - field: "name", - status: false, - checkOrder: "desc", - }, - }; - - monitorCountStub.resolves(2); - - const result = await getMonitorsByTeamId(req); - - expect(result).to.have.property("monitors"); - expect(result).to.have.property("monitorCount", 2); - }); - - it("should return monitors with basic query parameters", async function () { - const mockMonitors = [ - { _id: "1", type: "http", toObject: () => ({ _id: "1", type: "http" }) }, - { _id: "2", type: "ping", toObject: () => ({ _id: "2", type: "ping" }) }, - ]; - monitorFindStub.returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns(mockMonitors), - }), - }), - }); - - const req = { - params: { teamId: "team123" }, - query: { - type: "http", - page: 0, - rowsPerPage: 10, - field: "name", - status: true, - checkOrder: "asc", - }, - }; - - monitorCountStub.resolves(2); - - const result = await getMonitorsByTeamId(req); - - expect(result).to.have.property("monitors"); - expect(result).to.have.property("monitorCount", 2); - }); - - it("should handle type filter with array input", async function () { - const req = { - params: { teamId: "team123" }, - query: { - type: ["http", "ping"], - }, - }; - - monitorFindStub.returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns([]), - }), - }), - }); - monitorCountStub.resolves(0); - - await getMonitorsByTeamId(req); - - expect(Monitor.find.firstCall.args[0]).to.deep.equal({ - teamId: "team123", - type: { $in: ["http", "ping"] }, - }); - }); - - it("should handle text search filter", async function () { - const req = { - params: { teamId: "team123" }, - query: { - filter: "search", - }, - }; - - monitorFindStub.returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns([]), - }), - }), - }); - monitorCountStub.resolves(0); - - await getMonitorsByTeamId(req); - - expect(Monitor.find.firstCall.args[0]).to.deep.equal({ - teamId: "team123", - $or: [{ name: { $regex: "search", $options: "i" } }, { url: { $regex: "search", $options: "i" } }], - }); - }); - - it("should handle pagination parameters", async function () { - const req = { - params: { teamId: "team123" }, - query: { - page: 2, - rowsPerPage: 5, - }, - }; - - monitorFindStub.returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns([]), - }), - }), - }); - monitorCountStub.resolves(0); - - const result = await getMonitorsByTeamId(req); - expect(result).to.deep.equal({ - monitors: [], - monitorCount: 0, - }); - }); - - it("should handle sorting parameters", async function () { - const req = { - params: { teamId: "team123" }, - query: { - field: "name", - order: "asc", - }, - }; - - monitorFindStub.returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns([]), - }), - }), - }); - monitorCountStub.resolves(0); - - await getMonitorsByTeamId(req); - - const result = await getMonitorsByTeamId(req); - expect(result).to.deep.equal({ - monitors: [], - monitorCount: 0, - }); - }); - - it("should return early when limit is -1", async function () { - // Arrange - const req = { - params: { teamId: "team123" }, - query: { - limit: "-1", - }, - }; - - const mockMonitors = [ - { _id: "1", type: "http" }, - { _id: "2", type: "ping" }, - ]; - - monitorFindStub.returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns(mockMonitors), - }), - }), - }); - - monitorCountStub.resolves(2); - - // Act - const result = await getMonitorsByTeamId(req); - - // Assert - expect(result).to.deep.equal({ - monitors: mockMonitors, - monitorCount: 2, - }); - }); - - it("should normalize checks when normalize parameter is provided", async function () { - const req = { - params: { teamId: "team123" }, - query: { normalize: "true" }, - }; - monitorCountStub.resolves(2); - - const mockMonitors = [ - { _id: "1", type: "http", toObject: () => ({ _id: "1", type: "http" }) }, - { _id: "2", type: "ping", toObject: () => ({ _id: "2", type: "ping" }) }, - ]; - - monitorFindStub.returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().returns(mockMonitors), - }), - }), - }); - - const result = await getMonitorsByTeamId(req); - expect(result.monitorCount).to.equal(2); - expect(result.monitors).to.have.lengthOf(2); - }); - - it("should handle database errors", async function () { - const req = { - params: { teamId: "team123" }, - query: {}, - }; - - const error = new Error("Database error"); - monitorFindStub.returns({ - skip: sinon.stub().returns({ - limit: sinon.stub().returns({ - sort: sinon.stub().throws(error), - }), - }), - }); - - try { - await getMonitorsByTeamId(req); - expect.fail("Should have thrown an error"); - } catch (err) { - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("getMonitorsByTeamId"); - expect(err.message).to.equal("Database error"); - } - }); - }); - - describe("createMonitor", function () { - it("should create a monitor without notifications", async function () { - let monitorSaveStub = sinon.stub(Monitor.prototype, "save").resolves(); - - const req = { - body: { - name: "Test Monitor", - url: "http://test.com", - type: "http", - notifications: ["someNotification"], - }, - }; - - const expectedMonitor = { - name: "Test Monitor", - url: "http://test.com", - type: "http", - notifications: undefined, - save: monitorSaveStub, - }; - - const result = await createMonitor(req); - expect(result.name).to.equal(expectedMonitor.name); - expect(result.url).to.equal(expectedMonitor.url); - }); - - it("should handle database errors", async function () { - const req = { - body: { - name: "Test Monitor", - }, - }; - - try { - await createMonitor(req); - expect.fail("Should have thrown an error"); - } catch (err) { - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("createMonitor"); - } - }); - }); - - describe("deleteMonitor", function () { - it("should delete a monitor successfully", async function () { - const monitorId = "123456789"; - const mockMonitor = { - _id: monitorId, - name: "Test Monitor", - url: "http://test.com", - }; - - const req = { - params: { monitorId }, - }; - - monitorFindByIdAndDeleteStub.resolves(mockMonitor); - - const result = await deleteMonitor(req); - - expect(result).to.deep.equal(mockMonitor); - sinon.assert.calledWith(monitorFindByIdAndDeleteStub, monitorId); - }); - - it("should throw error when monitor not found", async function () { - const monitorId = "nonexistent123"; - const req = { - params: { monitorId }, - }; - - monitorFindByIdAndDeleteStub.resolves(null); - - try { - await deleteMonitor(req); - expect.fail("Should have thrown an error"); - } catch (err) { - expect(err.message).to.equal(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId)); - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("deleteMonitor"); - } - }); - - it("should handle database errors", async function () { - const monitorId = "123456789"; - const req = { - params: { monitorId }, - }; - - const dbError = new Error("Database connection error"); - monitorFindByIdAndDeleteStub.rejects(dbError); - - try { - await deleteMonitor(req); - expect.fail("Should have thrown an error"); - } catch (err) { - expect(err.message).to.equal("Database connection error"); - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("deleteMonitor"); - } - }); - }); - - describe("deleteAllMonitors", function () { - it("should delete all monitors for a team successfully", async function () { - const teamId = "team123"; - const mockMonitors = [ - { _id: "1", name: "Monitor 1", teamId }, - { _id: "2", name: "Monitor 2", teamId }, - ]; - - monitorFindStub.resolves(mockMonitors); - monitorDeleteManyStub.resolves({ deletedCount: 2 }); - - const result = await deleteAllMonitors(teamId); - - expect(result).to.deep.equal({ - monitors: mockMonitors, - deletedCount: 2, - }); - sinon.assert.calledWith(monitorFindStub, { teamId }); - sinon.assert.calledWith(monitorDeleteManyStub, { teamId }); - }); - - it("should return empty array when no monitors found", async function () { - const teamId = "emptyTeam"; - - monitorFindStub.resolves([]); - monitorDeleteManyStub.resolves({ deletedCount: 0 }); - - const result = await deleteAllMonitors(teamId); - - expect(result).to.deep.equal({ - monitors: [], - deletedCount: 0, - }); - sinon.assert.calledWith(monitorFindStub, { teamId }); - sinon.assert.calledWith(monitorDeleteManyStub, { teamId }); - }); - - it("should handle database errors", async function () { - const teamId = "team123"; - const dbError = new Error("Database connection error"); - monitorFindStub.rejects(dbError); - - try { - await deleteAllMonitors(teamId); - } catch (err) { - expect(err.message).to.equal("Database connection error"); - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("deleteAllMonitors"); - } - }); - - it("should handle deleteMany errors", async function () { - const teamId = "team123"; - monitorFindStub.resolves([{ _id: "1", name: "Monitor 1" }]); - monitorDeleteManyStub.rejects(new Error("Delete operation failed")); - - try { - await deleteAllMonitors(teamId); - } catch (err) { - expect(err.message).to.equal("Delete operation failed"); - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("deleteAllMonitors"); - } - }); - }); - - describe("deleteMonitorsByUserId", function () { - beforeEach(function () {}); - - afterEach(function () { - sinon.restore(); - }); - - it("should delete all monitors for a user successfully", async function () { - // Arrange - const userId = "user123"; - const mockResult = { - deletedCount: 3, - acknowledged: true, - }; - - monitorDeleteManyStub.resolves(mockResult); - - // Act - const result = await deleteMonitorsByUserId(userId); - - // Assert - expect(result).to.deep.equal(mockResult); - sinon.assert.calledWith(monitorDeleteManyStub, { userId: userId }); - }); - - it("should return zero deletedCount when no monitors found", async function () { - // Arrange - const userId = "nonexistentUser"; - const mockResult = { - deletedCount: 0, - acknowledged: true, - }; - - monitorDeleteManyStub.resolves(mockResult); - - // Act - const result = await deleteMonitorsByUserId(userId); - - // Assert - expect(result.deletedCount).to.equal(0); - sinon.assert.calledWith(monitorDeleteManyStub, { userId: userId }); - }); - - it("should handle database errors", async function () { - // Arrange - const userId = "user123"; - const dbError = new Error("Database connection error"); - monitorDeleteManyStub.rejects(dbError); - - // Act & Assert - try { - await deleteMonitorsByUserId(userId); - expect.fail("Should have thrown an error"); - } catch (err) { - expect(err.message).to.equal("Database connection error"); - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("deleteMonitorsByUserId"); - } - }); - }); - - describe("editMonitor", function () { - it("should edit a monitor successfully", async function () { - // Arrange - const candidateId = "monitor123"; - const candidateMonitor = { - name: "Updated Monitor", - url: "http://updated.com", - type: "http", - notifications: ["someNotification"], - }; - - const expectedUpdateData = { - name: "Updated Monitor", - url: "http://updated.com", - type: "http", - notifications: undefined, - }; - - const mockUpdatedMonitor = { - _id: candidateId, - ...expectedUpdateData, - }; - - monitorFindByIdAndUpdateStub.resolves(mockUpdatedMonitor); - - // Act - const result = await editMonitor(candidateId, candidateMonitor); - - // Assert - expect(result).to.deep.equal(mockUpdatedMonitor); - sinon.assert.calledWith(monitorFindByIdAndUpdateStub, candidateId, expectedUpdateData, { - new: true, - }); - }); - - it("should return null when monitor not found", async function () { - // Arrange - const candidateId = "nonexistent123"; - const candidateMonitor = { - name: "Updated Monitor", - }; - - monitorFindByIdAndUpdateStub.resolves(null); - - // Act - const result = await editMonitor(candidateId, candidateMonitor); - - // Assert - expect(result).to.be.null; - sinon.assert.calledWith(monitorFindByIdAndUpdateStub, candidateId, { name: "Updated Monitor", notifications: undefined }, { new: true }); - }); - - it("should remove notifications from update data", async function () { - // Arrange - const candidateId = "monitor123"; - const candidateMonitor = { - name: "Updated Monitor", - notifications: ["notification1", "notification2"], - }; - - const expectedUpdateData = { - name: "Updated Monitor", - notifications: undefined, - }; - - monitorFindByIdAndUpdateStub.resolves({ - _id: candidateId, - ...expectedUpdateData, - }); - - // Act - await editMonitor(candidateId, candidateMonitor); - - // Assert - sinon.assert.calledWith(monitorFindByIdAndUpdateStub, candidateId, expectedUpdateData, { - new: true, - }); - }); - - it("should handle database errors", async function () { - // Arrange - const candidateId = "monitor123"; - const candidateMonitor = { - name: "Updated Monitor", - }; - - const dbError = new Error("Database connection error"); - monitorFindByIdAndUpdateStub.rejects(dbError); - - // Act & Assert - try { - await editMonitor(candidateId, candidateMonitor); - expect.fail("Should have thrown an error"); - } catch (err) { - expect(err.message).to.equal("Database connection error"); - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("editMonitor"); - } - }); - }); - - describe("addDemoMonitors", function () { - it("should add demo monitors successfully", async function () { - // Arrange - const userId = "user123"; - const teamId = "team123"; - monitorInsertManyStub.resolves([{ _id: "123" }]); - const result = await addDemoMonitors(userId, teamId); - expect(result).to.deep.equal([{ _id: "123" }]); - }); - - it("should handle database errors", async function () { - const userId = "user123"; - const teamId = "team123"; - - const dbError = new Error("Database connection error"); - monitorInsertManyStub.rejects(dbError); - - try { - await addDemoMonitors(userId, teamId); - } catch (err) { - expect(err.message).to.equal("Database connection error"); - expect(err.service).to.equal("monitorModule"); - expect(err.method).to.equal("addDemoMonitors"); - } - }); - }); -}); diff --git a/server/tests/db/notificationModule.test.js b/server/tests/db/notificationModule.test.js deleted file mode 100755 index fbb97f33a..000000000 --- a/server/tests/db/notificationModule.test.js +++ /dev/null @@ -1,76 +0,0 @@ -import sinon from "sinon"; -import Notification from "../../db/models/Notification.js"; -import { createNotification, getNotificationsByMonitorId, deleteNotificationsByMonitorId } from "../../db/mongo/modules/notificationModule.js"; - -describe("notificationModule", function () { - const mockNotification = { - monitorId: "123", - }; - const mockNotifications = [mockNotification]; - let notificationSaveStub, notificationFindStub, notificationDeleteManyStub; - - beforeEach(function () { - notificationSaveStub = sinon.stub(Notification.prototype, "save").resolves(); - notificationFindStub = sinon.stub(Notification, "find").resolves(); - notificationDeleteManyStub = sinon.stub(Notification, "deleteMany").resolves(); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("createNotification", function () { - it("should create a new notification", async function () { - const notificationData = { _id: "123", name: "test" }; - notificationSaveStub.resolves(notificationData); - const res = await createNotification(notificationData); - expect(res).to.deep.equal(notificationData); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - notificationSaveStub.rejects(err); - try { - await createNotification(mockNotification); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("getNotificationsByMonitorId", function () { - it("should return notifications by monitor ID", async function () { - notificationFindStub.resolves(mockNotifications); - const res = await getNotificationsByMonitorId(mockNotification.monitorId); - expect(res).to.deep.equal(mockNotifications); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - notificationFindStub.rejects(err); - try { - await getNotificationsByMonitorId(mockNotification.monitorId); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("deleteNotificationsByMonitorId", function () { - it("should delete notifications by monitor ID", async function () { - notificationDeleteManyStub.resolves({ deletedCount: mockNotifications.length }); - const res = await deleteNotificationsByMonitorId(mockNotification.monitorId); - expect(res).to.deep.equal(mockNotifications.length); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - notificationDeleteManyStub.rejects(err); - try { - await deleteNotificationsByMonitorId(mockNotification.monitorId); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); -}); diff --git a/server/tests/db/pageSpeedCheckModule.test.js b/server/tests/db/pageSpeedCheckModule.test.js deleted file mode 100755 index 45135135d..000000000 --- a/server/tests/db/pageSpeedCheckModule.test.js +++ /dev/null @@ -1,63 +0,0 @@ -import sinon from "sinon"; -import PageSpeedCheck from "../../db/models/PageSpeedCheck.js"; -import { createPageSpeedCheck, deletePageSpeedChecksByMonitorId } from "../../db/mongo/modules/pageSpeedCheckModule.js"; - -const mockPageSpeedCheck = { - monitorId: "monitorId", - bestPractices: 1, - seo: 1, - performance: 1, -}; - -const mockDeletedResult = { deletedCount: 1 }; - -describe("pageSpeedCheckModule", function () { - let pageSpeedCheckSaveStub, pageSpeedCheckDeleteManyStub; - - beforeEach(function () { - pageSpeedCheckSaveStub = sinon.stub(PageSpeedCheck.prototype, "save"); - pageSpeedCheckDeleteManyStub = sinon.stub(PageSpeedCheck, "deleteMany"); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("createPageSpeedCheck", function () { - it("should return a page speed check", async function () { - pageSpeedCheckSaveStub.resolves(mockPageSpeedCheck); - const pageSpeedCheck = await createPageSpeedCheck(mockPageSpeedCheck); - expect(pageSpeedCheck).to.deep.equal(mockPageSpeedCheck); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - pageSpeedCheckSaveStub.rejects(err); - try { - await expect(createPageSpeedCheck(mockPageSpeedCheck)); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); - - describe("deletePageSpeedChecksByMonitorId", function () { - it("should return the number of deleted checks", async function () { - pageSpeedCheckDeleteManyStub.resolves(mockDeletedResult); - const result = await deletePageSpeedChecksByMonitorId("monitorId"); - expect(result).to.deep.equal(mockDeletedResult.deletedCount); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - pageSpeedCheckDeleteManyStub.rejects(err); - try { - await expect(deletePageSpeedChecksByMonitorId("monitorId")); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); -}); diff --git a/server/tests/db/recoveryModule.test.js b/server/tests/db/recoveryModule.test.js deleted file mode 100755 index 88afa257a..000000000 --- a/server/tests/db/recoveryModule.test.js +++ /dev/null @@ -1,168 +0,0 @@ -import sinon from "sinon"; -import RecoveryToken from "../../db/models/RecoveryToken.js"; -import User from "../../db/models/User.js"; -import { requestRecoveryToken, validateRecoveryToken, resetPassword } from "../../db/mongo/modules/recoveryModule.js"; -import { errorMessages } from "../../utils/messages.js"; - -const mockRecoveryToken = { - email: "test@test.com", - token: "1234567890", -}; - -const mockUser = { - email: "test@test.com", - password: "oldPassword", -}; - -const mockUserWithoutPassword = { - email: "test@test.com", -}; - -// Create a query builder that logs -const createQueryChain = (finalResult, comparePasswordResult = false) => ({ - select: () => ({ - select: async () => { - if (finalResult === mockUser) { - // Return a new object with all required methods - return { - email: "test@test.com", - password: "oldPassword", - comparePassword: sinon.stub().resolves(comparePasswordResult), - save: sinon.stub().resolves(), - }; - } - return finalResult; - }, - }), - // Add methods to the top level too - comparePassword: sinon.stub().resolves(comparePasswordResult), - save: sinon.stub().resolves(), -}); - -describe("recoveryModule", function () { - let deleteManyStub, saveStub, findOneStub, userCompareStub, userSaveStub, userFindOneStub; - let req, res; - - beforeEach(function () { - req = { - body: { email: "test@test.com" }, - }; - deleteManyStub = sinon.stub(RecoveryToken, "deleteMany"); - saveStub = sinon.stub(RecoveryToken.prototype, "save"); - findOneStub = sinon.stub(RecoveryToken, "findOne"); - userCompareStub = sinon.stub(User.prototype, "comparePassword"); - userSaveStub = sinon.stub(User.prototype, "save"); - userFindOneStub = sinon.stub().resolves(); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("requestRecoveryToken", function () { - it("should return a recovery token", async function () { - deleteManyStub.resolves(); - saveStub.resolves(mockRecoveryToken); - const result = await requestRecoveryToken(req, res); - expect(result.email).to.equal(mockRecoveryToken.email); - }); - - it("should handle an error", async function () { - const err = new Error("Test error"); - deleteManyStub.rejects(err); - try { - await requestRecoveryToken(req, res); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); - - describe("validateRecoveryToken", function () { - it("should return a recovery token if found", async function () { - findOneStub.resolves(mockRecoveryToken); - const result = await validateRecoveryToken(req, res); - expect(result).to.deep.equal(mockRecoveryToken); - }); - - it("should thrown an error if a token is not found", async function () { - findOneStub.resolves(null); - try { - await validateRecoveryToken(req, res); - } catch (error) { - expect(error).to.exist; - expect(error.message).to.equal(errorMessages.DB_TOKEN_NOT_FOUND); - } - }); - - it("should handle DB errors", async function () { - const err = new Error("Test error"); - findOneStub.rejects(err); - try { - await validateRecoveryToken(req, res); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); - - describe("resetPassword", function () { - beforeEach(function () { - req.body = { - password: "test", - newPassword: "test1", - }; - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should thrown an error if a recovery token is not found", async function () { - findOneStub.resolves(null); - try { - await resetPassword(req, res); - } catch (error) { - expect(error).to.exist; - expect(error.message).to.equal(errorMessages.DB_TOKEN_NOT_FOUND); - } - }); - - it("should throw an error if a user is not found", async function () { - findOneStub.resolves(mockRecoveryToken); - userFindOneStub = sinon.stub(User, "findOne").resolves(null); - try { - await resetPassword(req, res); - } catch (error) { - expect(error).to.exist; - expect(error.message).to.equal(errorMessages.DB_USER_NOT_FOUND); - } - }); - - it("should throw an error if the passwords match", async function () { - findOneStub.resolves(mockRecoveryToken); - saveStub.resolves(); - userFindOneStub = sinon.stub(User, "findOne").returns(createQueryChain(mockUser, true)); - try { - await resetPassword(req, res); - } catch (error) { - expect(error).to.exist; - expect(error.message).to.equal(errorMessages.DB_RESET_PASSWORD_BAD_MATCH); - } - }); - - it("should return a user without password if successful", async function () { - findOneStub.resolves(mockRecoveryToken); - saveStub.resolves(); - userFindOneStub = sinon - .stub(User, "findOne") - .returns(createQueryChain(mockUser)) // First call will resolve to mockUser - .onSecondCall() - .returns(createQueryChain(mockUserWithoutPassword)); - const result = await resetPassword(req, res); - expect(result).to.deep.equal(mockUserWithoutPassword); - }); - }); -}); diff --git a/server/tests/db/settingsModule.test.js b/server/tests/db/settingsModule.test.js deleted file mode 100755 index 9d1a80c3e..000000000 --- a/server/tests/db/settingsModule.test.js +++ /dev/null @@ -1,56 +0,0 @@ -import sinon from "sinon"; -import { getAppSettings, updateAppSettings } from "../../db/mongo/modules/settingsModule.js"; -import AppSettings from "../../db/models/AppSettings.js"; - -const mockAppSettings = { - appName: "Test App", -}; - -describe("SettingsModule", function () { - let appSettingsFindOneStub, appSettingsFindOneAndUpdateStub; - - beforeEach(function () { - appSettingsFindOneStub = sinon.stub(AppSettings, "findOne"); - appSettingsFindOneAndUpdateStub = sinon.stub(AppSettings, "findOneAndUpdate"); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("getAppSettings", function () { - it("should return app settings", async function () { - appSettingsFindOneStub.resolves(mockAppSettings); - const result = await getAppSettings(); - expect(result).to.deep.equal(mockAppSettings); - }); - - it("should handle an error", async function () { - const err = new Error("Test error"); - appSettingsFindOneStub.throws(err); - try { - await getAppSettings(); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); - - describe("updateAppSettings", function () { - it("should update app settings", async function () { - appSettingsFindOneAndUpdateStub.resolves(mockAppSettings); - const result = await updateAppSettings(mockAppSettings); - expect(result).to.deep.equal(mockAppSettings); - }); - - it("should handle an error", async function () { - const err = new Error("Test error"); - appSettingsFindOneAndUpdateStub.throws(err); - try { - await updateAppSettings(mockAppSettings); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - }); -}); diff --git a/server/tests/db/statusPageModule.test.js b/server/tests/db/statusPageModule.test.js deleted file mode 100755 index 54e1a287f..000000000 --- a/server/tests/db/statusPageModule.test.js +++ /dev/null @@ -1,70 +0,0 @@ -import sinon from "sinon"; -import { createStatusPage, getStatusPageByUrl } from "../../db/mongo/modules/statusPageModule.js"; -import StatusPage from "../../db/models/StatusPage.js"; -import { errorMessages } from "../../utils/messages.js"; - -describe("statusPageModule", function () { - let statusPageFindOneStub, statusPageSaveStub, statusPageFindStub; - - beforeEach(function () { - statusPageSaveStub = sinon.stub(StatusPage.prototype, "save"); - statusPageFindOneStub = sinon.stub(StatusPage, "findOne"); - statusPageFindStub = sinon.stub(StatusPage, "find"); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("createStatusPage", function () { - it("should throw an error if a non-unique url is provided", async function () { - statusPageFindOneStub.resolves(true); - try { - await createStatusPage({ url: "test" }); - } catch (error) { - expect(error.status).to.equal(400); - expect(error.message).to.equal(errorMessages.STATUS_PAGE_URL_NOT_UNIQUE); - } - }); - - it("should handle duplicate URL errors", async function () { - const err = new Error("test"); - err.code = 11000; - statusPageSaveStub.rejects(err); - try { - await createStatusPage({ url: "test" }); - } catch (error) { - expect(error).to.deep.equal(err); - } - }); - - it("should return a status page if a unique url is provided", async function () { - statusPageFindOneStub.resolves(null); - statusPageFindStub.resolves([]); - const mockStatusPage = { url: "test" }; - const statusPage = await createStatusPage(mockStatusPage); - expect(statusPage).to.exist; - expect(statusPage.url).to.equal(mockStatusPage.url); - }); - }); - - describe("getStatusPageByUrl", function () { - it("should throw an error if a status page is not found", async function () { - statusPageFindOneStub.resolves(null); - try { - await getStatusPageByUrl("test"); - } catch (error) { - expect(error.status).to.equal(404); - expect(error.message).to.equal(errorMessages.STATUS_PAGE_NOT_FOUND); - } - }); - - it("should return a status page if a status page is found", async function () { - const mockStatusPage = { url: "test" }; - statusPageFindOneStub.resolves(mockStatusPage); - const statusPage = await getStatusPageByUrl(mockStatusPage.url); - expect(statusPage).to.exist; - expect(statusPage).to.deep.equal(mockStatusPage); - }); - }); -}); diff --git a/server/tests/db/userModule.test.js b/server/tests/db/userModule.test.js deleted file mode 100755 index d0f5ecb3e..000000000 --- a/server/tests/db/userModule.test.js +++ /dev/null @@ -1,294 +0,0 @@ -import sinon from "sinon"; -import UserModel from "../../db/models/User.js"; -import TeamModel from "../../db/models/Team.js"; -import { - insertUser, - getUserByEmail, - updateUser, - deleteUser, - deleteTeam, - deleteAllOtherUsers, - getAllUsers, - logoutUser, -} from "../../db/mongo/modules/userModule.js"; -import { errorMessages } from "../../utils/messages.js"; - -const mockUser = { - email: "test@test.com", - password: "password", - role: ["user"], -}; -const mockSuperUser = { - email: "test@test.com", - password: "password", - role: ["superadmin"], -}; -const imageFile = { - image: 1, -}; - -describe("userModule", function () { - let teamSaveStub, - teamFindByIdAndDeleteStub, - userSaveStub, - userFindStub, - userFindOneStub, - userFindByIdAndUpdateStub, - userFindByIdAndDeleteStub, - userDeleteManyStub, - userUpdateOneStub, - generateAvatarImageStub, - parseBooleanStub; - - beforeEach(function () { - teamSaveStub = sinon.stub(TeamModel.prototype, "save"); - teamFindByIdAndDeleteStub = sinon.stub(TeamModel, "findByIdAndDelete"); - userSaveStub = sinon.stub(UserModel.prototype, "save"); - userFindStub = sinon.stub(UserModel, "find"); - userFindOneStub = sinon.stub(UserModel, "findOne"); - userFindByIdAndUpdateStub = sinon.stub(UserModel, "findByIdAndUpdate"); - userFindByIdAndDeleteStub = sinon.stub(UserModel, "findByIdAndDelete"); - userDeleteManyStub = sinon.stub(UserModel, "deleteMany"); - userUpdateOneStub = sinon.stub(UserModel, "updateOne"); - generateAvatarImageStub = sinon.stub().resolves({ image: 2 }); - parseBooleanStub = sinon.stub().returns(true); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("insertUser", function () { - it("should insert a regular user", async function () { - userSaveStub.resolves(mockUser); - userFindOneStub.returns({ - select: sinon.stub().returns({ - select: sinon.stub().resolves(mockUser), - }), - }); - const result = await insertUser(mockUser, imageFile, generateAvatarImageStub); - expect(result).to.deep.equal(mockUser); - }); - - it("should insert a superadmin user", async function () { - userSaveStub.resolves(mockSuperUser); - userFindOneStub.returns({ - select: sinon.stub().returns({ - select: sinon.stub().resolves(mockSuperUser), - }), - }); - const result = await insertUser(mockSuperUser, imageFile, generateAvatarImageStub); - expect(result).to.deep.equal(mockSuperUser); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - userSaveStub.rejects(err); - try { - await insertUser(mockUser, imageFile, generateAvatarImageStub); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - - it("should handle a duplicate key error", async function () { - const err = new Error("test error"); - err.code = 11000; - userSaveStub.rejects(err); - try { - await insertUser(mockUser, imageFile, generateAvatarImageStub); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); - - describe("getUserByEmail", function () { - it("should return a user", async function () { - userFindOneStub.returns({ - select: sinon.stub().resolves(mockUser), - }); - const result = await getUserByEmail(mockUser.email); - expect(result).to.deep.equal(mockUser); - }); - }); - - describe("getUserByEmail", function () { - it("throw an error if a user is not found", async function () { - userFindOneStub.returns({ - select: sinon.stub().resolves(null), - }); - try { - await getUserByEmail(mockUser.email); - } catch (error) { - expect(error.message).to.equal(errorMessages.DB_USER_NOT_FOUND); - } - }); - }); - - describe("updateUser", function () { - let req, res; - - beforeEach(function () { - req = { - params: { - userId: "testId", - }, - body: { - deleteProfileImage: "false", - email: "test@test.com", - }, - file: { - buffer: "test", - mimetype: "test", - }, - }; - res = {}; - }); - - afterEach(function () {}); - - it("should update a user", async function () { - parseBooleanStub.returns(false); - userFindByIdAndUpdateStub.returns({ - select: sinon.stub().returns({ - select: sinon.stub().resolves(mockUser), - }), - }); - const result = await updateUser(req, res, parseBooleanStub, generateAvatarImageStub); - expect(result).to.deep.equal(mockUser); - }); - - it("should delete a user profile image", async function () { - req.body.deleteProfileImage = "true"; - userFindByIdAndUpdateStub.returns({ - select: sinon.stub().returns({ - select: sinon.stub().resolves(mockUser), - }), - }); - const result = await updateUser(req, res, parseBooleanStub, generateAvatarImageStub); - expect(result).to.deep.equal(mockUser); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - userFindByIdAndUpdateStub.throws(err); - try { - await updateUser(req, res, parseBooleanStub, generateAvatarImageStub); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); - - describe("deleteUser", function () { - it("should return a deleted user", async function () { - userFindByIdAndDeleteStub.resolves(mockUser); - const result = await deleteUser("testId"); - expect(result).to.deep.equal(mockUser); - }); - - it("should throw an error if a user is not found", async function () { - try { - await deleteUser("testId"); - } catch (error) { - expect(error).to.exist; - expect(error.message).to.equal(errorMessages.DB_USER_NOT_FOUND); - } - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - userFindByIdAndDeleteStub.throws(err); - try { - await deleteUser("testId"); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); - - describe("deleteTeam", function () { - it("should return true if team deleted", async function () { - teamFindByIdAndDeleteStub.resolves(); - const result = await deleteTeam("testId"); - expect(result).to.equal(true); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - teamFindByIdAndDeleteStub.throws(err); - try { - await deleteTeam("testId"); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); - - describe("deleteAllOtherUsers", function () { - it("should return true if all other users deleted", async function () { - userDeleteManyStub.resolves(true); - const result = await deleteAllOtherUsers(); - expect(result).to.equal(true); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - userDeleteManyStub.throws(err); - try { - await deleteAllOtherUsers(); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); - - describe("getAllUsers", function () { - it("should return all users", async function () { - userFindStub.returns({ - select: sinon.stub().returns({ - select: sinon.stub().resolves([mockUser]), - }), - }); - const result = await getAllUsers(); - expect(result).to.deep.equal([mockUser]); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - userFindStub.throws(err); - try { - await getAllUsers(); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); - - describe("logoutUser", function () { - it("should return true if user logged out", async function () { - userUpdateOneStub.resolves(true); - const result = await logoutUser("testId"); - expect(result).to.equal(true); - }); - - it("should handle an error", async function () { - const err = new Error("test error"); - userUpdateOneStub.throws(err); - try { - await logoutUser("testId"); - } catch (error) { - expect(error).to.exist; - expect(error).to.deep.equal(err); - } - }); - }); -}); diff --git a/server/tests/services/emailService.test.js b/server/tests/services/emailService.test.js deleted file mode 100755 index cca86896a..000000000 --- a/server/tests/services/emailService.test.js +++ /dev/null @@ -1,168 +0,0 @@ -import sinon from "sinon"; -import EmailService from "../../service/emailService.js"; - -describe("EmailService - Constructor", function () { - let settingsServiceMock; - let fsMock; - let pathMock; - let compileMock; - let mjml2htmlMock; - let nodemailerMock; - let loggerMock; - - beforeEach(function () { - settingsServiceMock = { - getSettings: sinon.stub().returns({ - systemEmailHost: "smtp.example.com", - systemEmailPort: 465, - systemEmailAddress: "test@example.com", - systemEmailPassword: "password", - }), - }; - - fsMock = { - readFileSync: sinon.stub().returns(""), - }; - - pathMock = { - join: sinon.stub().callsFake((...args) => args.join("/")), - }; - - compileMock = sinon.stub().returns(() => ""); - - mjml2htmlMock = sinon.stub().returns({ html: "" }); - - nodemailerMock = { - createTransport: sinon.stub().returns({ - sendMail: sinon.stub().resolves({ messageId: "12345" }), - }), - }; - - loggerMock = { - error: sinon.stub(), - }; - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should initialize template loaders and email transporter", function () { - const emailService = new EmailService(settingsServiceMock, fsMock, pathMock, compileMock, mjml2htmlMock, nodemailerMock, loggerMock); - - // Verify that the settingsService is assigned correctly - expect(emailService.settingsService).to.equal(settingsServiceMock); - - // Verify that the template loaders are initialized - expect(emailService.templateLookup.welcomeEmailTemplate).to.be.a("function"); - expect(emailService.templateLookup.employeeActivationTemplate).to.be.a("function"); - - // Verify that the email transporter is initialized - expect(nodemailerMock.createTransport.calledOnce).to.be.true; - const emailConfig = nodemailerMock.createTransport.getCall(0).args[0]; - expect(emailConfig).to.deep.equal({ - host: "smtp.example.com", - port: 465, - secure: true, - auth: { - user: "test@example.com", - pass: "password", - }, - }); - }); - - it("should have undefined templates if FS fails", function () { - fsMock = { - readFileSync: sinon.stub().throws(new Error("File read error")), - }; - const emailService = new EmailService(settingsServiceMock, fsMock, pathMock, compileMock, mjml2htmlMock, nodemailerMock, loggerMock); - expect(loggerMock.error.called).to.be.true; - expect(loggerMock.error.firstCall.args[0].message).to.equal("File read error"); - }); -}); - -describe("EmailService - buildAndSendEmail", function () { - let settingsServiceMock; - let fsMock; - let pathMock; - let compileMock; - let mjml2htmlMock; - let nodemailerMock; - let loggerMock; - let emailService; - - beforeEach(function () { - settingsServiceMock = { - getSettings: sinon.stub().returns({ - systemEmailHost: "smtp.example.com", - systemEmailPort: 465, - systemEmailAddress: "test@example.com", - systemEmailPassword: "password", - }), - }; - - fsMock = { - readFileSync: sinon.stub().returns(""), - }; - - pathMock = { - join: sinon.stub().callsFake((...args) => args.join("/")), - }; - - compileMock = sinon.stub().returns(() => ""); - - mjml2htmlMock = sinon.stub().returns({ html: "" }); - - nodemailerMock = { - createTransport: sinon.stub().returns({ - sendMail: sinon.stub().resolves({ messageId: "12345" }), - }), - }; - - loggerMock = { - error: sinon.stub(), - }; - - emailService = new EmailService(settingsServiceMock, fsMock, pathMock, compileMock, mjml2htmlMock, nodemailerMock, loggerMock); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should build and send email successfully", async function () { - const messageId = await emailService.buildAndSendEmail("welcomeEmailTemplate", {}, "recipient@example.com", "Welcome"); - - expect(messageId).to.equal("12345"); - expect(nodemailerMock.createTransport().sendMail.calledOnce).to.be.true; - }); - - it("should log error if building HTML fails", async function () { - mjml2htmlMock.throws(new Error("MJML error")); - - const messageId = await emailService.buildAndSendEmail("welcomeEmailTemplate", {}, "recipient@example.com", "Welcome"); - expect(loggerMock.error.calledOnce).to.be.true; - expect(loggerMock.error.getCall(0).args[0].message).to.equal("MJML error"); - }); - - it("should log error if sending email fails", async function () { - nodemailerMock.createTransport().sendMail.rejects(new Error("SMTP error")); - await emailService.buildAndSendEmail("welcomeEmailTemplate", {}, "recipient@example.com", "Welcome"); - expect(loggerMock.error.calledOnce).to.be.true; - expect(loggerMock.error.getCall(0).args[0].message).to.equal("SMTP error"); - }); - - it("should log error if both building HTML and sending email fail", async function () { - mjml2htmlMock.throws(new Error("MJML error")); - nodemailerMock.createTransport().sendMail.rejects(new Error("SMTP error")); - - const messageId = await emailService.buildAndSendEmail("welcomeEmailTemplate", {}, "recipient@example.com", "Welcome"); - - expect(messageId).to.be.undefined; - expect(loggerMock.error.calledTwice).to.be.true; - expect(loggerMock.error.getCall(0).args[0].message).to.equal("MJML error"); - expect(loggerMock.error.getCall(1).args[0].message).to.equal("SMTP error"); - }); - - it("should log an error if buildHtml fails", async function () {}); -}); diff --git a/server/tests/services/jobQueue.test.js b/server/tests/services/jobQueue.test.js deleted file mode 100755 index 155f9119e..000000000 --- a/server/tests/services/jobQueue.test.js +++ /dev/null @@ -1,813 +0,0 @@ -import sinon from "sinon"; -import JobQueue from "../../service/jobQueue.js"; -import { log } from "console"; - -class QueueStub { - constructor(queueName, options) { - this.queueName = queueName; - this.options = options; - this.workers = []; - this.jobs = []; - } - - // Add any methods that are expected to be called on the Queue instance - add(job) { - this.jobs.push(job); - } - - removeRepeatable(id) { - const removedJob = this.jobs.find((job) => job.data._id === id); - this.jobs = this.jobs.filter((job) => job.data._id !== id); - if (removedJob) { - return true; - } - return false; - } - - getRepeatableJobs() { - return this.jobs; - } - async getJobs() { - return this.jobs; - } - - async pause() { - return true; - } - - async obliterate() { - return true; - } -} - -class WorkerStub { - constructor(QUEUE_NAME, workerTask) { - this.queueName = QUEUE_NAME; - this.workerTask = async () => workerTask({ data: { _id: 1 } }); - } - - async close() { - return true; - } -} - -describe("JobQueue", function () { - let settingsService, logger, db, networkService, statusService, notificationService, jobQueue; - - beforeEach(async function () { - settingsService = { getSettings: sinon.stub() }; - statusService = { updateStatus: sinon.stub() }; - notificationService = { handleNotifications: sinon.stub() }; - - logger = { error: sinon.stub(), info: sinon.stub() }; - db = { - getAllMonitors: sinon.stub().returns([]), - getMaintenanceWindowsByMonitorId: sinon.stub().returns([]), - }; - networkService = { getStatus: sinon.stub() }; - jobQueue = await JobQueue.createJobQueue(db, networkService, statusService, notificationService, settingsService, logger, QueueStub, WorkerStub); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("createJobQueue", function () { - it("should create a new JobQueue and add jobs for active monitors", async function () { - db.getAllMonitors.returns([ - { id: 1, isActive: true }, - { id: 2, isActive: true }, - ]); - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - // There should be double the jobs, as one is meant to be instantly executed - // And one is meant to be enqueued - expect(jobQueue.queue.jobs.length).to.equal(4); - }); - - it("should reject with an error if an error occurs", async function () { - db.getAllMonitors.throws("Error"); - try { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - } catch (error) { - expect(error.service).to.equal("JobQueue"); - expect(error.method).to.equal("createJobQueue"); - } - }); - - it("should reject with an error if an error occurs, should not overwrite error data", async function () { - const error = new Error("Error"); - error.service = "otherService"; - error.method = "otherMethod"; - db.getAllMonitors.throws(error); - - try { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - } catch (error) { - expect(error.service).to.equal("otherService"); - expect(error.method).to.equal("otherMethod"); - } - }); - }); - - describe("Constructor", function () { - it("should construct a new JobQueue with default port and host if not provided", async function () { - settingsService.getSettings.returns({}); - - expect(jobQueue.connection.host).to.equal("127.0.0.1"); - expect(jobQueue.connection.port).to.equal(6379); - }); - - it("should construct a new JobQueue with provided port and host", async function () { - settingsService.getSettings.returns({ redisHost: "localhost", redisPort: 1234 }); - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - expect(jobQueue.connection.host).to.equal("localhost"); - expect(jobQueue.connection.port).to.equal(1234); - }); - }); - - describe("isMaintenanceWindow", function () { - it("should throw an error if error occurs", async function () { - db.getMaintenanceWindowsByMonitorId.throws("Error"); - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - try { - jobQueue.isInMaintenanceWindow(1); - } catch (error) { - expect(error.service).to.equal("JobQueue"); - expect(error.method).to.equal("createWorker"); - } - }); - - it("should return true if in maintenance window with no repeat", async function () { - db.getMaintenanceWindowsByMonitorId.returns([ - { - active: true, - start: new Date(Date.now() - 1000).toISOString(), - end: new Date(Date.now() + 1000).toISOString(), - repeat: 0, - }, - ]); - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - const inWindow = await jobQueue.isInMaintenanceWindow(1); - expect(inWindow).to.be.true; - }); - - it("should return true if in maintenance window with repeat", async function () { - db.getMaintenanceWindowsByMonitorId.returns([ - { - active: true, - start: new Date(Date.now() - 10000).toISOString(), - end: new Date(Date.now() - 5000).toISOString(), - repeat: 1000, - }, - ]); - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - const inWindow = await jobQueue.isInMaintenanceWindow(1); - expect(inWindow).to.be.true; - }); - - it("should return false if in end < start", async function () { - db.getMaintenanceWindowsByMonitorId.returns([ - { - active: true, - start: new Date(Date.now() - 5000).toISOString(), - end: new Date(Date.now() - 10000).toISOString(), - repeat: 1000, - }, - ]); - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - const inWindow = await jobQueue.isInMaintenanceWindow(1); - expect(inWindow).to.be.false; - }); - - it("should return false if not in maintenance window", async function () { - db.getMaintenanceWindowsByMonitorId.returns([ - { - active: false, - start: new Date(Date.now() - 5000).toISOString(), - end: new Date(Date.now() - 10000).toISOString(), - repeat: 1000, - }, - ]); - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - const inWindow = await jobQueue.isInMaintenanceWindow(1); - expect(inWindow).to.be.false; - }); - }); - - describe("createJobHandler", function () { - it("resolve to an error if an error is thrown within", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - jobQueue.isInMaintenanceWindow = sinon.stub().throws("Error"); - try { - const handler = jobQueue.createJobHandler(); - await handler({ data: { _id: 1 } }); - } catch (error) { - expect(error.service).to.equal("JobQueue"); - expect(error.details).to.equal(`Error processing job 1: Error`); - } - }); - - it("should log info if job is in maintenance window", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - jobQueue.isInMaintenanceWindow = sinon.stub().returns(true); - const handler = jobQueue.createJobHandler(); - await handler({ data: { _id: 1 } }); - expect(logger.info.calledOnce).to.be.true; - expect(logger.info.firstCall.args[0].message).to.equal("Monitor 1 is in maintenance window"); - }); - - it("should return if status has not changed", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - jobQueue.isInMaintenanceWindow = sinon.stub().returns(false); - statusService.updateStatus = sinon.stub().returns({ statusChanged: false }); - const handler = jobQueue.createJobHandler(); - await handler({ data: { _id: 1 } }); - expect(jobQueue.notificationService.handleNotifications.notCalled).to.be.true; - }); - - it("should return if status has changed, but prevStatus was undefined (monitor paused)", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - jobQueue.isInMaintenanceWindow = sinon.stub().returns(false); - statusService.updateStatus = sinon.stub().returns({ statusChanged: true, prevStatus: undefined }); - const handler = jobQueue.createJobHandler(); - await handler({ data: { _id: 1 } }); - expect(jobQueue.notificationService.handleNotifications.notCalled).to.be.true; - }); - - it("should call notification service if status changed and monitor was not paused", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - jobQueue.isInMaintenanceWindow = sinon.stub().returns(false); - statusService.updateStatus = sinon.stub().returns({ statusChanged: true, prevStatus: false }); - const handler = jobQueue.createJobHandler(); - await handler({ data: { _id: 1 } }); - expect(jobQueue.notificationService.handleNotifications.calledOnce).to.be.true; - }); - }); - - describe("getWorkerStats", function () { - it("should throw an error if getRepeatable Jobs fails", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - jobQueue.queue.getRepeatableJobs = async () => { - throw new Error("Error"); - }; - try { - const stats = await jobQueue.getWorkerStats(); - } catch (error) { - expect(error.service).to.equal("JobQueue"); - expect(error.method).to.equal("getWorkerStats"); - } - }); - - it("should throw an error if getRepeatable Jobs fails but respect existing error data", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - jobQueue.queue.getRepeatableJobs = async () => { - const error = new Error("Existing Error"); - error.service = "otherService"; - error.method = "otherMethod"; - throw error; - }; - try { - await jobQueue.getWorkerStats(); - } catch (error) { - expect(error.service).to.equal("otherService"); - expect(error.method).to.equal("otherMethod"); - } - }); - }); - - describe("scaleWorkers", function () { - it("should scale workers to 5 if no workers", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - expect(jobQueue.workers.length).to.equal(5); - }); - - it("should scale workers up", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - jobQueue.scaleWorkers({ - load: 100, - jobs: Array.from({ length: 100 }, (_, i) => i + 1), - }); - expect(jobQueue.workers.length).to.equal(20); - }); - - it("should scale workers down, even with error of worker.close fails", async function () { - WorkerStub.prototype.close = async () => { - throw new Error("Error"); - }; - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - await jobQueue.scaleWorkers({ - load: 100, - jobs: Array.from({ length: 100 }, (_, i) => i + 1), - }); - - const res = await jobQueue.scaleWorkers({ - load: 0, - jobs: [], - }); - expect(jobQueue.workers.length).to.equal(5); - }); - - it("should scale workers down", async function () { - WorkerStub.prototype.close = async () => { - return true; - }; - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - await jobQueue.scaleWorkers({ - load: 40, - jobs: Array.from({ length: 40 }, (_, i) => i + 1), - }); - - const res = await jobQueue.scaleWorkers({ - load: 0, - jobs: [], - }); - expect(jobQueue.workers.length).to.equal(5); - }); - - it("should return false if scaling doesn't happen", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - const res = await jobQueue.scaleWorkers({ load: 5 }); - expect(jobQueue.workers.length).to.equal(5); - expect(res).to.be.false; - }); - }); - - describe("getJobs", function () { - it("should return jobs", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - const jobs = await jobQueue.getJobs(); - expect(jobs.length).to.equal(0); - }); - - it("should throw an error if getRepeatableJobs fails", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - try { - jobQueue.queue.getRepeatableJobs = async () => { - throw new Error("error"); - }; - - await jobQueue.getJobs(true); - } catch (error) { - expect(error.service).to.equal("JobQueue"); - expect(error.method).to.equal("getJobs"); - } - }); - - it("should throw an error if getRepeatableJobs fails but respect existing error data", async function () { - const jobQueue = await JobQueue.createJobQueue( - db, - networkService, - statusService, - notificationService, - settingsService, - logger, - QueueStub, - WorkerStub - ); - try { - jobQueue.queue.getRepeatableJobs = async () => { - const error = new Error("Existing error"); - error.service = "otherService"; - error.method = "otherMethod"; - throw error; - }; - - await jobQueue.getJobs(true); - } catch (error) { - expect(error.service).to.equal("otherService"); - expect(error.method).to.equal("otherMethod"); - } - }); - }); - - describe("getJobStats", function () { - it("should return job stats for no jobs", async function () { - const jobStats = await jobQueue.getJobStats(); - expect(jobStats).to.deep.equal({ jobs: [], workers: 5 }); - }); - - it("should return job stats for jobs", async function () { - jobQueue.queue.getJobs = async () => { - return [{ data: { url: "test" }, getState: async () => "completed" }]; - }; - const jobStats = await jobQueue.getJobStats(); - expect(jobStats).to.deep.equal({ - jobs: [{ url: "test", state: "completed" }], - workers: 5, - }); - }); - - it("should reject with an error if mapping jobs fails", async function () { - jobQueue.queue.getJobs = async () => { - return [ - { - data: { url: "test" }, - getState: async () => { - throw new Error("Mapping Error"); - }, - }, - ]; - }; - try { - await jobQueue.getJobStats(); - } catch (error) { - expect(error.message).to.equal("Mapping Error"); - expect(error.service).to.equal("JobQueue"); - expect(error.method).to.equal("getJobStats"); - } - }); - - it("should reject with an error if mapping jobs fails but respect existing error data", async function () { - jobQueue.queue.getJobs = async () => { - return [ - { - data: { url: "test" }, - getState: async () => { - const error = new Error("Mapping Error"); - error.service = "otherService"; - error.method = "otherMethod"; - throw error; - }, - }, - ]; - }; - try { - await jobQueue.getJobStats(); - } catch (error) { - expect(error.message).to.equal("Mapping Error"); - expect(error.service).to.equal("otherService"); - expect(error.method).to.equal("otherMethod"); - } - }); - }); - - describe("addJob", function () { - it("should add a job to the queue", async function () { - jobQueue.addJob("test", { url: "test" }); - expect(jobQueue.queue.jobs.length).to.equal(1); - }); - - it("should reject with an error if adding fails", async function () { - jobQueue.queue.add = async () => { - throw new Error("Error adding job"); - }; - try { - await jobQueue.addJob("test", { url: "test" }); - } catch (error) { - expect(error.message).to.equal("Error adding job"); - expect(error.service).to.equal("JobQueue"); - expect(error.method).to.equal("addJob"); - } - }); - - it("should reject with an error if adding fails but respect existing error data", async function () { - jobQueue.queue.add = async () => { - const error = new Error("Error adding job"); - error.service = "otherService"; - error.method = "otherMethod"; - throw error; - }; - try { - await jobQueue.addJob("test", { url: "test" }); - } catch (error) { - expect(error.message).to.equal("Error adding job"); - expect(error.service).to.equal("otherService"); - expect(error.method).to.equal("otherMethod"); - } - }); - }); - - describe("deleteJob", function () { - it("should delete a job from the queue", async function () { - jobQueue.getWorkerStats = sinon.stub().returns({ load: 1, jobs: [{}] }); - jobQueue.scaleWorkers = sinon.stub(); - const monitor = { _id: 1 }; - const job = { data: monitor }; - jobQueue.queue.jobs = [job]; - await jobQueue.deleteJob(monitor); - // expect(jobQueue.queue.jobs.length).to.equal(0); - // expect(logger.info.calledOnce).to.be.true; - // expect(jobQueue.getWorkerStats.calledOnce).to.be.true; - // expect(jobQueue.scaleWorkers.calledOnce).to.be.true; - }); - - it("should log an error if job is not found", async function () { - jobQueue.getWorkerStats = sinon.stub().returns({ load: 1, jobs: [{}] }); - jobQueue.scaleWorkers = sinon.stub(); - const monitor = { _id: 1 }; - const job = { data: monitor }; - jobQueue.queue.jobs = [job]; - await jobQueue.deleteJob({ id_: 2 }); - expect(logger.error.calledOnce).to.be.true; - }); - - it("should reject with an error if removeRepeatable fails", async function () { - jobQueue.queue.removeRepeatable = async () => { - const error = new Error("removeRepeatable error"); - throw error; - }; - - try { - await jobQueue.deleteJob({ _id: 1 }); - } catch (error) { - expect(error.message).to.equal("removeRepeatable error"); - expect(error.service).to.equal("JobQueue"); - expect(error.method).to.equal("deleteJob"); - } - }); - - it("should reject with an error if removeRepeatable fails but respect existing error data", async function () { - jobQueue.queue.removeRepeatable = async () => { - const error = new Error("removeRepeatable error"); - error.service = "otherService"; - error.method = "otherMethod"; - throw error; - }; - - try { - await jobQueue.deleteJob({ _id: 1 }); - } catch (error) { - expect(error.message).to.equal("removeRepeatable error"); - expect(error.service).to.equal("otherService"); - expect(error.method).to.equal("otherMethod"); - } - }); - }); - - describe("getMetrics", function () { - it("should return metrics for the job queue", async function () { - jobQueue.queue.getWaitingCount = async () => 1; - jobQueue.queue.getActiveCount = async () => 2; - jobQueue.queue.getCompletedCount = async () => 3; - jobQueue.queue.getFailedCount = async () => 4; - jobQueue.queue.getDelayedCount = async () => 5; - jobQueue.queue.getRepeatableJobs = async () => [1, 2, 3]; - const metrics = await jobQueue.getMetrics(); - expect(metrics).to.deep.equal({ - waiting: 1, - active: 2, - completed: 3, - failed: 4, - delayed: 5, - repeatableJobs: 3, - }); - }); - - it("should log an error if metrics operations fail", async function () { - jobQueue.queue.getWaitingCount = async () => { - throw new Error("Error"); - }; - await jobQueue.getMetrics(); - expect(logger.error.calledOnce).to.be.true; - expect(logger.error.firstCall.args[0].message).to.equal("Error"); - }); - }); - - describe("obliterate", function () { - it("should return true if obliteration is successful", async function () { - jobQueue.queue.pause = async () => true; - jobQueue.getJobs = async () => [{ key: 1, id: 1 }]; - jobQueue.queue.removeRepeatableByKey = async () => true; - jobQueue.queue.remove = async () => true; - jobQueue.queue.obliterate = async () => true; - const obliteration = await jobQueue.obliterate(); - expect(obliteration).to.be.true; - }); - - it("should throw an error if obliteration fails", async function () { - jobQueue.getMetrics = async () => { - throw new Error("Error"); - }; - - try { - await jobQueue.obliterate(); - } catch (error) { - expect(error.service).to.equal("JobQueue"); - expect(error.method).to.equal("obliterate"); - } - }); - - it("should throw an error if obliteration fails but respect existing error data", async function () { - jobQueue.getMetrics = async () => { - const error = new Error("Error"); - error.service = "otherService"; - error.method = "otherMethod"; - throw error; - }; - - try { - await jobQueue.obliterate(); - } catch (error) { - expect(error.service).to.equal("otherService"); - expect(error.method).to.equal("otherMethod"); - } - }); - }); -}); diff --git a/server/tests/services/networkService.test.js b/server/tests/services/networkService.test.js deleted file mode 100755 index cb48ed9b2..000000000 --- a/server/tests/services/networkService.test.js +++ /dev/null @@ -1,439 +0,0 @@ -import sinon from "sinon"; -import NetworkService from "../../service/networkService.js"; -import { expect } from "chai"; -import http from "http"; -import { errorMessages } from "../../utils/messages.js"; -describe("Network Service", function () { - let axios, ping, Docker, logger, networkService; - - beforeEach(function () { - axios = { - get: sinon.stub().resolves({ - data: { foo: "bar" }, - status: 200, - }), - }; - Docker = class { - listContainers = sinon.stub().resolves([ - { - Names: ["http://test.com"], - Id: "http://test.com", - }, - ]); - getContainer = sinon.stub().returns({ - inspect: sinon.stub().resolves({ State: { Status: "running" } }), - }); - }; - ping = { - promise: { - probe: sinon.stub().resolves({ response: { alive: true }, responseTime: 100, alive: true }), - }, - }; - logger = { error: sinon.stub() }; - networkService = new NetworkService(axios, ping, logger, http, Docker); - }); - - describe("constructor", function () { - it("should create a new NetworkService instance", function () { - const networkService = new NetworkService(); - expect(networkService).to.be.an.instanceOf(NetworkService); - }); - }); - - describe("timeRequest", function () { - it("should time an asynchronous operation", async function () { - const operation = sinon.stub().resolves("success"); - const { response, responseTime } = await networkService.timeRequest(operation); - expect(response).to.equal("success"); - expect(responseTime).to.be.a("number"); - }); - - it("should handle errors if operation throws error", async function () { - const error = new Error("Test error"); - const operation = sinon.stub().throws(error); - const { response, responseTime } = await networkService.timeRequest(operation); - expect(response).to.be.null; - expect(responseTime).to.be.a("number"); - expect(error.message).to.equal("Test error"); - }); - }); - - describe("requestPing", function () { - it("should return a response object if ping successful", async function () { - const pingResult = await networkService.requestPing({ - data: { url: "http://test.com", _id: "123" }, - }); - expect(pingResult.monitorId).to.equal("123"); - expect(pingResult.type).to.equal("ping"); - expect(pingResult.responseTime).to.be.a("number"); - expect(pingResult.status).to.be.true; - }); - - it("should return a response object if ping unsuccessful", async function () { - const error = new Error("Test error"); - networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); - const pingResult = await networkService.requestPing({ - data: { url: "http://test.com", _id: "123" }, - }); - expect(pingResult.monitorId).to.equal("123"); - expect(pingResult.type).to.equal("ping"); - expect(pingResult.responseTime).to.be.a("number"); - expect(pingResult.status).to.be.false; - expect(pingResult.code).to.equal(networkService.PING_ERROR); - }); - - it("should throw an error if ping cannot resolve", async function () { - const error = new Error("test error"); - networkService.timeRequest = sinon.stub().throws(error); - try { - await networkService.requestPing({ - data: { url: "http://test.com", _id: "123" }, - }); - } catch (error) { - expect(error).to.exist; - expect(error.method).to.equal("requestPing"); - } - }); - }); - - describe("requestHttp", function () { - it("should return a response object if http successful", async function () { - const job = { data: { url: "http://test.com", _id: "123", type: "http" } }; - const httpResult = await networkService.requestHttp(job); - expect(httpResult.monitorId).to.equal("123"); - expect(httpResult.type).to.equal("http"); - expect(httpResult.responseTime).to.be.a("number"); - expect(httpResult.status).to.be.true; - }); - - it("should return a response object if http unsuccessful", async function () { - const error = new Error("Test error"); - error.response = { status: 404 }; - networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); - const job = { data: { url: "http://test.com", _id: "123", type: "http" } }; - const httpResult = await networkService.requestHttp(job); - expect(httpResult.monitorId).to.equal("123"); - expect(httpResult.type).to.equal("http"); - expect(httpResult.responseTime).to.be.a("number"); - expect(httpResult.status).to.be.false; - expect(httpResult.code).to.equal(404); - }); - - it("should return a response object if http unsuccessful with unknown code", async function () { - const error = new Error("Test error"); - error.response = {}; - networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); - const job = { data: { url: "http://test.com", _id: "123", type: "http" } }; - const httpResult = await networkService.requestHttp(job); - expect(httpResult.monitorId).to.equal("123"); - expect(httpResult.type).to.equal("http"); - expect(httpResult.responseTime).to.be.a("number"); - expect(httpResult.status).to.be.false; - expect(httpResult.code).to.equal(networkService.NETWORK_ERROR); - }); - - it("should throw an error if an error occurs", async function () { - const error = new Error("test error"); - networkService.timeRequest = sinon.stub().throws(error); - try { - await networkService.requestHttp({ - data: { url: "http://test.com", _id: "123" }, - }); - } catch (error) { - expect(error).to.exist; - expect(error.method).to.equal("requestHttp"); - } - }); - }); - - describe("requestPagespeed", function () { - it("should return a response object if pagespeed successful", async function () { - const job = { data: { url: "http://test.com", _id: "123", type: "pagespeed" } }; - const pagespeedResult = await networkService.requestPagespeed(job); - expect(pagespeedResult.monitorId).to.equal("123"); - expect(pagespeedResult.type).to.equal("pagespeed"); - expect(pagespeedResult.responseTime).to.be.a("number"); - expect(pagespeedResult.status).to.be.true; - }); - - it("should return a response object if pagespeed unsuccessful", async function () { - const error = new Error("Test error"); - error.response = { status: 404 }; - networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); - const job = { data: { url: "http://test.com", _id: "123", type: "pagespeed" } }; - const pagespeedResult = await networkService.requestPagespeed(job); - expect(pagespeedResult.monitorId).to.equal("123"); - expect(pagespeedResult.type).to.equal("pagespeed"); - expect(pagespeedResult.responseTime).to.be.a("number"); - expect(pagespeedResult.status).to.be.false; - expect(pagespeedResult.code).to.equal(404); - }); - - it("should return a response object if pagespeed unsuccessful with an unknown code", async function () { - const error = new Error("Test error"); - error.response = {}; - networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); - const job = { data: { url: "http://test.com", _id: "123", type: "pagespeed" } }; - const pagespeedResult = await networkService.requestPagespeed(job); - expect(pagespeedResult.monitorId).to.equal("123"); - expect(pagespeedResult.type).to.equal("pagespeed"); - expect(pagespeedResult.responseTime).to.be.a("number"); - expect(pagespeedResult.status).to.be.false; - expect(pagespeedResult.code).to.equal(networkService.NETWORK_ERROR); - }); - - it("should throw an error if pagespeed cannot resolve", async function () { - const error = new Error("test error"); - networkService.timeRequest = sinon.stub().throws(error); - try { - await networkService.requestPagespeed({ - data: { url: "http://test.com", _id: "123" }, - }); - } catch (error) { - expect(error).to.exist; - expect(error.method).to.equal("requestPagespeed"); - } - }); - }); - - describe("requestHardware", function () { - it("should return a response object if hardware successful", async function () { - const job = { data: { url: "http://test.com", _id: "123", type: "hardware" } }; - const httpResult = await networkService.requestHardware(job); - expect(httpResult.monitorId).to.equal("123"); - expect(httpResult.type).to.equal("hardware"); - expect(httpResult.responseTime).to.be.a("number"); - expect(httpResult.status).to.be.true; - }); - - it("should return a response object if hardware successful and job has a secret", async function () { - const job = { - data: { - url: "http://test.com", - _id: "123", - type: "hardware", - secret: "my_secret", - }, - }; - const httpResult = await networkService.requestHardware(job); - expect(httpResult.monitorId).to.equal("123"); - expect(httpResult.type).to.equal("hardware"); - expect(httpResult.responseTime).to.be.a("number"); - expect(httpResult.status).to.be.true; - }); - - it("should return a response object if hardware unsuccessful", async function () { - const error = new Error("Test error"); - error.response = { status: 404 }; - networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); - const job = { data: { url: "http://test.com", _id: "123", type: "hardware" } }; - const httpResult = await networkService.requestHardware(job); - expect(httpResult.monitorId).to.equal("123"); - expect(httpResult.type).to.equal("hardware"); - expect(httpResult.responseTime).to.be.a("number"); - expect(httpResult.status).to.be.false; - expect(httpResult.code).to.equal(404); - }); - - it("should return a response object if hardware unsuccessful with unknown code", async function () { - const error = new Error("Test error"); - error.response = {}; - networkService.timeRequest = sinon.stub().resolves({ response: null, responseTime: 1, error }); - const job = { data: { url: "http://test.com", _id: "123", type: "hardware" } }; - const httpResult = await networkService.requestHardware(job); - expect(httpResult.monitorId).to.equal("123"); - expect(httpResult.type).to.equal("hardware"); - expect(httpResult.responseTime).to.be.a("number"); - expect(httpResult.status).to.be.false; - expect(httpResult.code).to.equal(networkService.NETWORK_ERROR); - }); - - it("should throw an error if hardware cannot resolve", async function () { - const error = new Error("test error"); - networkService.timeRequest = sinon.stub().throws(error); - try { - await networkService.requestHardware({ - data: { url: "http://test.com", _id: "123" }, - }); - } catch (error) { - expect(error).to.exist; - expect(error.method).to.equal("requestHardware"); - } - }); - }); - - describe("requestDocker", function () { - it("should return a response object if docker successful", async function () { - const job = { data: { url: "http://test.com", _id: "123", type: "docker" } }; - const dockerResult = await networkService.requestDocker(job); - expect(dockerResult.monitorId).to.equal("123"); - expect(dockerResult.type).to.equal("docker"); - expect(dockerResult.responseTime).to.be.a("number"); - expect(dockerResult.status).to.be.true; - }); - - it("should return a response object with status false if container not running", async function () { - Docker = class { - listContainers = sinon.stub().resolves([ - { - Names: ["/my_container"], - Id: "abc123", - }, - ]); - getContainer = sinon.stub().returns({ - inspect: sinon.stub().resolves({ State: { Status: "stopped" } }), - }); - }; - networkService = new NetworkService(axios, ping, logger, http, Docker); - const job = { data: { url: "abc123", _id: "123", type: "docker" } }; - const dockerResult = await networkService.requestDocker(job); - expect(dockerResult.status).to.be.false; - expect(dockerResult.code).to.equal(200); - }); - - it("should handle an error when fetching the container", async function () { - Docker = class { - listContainers = sinon.stub().resolves([ - { - Names: ["/my_container"], - Id: "abc123", - }, - ]); - getContainer = sinon.stub().returns({ - inspect: sinon.stub().throws(new Error("test error")), - }); - }; - networkService = new NetworkService(axios, ping, logger, http, Docker); - const job = { data: { url: "abc123", _id: "123", type: "docker" } }; - const dockerResult = await networkService.requestDocker(job); - expect(dockerResult.status).to.be.false; - expect(dockerResult.code).to.equal(networkService.NETWORK_ERROR); - }); - - it("should throw an error if operations fail", async function () { - Docker = class { - listContainers = sinon.stub().resolves([ - { - Names: ["/my_container"], - Id: "abc123", - }, - ]); - getContainer = sinon.stub().throws(new Error("test error")); - }; - networkService = new NetworkService(axios, ping, logger, http, Docker); - const job = { data: { url: "abc123", _id: "123", type: "docker" } }; - try { - await networkService.requestDocker(job); - } catch (error) { - expect(error.message).to.equal("test error"); - } - }); - - it("should throw an error if no matching images found", async function () { - Docker = class { - listContainers = sinon.stub().resolves([]); - getContainer = sinon.stub().throws(new Error("test error")); - }; - networkService = new NetworkService(axios, ping, logger, http, Docker); - const job = { data: { url: "abc123", _id: "123", type: "docker" } }; - try { - await networkService.requestDocker(job); - } catch (error) { - expect(error.message).to.equal(errorMessages.DOCKER_NOT_FOUND); - } - }); - }); - - describe("getStatus", function () { - beforeEach(function () { - networkService.requestPing = sinon.stub(); - networkService.requestHttp = sinon.stub(); - networkService.requestPagespeed = sinon.stub(); - networkService.requestHardware = sinon.stub(); - networkService.requestDocker = sinon.stub(); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should call requestPing if type is ping", async function () { - await networkService.getStatus({ data: { type: "ping" } }); - expect(networkService.requestPing.calledOnce).to.be.true; - expect(networkService.requestDocker.notCalled).to.be.true; - expect(networkService.requestHttp.notCalled).to.be.true; - expect(networkService.requestPagespeed.notCalled).to.be.true; - }); - - it("should call requestHttp if type is http", async function () { - await networkService.getStatus({ data: { type: "http" } }); - expect(networkService.requestPing.notCalled).to.be.true; - expect(networkService.requestDocker.notCalled).to.be.true; - expect(networkService.requestHttp.calledOnce).to.be.true; - expect(networkService.requestPagespeed.notCalled).to.be.true; - }); - - it("should call requestPagespeed if type is pagespeed", async function () { - await networkService.getStatus({ data: { type: "pagespeed" } }); - expect(networkService.requestPing.notCalled).to.be.true; - expect(networkService.requestDocker.notCalled).to.be.true; - expect(networkService.requestHttp.notCalled).to.be.true; - expect(networkService.requestPagespeed.calledOnce).to.be.true; - }); - - it("should call requestHardware if type is hardware", async function () { - await networkService.getStatus({ data: { type: "hardware" } }); - expect(networkService.requestHardware.calledOnce).to.be.true; - expect(networkService.requestDocker.notCalled).to.be.true; - expect(networkService.requestPing.notCalled).to.be.true; - expect(networkService.requestPagespeed.notCalled).to.be.true; - }); - - it("should call requestDocker if type is Docker", async function () { - await networkService.getStatus({ data: { type: "docker" } }); - expect(networkService.requestDocker.calledOnce).to.be.true; - expect(networkService.requestHardware.notCalled).to.be.true; - expect(networkService.requestPing.notCalled).to.be.true; - expect(networkService.requestPagespeed.notCalled).to.be.true; - }); - - it("should throw an error if an unknown type is provided", async function () { - try { - await networkService.getStatus({ data: { type: "unknown" } }); - } catch (error) { - expect(error.service).to.equal("NetworkService"); - expect(error.method).to.equal("getStatus"); - expect(error.message).to.equal("Unsupported type: unknown"); - } - }); - - it("should throw an error if job type is undefined", async function () { - try { - await networkService.getStatus({ data: { type: undefined } }); - } catch (error) { - expect(error.service).to.equal("NetworkService"); - expect(error.method).to.equal("getStatus"); - expect(error.message).to.equal("Unsupported type: unknown"); - } - }); - - it("should throw an error if job is empty", async function () { - try { - await networkService.getStatus({}); - } catch (error) { - expect(error.method).to.equal("getStatus"); - expect(error.message).to.equal("Unsupported type: unknown"); - } - }); - - it("should throw an error if job is null", async function () { - try { - await networkService.getStatus(null); - } catch (error) { - expect(error.service).to.equal("NetworkService"); - expect(error.method).to.equal("getStatus"); - expect(error.message).to.equal("Unsupported type: unknown"); - } - }); - }); -}); diff --git a/server/tests/services/notificationService.test.js b/server/tests/services/notificationService.test.js deleted file mode 100755 index 211cc8b46..000000000 --- a/server/tests/services/notificationService.test.js +++ /dev/null @@ -1,285 +0,0 @@ -import sinon from "sinon"; -import NotificationService from "../../service/notificationService.js"; -import { expect } from "chai"; - -describe("NotificationService", function () { - let emailService, db, logger, notificationService; - - beforeEach(function () { - db = { - getNotificationsByMonitorId: sinon.stub(), - }; - emailService = { - buildAndSendEmail: sinon.stub(), - }; - logger = { - warn: sinon.stub(), - }; - - notificationService = new NotificationService(emailService, db, logger); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("constructor", function () { - it("should create a new instance of NotificationService", function () { - expect(notificationService).to.be.an.instanceOf(NotificationService); - }); - }); - - describe("sendEmail", function () { - it("should send an email notification with Up Template", async function () { - const networkResponse = { - monitor: { - name: "Test Monitor", - url: "http://test.com", - }, - status: true, - prevStatus: false, - }; - const address = "test@test.com"; - await notificationService.sendEmail(networkResponse, address); - expect(notificationService.emailService.buildAndSendEmail.calledOnce).to.be.true; - expect( - notificationService.emailService.buildAndSendEmail.calledWith("serverIsUpTemplate", { monitor: "Test Monitor", url: "http://test.com" }) - ); - }); - - it("should send an email notification with Down Template", async function () { - const networkResponse = { - monitor: { - name: "Test Monitor", - url: "http://test.com", - }, - status: false, - prevStatus: true, - }; - const address = "test@test.com"; - await notificationService.sendEmail(networkResponse, address); - expect(notificationService.emailService.buildAndSendEmail.calledOnce).to.be.true; - }); - - it("should send an email notification with Up Template", async function () { - const networkResponse = { - monitor: { - name: "Test Monitor", - url: "http://test.com", - }, - status: true, - prevStatus: false, - }; - const address = "test@test.com"; - await notificationService.sendEmail(networkResponse, address); - expect(notificationService.emailService.buildAndSendEmail.calledOnce).to.be.true; - }); - }); - - describe("handleNotifications", function () { - it("should handle notifications based on the network response", async function () { - notificationService.sendEmail = sinon.stub(); - const res = await notificationService.handleNotifications({ - monitor: { - type: "email", - address: "www.google.com", - }, - }); - expect(res).to.be.true; - }); - - it("should handle hardware notifications", async function () { - notificationService.sendEmail = sinon.stub(); - const res = await notificationService.handleNotifications({ - monitor: { - type: "hardware", - address: "www.google.com", - }, - }); - expect(res).to.be.true; - }); - - it("should handle an error when getting notifications", async function () { - const testError = new Error("Test Error"); - notificationService.db.getNotificationsByMonitorId.rejects(testError); - await notificationService.handleNotifications({ monitorId: "123" }); - expect(notificationService.logger.warn.calledOnce).to.be.true; - }); - }); - - describe("sendHardwareEmail", function () { - let networkResponse, address, alerts; - - beforeEach(function () { - networkResponse = { - monitor: { - name: "Test Monitor", - url: "http://test.com", - }, - status: true, - prevStatus: false, - }; - address = "test@test.com"; - alerts = ["test"]; - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should send an email notification with Hardware Template", async function () { - emailService.buildAndSendEmail.resolves(true); - const res = await notificationService.sendHardwareEmail(networkResponse, address, alerts); - expect(res).to.be.true; - }); - - it("should return false if no alerts are provided", async function () { - alerts = []; - emailService.buildAndSendEmail.resolves(true); - const res = await notificationService.sendHardwareEmail(networkResponse, address, alerts); - expect(res).to.be.false; - }); - }); - - describe("handleStatusNotifications", function () { - let networkResponse; - - beforeEach(function () { - networkResponse = { - monitor: { - name: "Test Monitor", - url: "http://test.com", - }, - statusChanged: true, - status: true, - prevStatus: false, - }; - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should handle status notifications", async function () { - db.getNotificationsByMonitorId.resolves([{ type: "email", address: "test@test.com" }]); - const res = await notificationService.handleStatusNotifications(networkResponse); - expect(res).to.be.true; - }); - - it("should return false if status hasn't changed", async function () { - networkResponse.statusChanged = false; - const res = await notificationService.handleStatusNotifications(networkResponse); - expect(res).to.be.false; - }); - - it("should return false if prevStatus is undefined", async function () { - networkResponse.prevStatus = undefined; - const res = await notificationService.handleStatusNotifications(networkResponse); - expect(res).to.be.false; - }); - - it("should handle an error", async function () { - const testError = new Error("Test Error"); - db.getNotificationsByMonitorId.rejects(testError); - try { - await notificationService.handleStatusNotifications(networkResponse); - } catch (error) { - expect(error).to.be.an.instanceOf(Error); - expect(error.message).to.equal("Test Error"); - } - }); - }); - - describe("handleHardwareNotifications", function () { - let networkResponse; - - beforeEach(function () { - networkResponse = { - monitor: { - name: "Test Monitor", - url: "http://test.com", - thresholds: { - usage_cpu: 1, - usage_memory: 1, - usage_disk: 1, - }, - }, - payload: { - data: { - cpu: { - usage_percent: 0.655, - }, - memory: { - usage_percent: 0.783, - }, - disk: [ - { - name: "/dev/sda1", - usage_percent: 0.452, - }, - { - name: "/dev/sdb1", - usage_percent: 0.627, - }, - ], - }, - }, - }; - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("it should return false if no thresholds are set", function () { - it("should return false if no thresholds are set", async function () { - networkResponse.monitor.thresholds = undefined; - const res = await notificationService.handleHardwareNotifications(networkResponse); - expect(res).to.be.false; - }); - - it("should return false if metrics are null", async function () { - networkResponse.payload.data = null; - const res = await notificationService.handleHardwareNotifications(networkResponse); - expect(res).to.be.false; - }); - - it("should return true if request is well formed and thresholds > 0", async function () { - db.getNotificationsByMonitorId.resolves([ - { - type: "email", - address: "test@test.com", - alertThreshold: 1, - cpuAlertThreshold: 1, - memoryAlertThreshold: 1, - diskAlertThreshold: 1, - save: sinon.stub().resolves(), - }, - ]); - const res = await notificationService.handleHardwareNotifications(networkResponse); - expect(res).to.be.true; - }); - - it("should return true if thresholds are exceeded", async function () { - db.getNotificationsByMonitorId.resolves([ - { - type: "email", - address: "test@test.com", - alertThreshold: 1, - cpuAlertThreshold: 1, - memoryAlertThreshold: 1, - diskAlertThreshold: 1, - save: sinon.stub().resolves(), - }, - ]); - networkResponse.monitor.thresholds = { - usage_cpu: 0.01, - usage_memory: 0.01, - usage_disk: 0.01, - }; - const res = await notificationService.handleHardwareNotifications(networkResponse); - expect(res).to.be.true; - }); - }); - }); -}); diff --git a/server/tests/services/settingsService.test.js b/server/tests/services/settingsService.test.js deleted file mode 100755 index 6c9960e5e..000000000 --- a/server/tests/services/settingsService.test.js +++ /dev/null @@ -1,140 +0,0 @@ -import sinon from "sinon"; -import SettingsService from "../../service/settingsService.js"; -import { expect } from "chai"; -import NetworkService from "../../service/networkService.js"; -const SERVICE_NAME = "SettingsService"; - -describe("SettingsService", function () { - let sandbox, mockAppSettings; - - beforeEach(function () { - sandbox = sinon.createSandbox(); - sandbox.stub(process.env, "CLIENT_HOST").value("http://localhost"); - sandbox.stub(process.env, "JWT_SECRET").value("secret"); - sandbox.stub(process.env, "REFRESH_TOKEN_SECRET").value("refreshSecret"); - sandbox.stub(process.env, "DB_TYPE").value("postgres"); - sandbox.stub(process.env, "DB_CONNECTION_STRING").value("postgres://user:pass@localhost/db"); - sandbox.stub(process.env, "REDIS_HOST").value("localhost"); - sandbox.stub(process.env, "REDIS_PORT").value("6379"); - sandbox.stub(process.env, "TOKEN_TTL").value("3600"); - sandbox.stub(process.env, "REFRESH_TOKEN_TTL").value("86400"); - sandbox.stub(process.env, "PAGESPEED_API_KEY").value("apiKey"); - sandbox.stub(process.env, "SYSTEM_EMAIL_HOST").value("smtp.mailtrap.io"); - sandbox.stub(process.env, "SYSTEM_EMAIL_PORT").value("2525"); - sandbox.stub(process.env, "SYSTEM_EMAIL_ADDRESS").value("test@example.com"); - sandbox.stub(process.env, "SYSTEM_EMAIL_PASSWORD").value("password"); - }); - - mockAppSettings = { - settingOne: 123, - settingTwo: 456, - }; - - afterEach(function () { - sandbox.restore(); - sinon.restore(); - }); - - describe("constructor", function () { - it("should construct a new SettingsService", function () { - const settingsService = new SettingsService(mockAppSettings); - expect(settingsService.appSettings).to.equal(mockAppSettings); - }); - }); - - describe("loadSettings", function () { - it("should load settings from DB when environment variables are not set", async function () { - const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" }; - const appSettings = { findOne: sinon.stub().returns(dbSettings) }; - const settingsService = new SettingsService(appSettings); - settingsService.settings = {}; - const result = await settingsService.loadSettings(); - expect(result).to.deep.equal(dbSettings); - }); - - it("should throw an error if settings are not found", async function () { - const appSettings = { findOne: sinon.stub().returns(null) }; - const settingsService = new SettingsService(appSettings); - settingsService.settings = null; - - try { - await settingsService.loadSettings(); - } catch (error) { - expect(error.message).to.equal("Settings not found"); - expect(error.service).to.equal(SERVICE_NAME); - expect(error.method).to.equal("loadSettings"); - } - }); - - it("should add its method and service name to error if not present", async function () { - const appSettings = { findOne: sinon.stub().throws(new Error("Test error")) }; - const settingsService = new SettingsService(appSettings); - try { - await settingsService.loadSettings(); - } catch (error) { - expect(error.message).to.equal("Test error"); - expect(error.service).to.equal(SERVICE_NAME); - expect(error.method).to.equal("loadSettings"); - } - }); - - it("should not add its method and service name to error if present", async function () { - const error = new Error("Test error"); - error.method = "otherMethod"; - error.service = "OTHER_SERVICE"; - const appSettings = { findOne: sinon.stub().throws(error) }; - const settingsService = new SettingsService(appSettings); - try { - await settingsService.loadSettings(); - } catch (error) { - expect(error.message).to.equal("Test error"); - expect(error.service).to.equal("OTHER_SERVICE"); - expect(error.method).to.equal("otherMethod"); - } - }); - - it("should merge DB settings with environment variables", async function () { - const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" }; - const appSettings = { findOne: sinon.stub().returns(dbSettings) }; - const settingsService = new SettingsService(appSettings); - const result = await settingsService.loadSettings(); - expect(result).to.deep.equal(settingsService.settings); - expect(settingsService.settings.logLevel).to.equal("debug"); - expect(settingsService.settings.apiBaseUrl).to.equal("http://localhost"); - }); - }); - - describe("reloadSettings", function () { - it("should call loadSettings", async function () { - const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" }; - const appSettings = { findOne: sinon.stub().returns(dbSettings) }; - const settingsService = new SettingsService(appSettings); - settingsService.settings = {}; - const result = await settingsService.reloadSettings(); - expect(result).to.deep.equal(dbSettings); - }); - }); - - describe("getSettings", function () { - it("should return the current settings", function () { - const dbSettings = { logLevel: "debug", apiBaseUrl: "http://localhost" }; - const appSettings = { findOne: sinon.stub().returns(dbSettings) }; - const settingsService = new SettingsService(appSettings); - settingsService.settings = dbSettings; - const result = settingsService.getSettings(); - expect(result).to.deep.equal(dbSettings); - }); - - it("should throw an error if settings have not been loaded", function () { - const appSettings = { findOne: sinon.stub().returns(null) }; - const settingsService = new SettingsService(appSettings); - settingsService.settings = null; - - try { - settingsService.getSettings(); - } catch (error) { - expect(error.message).to.equal("Settings have not been loaded"); - } - }); - }); -}); diff --git a/server/tests/services/statusService.test.js b/server/tests/services/statusService.test.js deleted file mode 100755 index 289a36884..000000000 --- a/server/tests/services/statusService.test.js +++ /dev/null @@ -1,255 +0,0 @@ -import sinon from "sinon"; -import StatusService from "../../service/statusService.js"; -import { afterEach, describe } from "node:test"; - -describe("StatusService", () => { - let db, logger, statusService; - - beforeEach(function () { - db = { - getMonitorById: sinon.stub(), - createCheck: sinon.stub(), - createPagespeedCheck: sinon.stub(), - }; - logger = { - info: sinon.stub(), - error: sinon.stub(), - }; - statusService = new StatusService(db, logger); - }); - - afterEach(() => { - sinon.restore(); - }); - - describe("constructor", () => { - it("should create an instance of StatusService", function () { - expect(statusService).to.be.an.instanceOf(StatusService); - }); - }); - - describe("getStatusString", () => { - it("should return 'up' if status is true", function () { - expect(statusService.getStatusString(true)).to.equal("up"); - }); - - it("should return 'down' if status is false", function () { - expect(statusService.getStatusString(false)).to.equal("down"); - }); - - it("should return 'unknown' if status is undefined or null", function () { - expect(statusService.getStatusString(undefined)).to.equal("unknown"); - }); - }); - - describe("updateStatus", () => { - beforeEach(function () { - // statusService.insertCheck = sinon.stub().resolves; - }); - - afterEach(() => { - sinon.restore(); - }); - - it("should throw an error if an error occurs", async function () { - const error = new Error("Test error"); - statusService.db.getMonitorById = sinon.stub().throws(error); - try { - await statusService.updateStatus({ monitorId: "test", status: true }); - } catch (error) { - expect(error.message).to.equal("Test error"); - } - // expect(statusService.insertCheck.calledOnce).to.be.true; - }); - - it("should return {statusChanged: false} if status hasn't changed", async function () { - statusService.db.getMonitorById = sinon.stub().returns({ status: true }); - const result = await statusService.updateStatus({ - monitorId: "test", - status: true, - }); - expect(result).to.deep.equal({ statusChanged: false }); - // expect(statusService.insertCheck.calledOnce).to.be.true; - }); - - it("should return {statusChanged: true} if status has changed from down to up", async function () { - statusService.db.getMonitorById = sinon.stub().returns({ status: false, save: sinon.stub() }); - const result = await statusService.updateStatus({ - monitorId: "test", - status: true, - }); - expect(result.statusChanged).to.be.true; - expect(result.monitor.status).to.be.true; - expect(result.prevStatus).to.be.false; - // expect(statusService.insertCheck.calledOnce).to.be.true; - }); - - it("should return {statusChanged: true} if status has changed from up to down", async function () { - statusService.db.getMonitorById = sinon.stub().returns({ status: true, save: sinon.stub() }); - const result = await statusService.updateStatus({ - monitorId: "test", - status: false, - }); - expect(result.statusChanged).to.be.true; - expect(result.monitor.status).to.be.false; - expect(result.prevStatus).to.be.true; - // expect(statusService.insertCheck.calledOnce).to.be.true; - }); - }); - - describe("buildCheck", () => { - it("should build a check object", function () { - const check = statusService.buildCheck({ - monitorId: "test", - type: "test", - status: true, - responseTime: 100, - code: 200, - message: "Test message", - payload: { test: "test" }, - }); - expect(check.monitorId).to.equal("test"); - expect(check.status).to.be.true; - expect(check.statusCode).to.equal(200); - expect(check.responseTime).to.equal(100); - expect(check.message).to.equal("Test message"); - }); - - it("should build a check object for pagespeed type", function () { - const check = statusService.buildCheck({ - monitorId: "test", - type: "pagespeed", - status: true, - responseTime: 100, - code: 200, - message: "Test message", - payload: { - lighthouseResult: { - categories: { - accessibility: { score: 1 }, - "best-practices": { score: 1 }, - performance: { score: 1 }, - seo: { score: 1 }, - }, - audits: { - "cumulative-layout-shift": { score: 1 }, - "speed-index": { score: 1 }, - "first-contentful-paint": { score: 1 }, - "largest-contentful-paint": { score: 1 }, - "total-blocking-time": { score: 1 }, - }, - }, - }, - }); - expect(check.monitorId).to.equal("test"); - expect(check.status).to.be.true; - expect(check.statusCode).to.equal(200); - expect(check.responseTime).to.equal(100); - expect(check.message).to.equal("Test message"); - expect(check.accessibility).to.equal(100); - expect(check.bestPractices).to.equal(100); - expect(check.performance).to.equal(100); - expect(check.seo).to.equal(100); - expect(check.audits).to.deep.equal({ - cls: { score: 1 }, - si: { score: 1 }, - fcp: { score: 1 }, - lcp: { score: 1 }, - tbt: { score: 1 }, - }); - }); - - it("should build a check object for pagespeed type with missing data", function () { - const check = statusService.buildCheck({ - monitorId: "test", - type: "pagespeed", - status: true, - responseTime: 100, - code: 200, - message: "Test message", - payload: { - lighthouseResult: { - categories: {}, - audits: {}, - }, - }, - }); - expect(check.monitorId).to.equal("test"); - expect(check.status).to.be.true; - expect(check.statusCode).to.equal(200); - expect(check.responseTime).to.equal(100); - expect(check.message).to.equal("Test message"); - expect(check.accessibility).to.equal(0); - expect(check.bestPractices).to.equal(0); - expect(check.performance).to.equal(0); - expect(check.seo).to.equal(0); - expect(check.audits).to.deep.equal({ - cls: 0, - si: 0, - fcp: 0, - lcp: 0, - tbt: 0, - }); - }); - - it("should build a check for hardware type", function () { - const check = statusService.buildCheck({ - monitorId: "test", - type: "hardware", - status: true, - responseTime: 100, - code: 200, - message: "Test message", - payload: { data: { cpu: "cpu", memory: "memory", disk: "disk", host: "host" } }, - }); - expect(check.monitorId).to.equal("test"); - expect(check.status).to.be.true; - expect(check.statusCode).to.equal(200); - expect(check.responseTime).to.equal(100); - expect(check.message).to.equal("Test message"); - expect(check.cpu).to.equal("cpu"); - expect(check.memory).to.equal("memory"); - expect(check.disk).to.equal("disk"); - expect(check.host).to.equal("host"); - }); - - it("should build a check for hardware type with missing data", function () { - const check = statusService.buildCheck({ - monitorId: "test", - type: "hardware", - status: true, - responseTime: 100, - code: 200, - message: "Test message", - payload: {}, - }); - expect(check.monitorId).to.equal("test"); - expect(check.status).to.be.true; - expect(check.statusCode).to.equal(200); - expect(check.responseTime).to.equal(100); - expect(check.message).to.equal("Test message"); - expect(check.cpu).to.deep.equal({}); - expect(check.memory).to.deep.equal({}); - expect(check.disk).to.deep.equal({}); - expect(check.host).to.deep.equal({}); - }); - }); - - describe("insertCheck", () => { - it("should log an error if one is thrown", async function () { - const testError = new Error("Test error"); - statusService.db.createCheck = sinon.stub().throws(testError); - try { - await statusService.insertCheck({ monitorId: "test" }); - } catch (error) { - expect(error.message).to.equal(testError.message); - } - expect(statusService.logger.error.calledOnce).to.be.true; - }); - - it("should insert a check into the database", async function () { - await statusService.insertCheck({ monitorId: "test", type: "http" }); - expect(statusService.db.createCheck.calledOnce).to.be.true; - }); - }); -}); diff --git a/server/tests/utils/dataUtils.test.js b/server/tests/utils/dataUtils.test.js deleted file mode 100755 index ec9eabb40..000000000 --- a/server/tests/utils/dataUtils.test.js +++ /dev/null @@ -1,61 +0,0 @@ -import { NormalizeData, calculatePercentile } from "../../utils/dataUtils.js"; -import sinon from "sinon"; - -describe("NormalizeData", function () { - it("should normalize response times when checks length is greater than 1", function () { - const checks = [ - { responseTime: 20, _doc: { id: 1 } }, - { responseTime: 40, _doc: { id: 2 } }, - { responseTime: 60, _doc: { id: 3 } }, - ]; - const rangeMin = 1; - const rangeMax = 100; - - const result = NormalizeData(checks, rangeMin, rangeMax); - - expect(result).to.be.an("array"); - expect(result).to.have.lengthOf(3); - result.forEach((check) => { - expect(check).to.have.property("responseTime").that.is.a("number"); - expect(check).to.have.property("originalResponseTime").that.is.a("number"); - }); - }); - - it("should return checks with original response times when checks length is 1", function () { - const checks = [{ responseTime: 20, _doc: { id: 1 } }]; - const rangeMin = 1; - const rangeMax = 100; - - const result = NormalizeData(checks, rangeMin, rangeMax); - expect(result).to.be.an("array"); - expect(result).to.have.lengthOf(1); - expect(result[0]).to.have.property("originalResponseTime", 20); - }); - - it("should handle edge cases with extreme response times", function () { - const checks = [ - { responseTime: 5, _doc: { id: 1 } }, - { responseTime: 95, _doc: { id: 2 } }, - ]; - const rangeMin = 1; - const rangeMax = 100; - - const result = NormalizeData(checks, rangeMin, rangeMax); - - expect(result).to.be.an("array"); - expect(result).to.have.lengthOf(2); - expect(result[0]).to.have.property("responseTime").that.is.at.least(rangeMin); - expect(result[1]).to.have.property("responseTime").that.is.at.most(rangeMax); - }); -}); - -describe("calculatePercentile", function () { - it("should return the lower value when upper is greater than or equal to the length of the sorted array", function () { - const checks = [{ responseTime: 10 }, { responseTime: 20 }, { responseTime: 30 }, { responseTime: 40 }, { responseTime: 50 }]; - - const percentile = 100; - const result = calculatePercentile(checks, percentile); - const expected = 50; - expect(result).to.equal(expected); - }); -}); diff --git a/server/tests/utils/imageProcessing.test.js b/server/tests/utils/imageProcessing.test.js deleted file mode 100755 index 09330c317..000000000 --- a/server/tests/utils/imageProcessing.test.js +++ /dev/null @@ -1,53 +0,0 @@ -import { expect } from "chai"; -import sinon from "sinon"; -import sharp from "sharp"; -import { GenerateAvatarImage } from "../../utils/imageProcessing.js"; - -describe("imageProcessing - GenerateAvatarImage", function () { - it("should resize the image to 64x64 and return a base64 string", async function () { - const file = { - buffer: Buffer.from("test image buffer"), - }; - - // Stub the sharp function - const toBufferStub = sinon.stub().resolves(Buffer.from("resized image buffer")); - const resizeStub = sinon.stub().returns({ toBuffer: toBufferStub }); - const sharpStub = sinon.stub(sharp.prototype, "resize").returns({ toBuffer: toBufferStub }); - - const result = await GenerateAvatarImage(file); - - // Verify the result - const expected = Buffer.from("resized image buffer").toString("base64"); - expect(result).to.equal(expected); - - // Verify that the sharp function was called with the correct arguments - expect(sharpStub.calledOnceWith({ width: 64, height: 64, fit: "cover" })).to.be.true; - expect(toBufferStub.calledOnce).to.be.true; - - // Restore the stubbed functions - sharpStub.restore(); - }); - - it("should throw an error if resizing fails", async function () { - const file = { - buffer: Buffer.from("test image buffer"), - }; - - // Stub the sharp function to throw an error - const toBufferStub = sinon.stub().rejects(new Error("Resizing failed")); - const resizeStub = sinon.stub().returns({ toBuffer: toBufferStub }); - const sharpStub = sinon.stub(sharp.prototype, "resize").returns({ toBuffer: toBufferStub }); - - try { - await GenerateAvatarImage(file); - // If no error is thrown, fail the test - expect.fail("Expected error to be thrown"); - } catch (error) { - // Verify that the error message is correct - expect(error.message).to.equal("Resizing failed"); - } - - // Restore the stubbed functions - sharpStub.restore(); - }); -}); diff --git a/server/tests/utils/logger.test.js b/server/tests/utils/logger.test.js deleted file mode 100755 index 2d5bd82a9..000000000 --- a/server/tests/utils/logger.test.js +++ /dev/null @@ -1,139 +0,0 @@ -import sinon from "sinon"; -import logger from "../../utils/logger.js"; -import { Logger } from "../../utils/logger.js"; -import winston from "winston"; - -describe("Logger", function () { - let infoStub, warnStub, errorStub; - - beforeEach(function () { - infoStub = sinon.stub(logger.logger, "info"); - warnStub = sinon.stub(logger.logger, "warn"); - errorStub = sinon.stub(logger.logger, "error"); - }); - - afterEach(function () { - sinon.restore(); - }); - - describe("constructor", function () { - let createLoggerStub; - - beforeEach(function () { - createLoggerStub = sinon.stub(winston, "createLogger"); - }); - - afterEach(function () { - sinon.restore(); - }); - - it("should convert message to JSON string if it is an object", function () { - const logMessage = { key: "value" }; - const expectedMessage = JSON.stringify(logMessage, null, 2); - - createLoggerStub.callsFake((config) => { - const consoleTransport = config.transports[0]; - const logEntry = { - level: "info", - message: logMessage, - timestamp: new Date().toISOString(), - }; - const formattedMessage = consoleTransport.format.transform(logEntry); - expect(formattedMessage).to.include(expectedMessage); - return { log: sinon.spy() }; - }); - - const logger = new Logger(); - logger.logger.info(logMessage); - }); - - it("should convert details to JSON string if it is an object", function () { - const logDetails = { key: "value" }; - const expectedDetails = JSON.stringify(logDetails, null, 2); // Removed .s - - createLoggerStub.callsFake((config) => { - const consoleTransport = config.transports[0]; - const logEntry = { - level: "info", - message: "", // Add empty message since it's required - details: logDetails, - timestamp: new Date().toISOString(), - }; - const formattedMessage = consoleTransport.format.transform(logEntry); - expect(formattedMessage).to.include(expectedDetails); - return { info: sinon.spy() }; // Changed to return info method - }); - - const logger = new Logger(); - logger.logger.info("", { details: logDetails }); // Updated to pass details properly - }); - }); - - describe("info", function () { - it("should log an informational message", function () { - const config = { - message: "Info message", - service: "TestService", - method: "TestMethod", - details: { key: "value" }, - }; - - logger.info(config); - - expect(infoStub.calledOnce).to.be.true; - expect( - infoStub.calledWith(config.message, { - service: config.service, - method: config.method, - details: config.details, - }) - ).to.be.true; - }); - }); - - describe("warn", function () { - it("should log a warning message", function () { - const config = { - message: "Warning message", - service: "TestService", - method: "TestMethod", - details: { key: "value" }, - }; - - logger.warn(config); - - expect(warnStub.calledOnce).to.be.true; - expect( - warnStub.calledWith(config.message, { - service: config.service, - method: config.method, - details: config.details, - }) - ).to.be.true; - }); - }); - - describe("error", function () { - it("should log an error message", function () { - const config = { - message: "Error message", - service: "TestService", - method: "TestMethod", - details: { key: "value" }, - stack: "Error stack trace", - }; - - logger.error(config); - - expect(errorStub.calledOnce).to.be.true; - expect( - errorStub.calledWith(config.message, { - service: config.service, - method: config.method, - details: config.details, - stack: config.stack, - }) - ).to.be.true; - }); - }); -}); diff --git a/server/tests/utils/messages.test.js b/server/tests/utils/messages.test.js deleted file mode 100755 index a7e98c477..000000000 --- a/server/tests/utils/messages.test.js +++ /dev/null @@ -1,23 +0,0 @@ -import { errorMessages, successMessages } from "../../utils/messages.js"; -describe("Messages", function () { - describe("messages - errorMessages", function () { - it("should have a DB_FIND_MONITOR_BY_ID function", function () { - const monitorId = "12345"; - expect(errorMessages.DB_FIND_MONITOR_BY_ID(monitorId)).to.equal(`Monitor with id ${monitorId} not found`); - }); - - it("should have a DB_DELETE_CHECKS function", function () { - const monitorId = "12345"; - expect(errorMessages.DB_DELETE_CHECKS(monitorId)).to.equal(`No checks found for monitor with id ${monitorId}`); - }); - }); - - describe("messages - successMessages", function () { - it("should have a MONITOR_GET_BY_USER_ID function", function () { - const userId = "12345"; - expect(successMessages.MONITOR_GET_BY_USER_ID(userId)).to.equal(`Got monitor for ${userId} successfully"`); - }); - - // Add more tests for other success messages as needed - }); -}); diff --git a/server/tests/utils/utils.test.js b/server/tests/utils/utils.test.js deleted file mode 100755 index 2630bb8e6..000000000 --- a/server/tests/utils/utils.test.js +++ /dev/null @@ -1,50 +0,0 @@ -import { ParseBoolean, getTokenFromHeaders } from "../../utils/utils.js"; - -describe("utils - ParseBoolean", function () { - it("should return true", function () { - const result = ParseBoolean("true"); - expect(result).to.be.true; - }); - - it("should return false", function () { - const result = ParseBoolean("false"); - expect(result).to.be.false; - }); - - it("should return false", function () { - const result = ParseBoolean(null); - expect(result).to.be.false; - }); - - it("should return false", function () { - const result = ParseBoolean(undefined); - expect(result).to.be.false; - }); -}); - -describe("utils - getTokenFromHeaders", function () { - it("should throw an error if authorization header is missing", function () { - const headers = {}; - expect(() => getTokenFromHeaders(headers)).to.throw("No auth headers"); - }); - - it("should throw an error if authorization header does not start with Bearer", function () { - const headers = { authorization: "Basic abcdef" }; - expect(() => getTokenFromHeaders(headers)).to.throw("Invalid auth headers"); - }); - - it("should return the token if authorization header is correctly formatted", function () { - const headers = { authorization: "Bearer abcdef" }; - expect(getTokenFromHeaders(headers)).to.equal("abcdef"); - }); - - it("should throw an error if authorization header has more than two parts", function () { - const headers = { authorization: "Bearer abc def" }; - expect(() => getTokenFromHeaders(headers)).to.throw("Invalid auth headers"); - }); - - it("should throw an error if authorization header has less than two parts", function () { - const headers = { authorization: "Bearer" }; - expect(() => getTokenFromHeaders(headers)).to.throw("Invalid auth headers"); - }); -}); diff --git a/server/tsconfig.json b/server/tsconfig.json new file mode 100644 index 000000000..3aa773ab1 --- /dev/null +++ b/server/tsconfig.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist", + "module": "nodenext", + "target": "esnext", + "types": ["node"], + "sourceMap": true, + "declaration": false, + "declarationMap": false, + "noUncheckedIndexedAccess": true, + "exactOptionalPropertyTypes": true, + "strict": true, + "isolatedModules": true, + "noUncheckedSideEffectImports": true, + "moduleDetection": "force", + "skipLibCheck": true, + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "forceConsistentCasingInFileNames": true, + "allowJs": true, + "checkJs": false + }, + "exclude": ["node_modules", "dist", "**/*.config.js"] +} From f767125acd078252bb3bb1551cda01a944b76f76 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 22 Sep 2025 13:03:54 -0700 Subject: [PATCH 002/400] tsx, tpyescript --- server/package-lock.json | 17 ++++++++++++++++- server/package.json | 3 ++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index e2affbca0..06c1c30b3 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -55,7 +55,8 @@ "nodemon": "3.1.9", "prettier": "^3.3.3", "sinon": "19.0.2", - "tsx": "4.20.5" + "tsx": "4.20.5", + "typescript": "5.9.2" } }, "node_modules/@asamuzakjp/css-color": { @@ -9139,6 +9140,20 @@ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==", "license": "MIT" }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, "node_modules/uglify-js": { "version": "3.19.3", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", diff --git a/server/package.json b/server/package.json index c57c3a4ce..637c785cc 100755 --- a/server/package.json +++ b/server/package.json @@ -64,6 +64,7 @@ "nodemon": "3.1.9", "prettier": "^3.3.3", "sinon": "19.0.2", - "tsx": "4.20.5" + "tsx": "4.20.5", + "typescript": "5.9.2" } } From 23126a92cdcbd856fdbeddcb9b5c46a3b766537f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 22 Sep 2025 13:04:45 -0700 Subject: [PATCH 003/400] move v1 controllers to v1 dir --- server/src/config/controllers.js | 24 +++++++++---------- .../{ => v1}/announcementsController.js | 2 +- .../controllers/{ => v1}/authController.js | 2 +- .../controllers/{ => v1}/baseController.js | 2 +- .../controllers/{ => v1}/checkController.js | 2 +- .../controllers/{ => v1}/controllerUtils.js | 0 .../{ => v1}/diagnosticController.js | 0 .../controllers/{ => v1}/inviteController.js | 2 +- .../src/controllers/{ => v1}/logController.js | 0 .../{ => v1}/maintenanceWindowController.js | 2 +- .../controllers/{ => v1}/monitorController.js | 2 +- .../{ => v1}/notificationController.js | 2 +- .../controllers/{ => v1}/queueController.js | 0 .../{ => v1}/settingsController.js | 4 ++-- .../{ => v1}/statusPageController.js | 2 +- server/src/routes/v1/monitorRoute.js | 2 +- 16 files changed, 24 insertions(+), 24 deletions(-) rename server/src/controllers/{ => v1}/announcementsController.js (96%) rename server/src/controllers/{ => v1}/authController.js (99%) rename server/src/controllers/{ => v1}/baseController.js (97%) rename server/src/controllers/{ => v1}/checkController.js (99%) rename server/src/controllers/{ => v1}/controllerUtils.js (100%) rename server/src/controllers/{ => v1}/diagnosticController.js (100%) rename server/src/controllers/{ => v1}/inviteController.js (99%) rename server/src/controllers/{ => v1}/logController.js (100%) rename server/src/controllers/{ => v1}/maintenanceWindowController.js (99%) mode change 100755 => 100644 rename server/src/controllers/{ => v1}/monitorController.js (99%) mode change 100755 => 100644 rename server/src/controllers/{ => v1}/notificationController.js (98%) mode change 100755 => 100644 rename server/src/controllers/{ => v1}/queueController.js (100%) rename server/src/controllers/{ => v1}/settingsController.js (95%) mode change 100755 => 100644 rename server/src/controllers/{ => v1}/statusPageController.js (99%) mode change 100755 => 100644 diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js index 693e35ab8..5c54579eb 100644 --- a/server/src/config/controllers.js +++ b/server/src/config/controllers.js @@ -1,19 +1,19 @@ -import { createCommonDependencies } from "../controllers/baseController.js"; +import { createCommonDependencies } from "../controllers/v1/baseController.js"; // Services // Controllers -import MonitorController from "../controllers/monitorController.js"; -import AuthController from "../controllers/authController.js"; -import SettingsController from "../controllers/settingsController.js"; -import CheckController from "../controllers/checkController.js"; -import InviteController from "../controllers/inviteController.js"; -import MaintenanceWindowController from "../controllers/maintenanceWindowController.js"; -import QueueController from "../controllers/queueController.js"; -import LogController from "../controllers/logController.js"; -import StatusPageController from "../controllers/statusPageController.js"; -import NotificationController from "../controllers/notificationController.js"; -import DiagnosticController from "../controllers/diagnosticController.js"; +import MonitorController from "../controllers/v1/monitorController.js"; +import AuthController from "../controllers/v1/authController.js"; +import SettingsController from "../controllers/v1/settingsController.js"; +import CheckController from "../controllers/v1/checkController.js"; +import InviteController from "../controllers/v1/inviteController.js"; +import MaintenanceWindowController from "../controllers/v1/maintenanceWindowController.js"; +import QueueController from "../controllers/v1/queueController.js"; +import LogController from "../controllers/v1/logController.js"; +import StatusPageController from "../controllers/v1/statusPageController.js"; +import NotificationController from "../controllers/v1/notificationController.js"; +import DiagnosticController from "../controllers/v1/diagnosticController.js"; export const initializeControllers = (services) => { const controllers = {}; diff --git a/server/src/controllers/announcementsController.js b/server/src/controllers/v1/announcementsController.js similarity index 96% rename from server/src/controllers/announcementsController.js rename to server/src/controllers/v1/announcementsController.js index 430859791..0425b44aa 100755 --- a/server/src/controllers/announcementsController.js +++ b/server/src/controllers/v1/announcementsController.js @@ -1,4 +1,4 @@ -import { createAnnouncementValidation } from "../validation/joi.js"; +import { createAnnouncementValidation } from "../../validation/joi.js"; import BaseController from "./baseController.js"; const SERVICE_NAME = "announcementController"; diff --git a/server/src/controllers/authController.js b/server/src/controllers/v1/authController.js similarity index 99% rename from server/src/controllers/authController.js rename to server/src/controllers/v1/authController.js index c186e8423..17ed517eb 100755 --- a/server/src/controllers/authController.js +++ b/server/src/controllers/v1/authController.js @@ -10,7 +10,7 @@ import { editUserByIdParamValidation, editUserByIdBodyValidation, editSuperadminUserByIdBodyValidation, -} from "../validation/joi.js"; +} from "../../validation/joi.js"; const SERVICE_NAME = "authController"; diff --git a/server/src/controllers/baseController.js b/server/src/controllers/v1/baseController.js similarity index 97% rename from server/src/controllers/baseController.js rename to server/src/controllers/v1/baseController.js index 910cf57b4..314905c82 100644 --- a/server/src/controllers/baseController.js +++ b/server/src/controllers/v1/baseController.js @@ -1,4 +1,4 @@ -import { AppError } from "../service/infrastructure/errorService.js"; +import { AppError } from "../../service/infrastructure/errorService.js"; export const createCommonDependencies = (db, errorService, logger, stringService) => { return { diff --git a/server/src/controllers/checkController.js b/server/src/controllers/v1/checkController.js similarity index 99% rename from server/src/controllers/checkController.js rename to server/src/controllers/v1/checkController.js index 4069df3ff..ea293d74d 100755 --- a/server/src/controllers/checkController.js +++ b/server/src/controllers/v1/checkController.js @@ -9,7 +9,7 @@ import { ackCheckBodyValidation, ackAllChecksParamValidation, ackAllChecksBodyValidation, -} from "../validation/joi.js"; +} from "../../validation/joi.js"; const SERVICE_NAME = "checkController"; diff --git a/server/src/controllers/controllerUtils.js b/server/src/controllers/v1/controllerUtils.js similarity index 100% rename from server/src/controllers/controllerUtils.js rename to server/src/controllers/v1/controllerUtils.js diff --git a/server/src/controllers/diagnosticController.js b/server/src/controllers/v1/diagnosticController.js similarity index 100% rename from server/src/controllers/diagnosticController.js rename to server/src/controllers/v1/diagnosticController.js diff --git a/server/src/controllers/inviteController.js b/server/src/controllers/v1/inviteController.js similarity index 99% rename from server/src/controllers/inviteController.js rename to server/src/controllers/v1/inviteController.js index a377234c0..1ec586f31 100755 --- a/server/src/controllers/inviteController.js +++ b/server/src/controllers/v1/inviteController.js @@ -1,4 +1,4 @@ -import { inviteBodyValidation, inviteVerificationBodyValidation } from "../validation/joi.js"; +import { inviteBodyValidation, inviteVerificationBodyValidation } from "../../validation/joi.js"; import BaseController from "./baseController.js"; const SERVICE_NAME = "inviteController"; diff --git a/server/src/controllers/logController.js b/server/src/controllers/v1/logController.js similarity index 100% rename from server/src/controllers/logController.js rename to server/src/controllers/v1/logController.js diff --git a/server/src/controllers/maintenanceWindowController.js b/server/src/controllers/v1/maintenanceWindowController.js old mode 100755 new mode 100644 similarity index 99% rename from server/src/controllers/maintenanceWindowController.js rename to server/src/controllers/v1/maintenanceWindowController.js index 2fdb1ee15..be373ace5 --- a/server/src/controllers/maintenanceWindowController.js +++ b/server/src/controllers/v1/maintenanceWindowController.js @@ -6,7 +6,7 @@ import { getMaintenanceWindowsByMonitorIdParamValidation, getMaintenanceWindowsByTeamIdQueryValidation, deleteMaintenanceWindowByIdParamValidation, -} from "../validation/joi.js"; +} from "../../validation/joi.js"; import BaseController from "./baseController.js"; const SERVICE_NAME = "maintenanceWindowController"; diff --git a/server/src/controllers/monitorController.js b/server/src/controllers/v1/monitorController.js old mode 100755 new mode 100644 similarity index 99% rename from server/src/controllers/monitorController.js rename to server/src/controllers/v1/monitorController.js index 0f40a4a01..462d5a278 --- a/server/src/controllers/monitorController.js +++ b/server/src/controllers/v1/monitorController.js @@ -11,7 +11,7 @@ import { getCertificateParamValidation, getHardwareDetailsByIdParamValidation, getHardwareDetailsByIdQueryValidation, -} from "../validation/joi.js"; +} from "../../validation/joi.js"; import sslChecker from "ssl-checker"; import { fetchMonitorCertificate } from "./controllerUtils.js"; import BaseController from "./baseController.js"; diff --git a/server/src/controllers/notificationController.js b/server/src/controllers/v1/notificationController.js old mode 100755 new mode 100644 similarity index 98% rename from server/src/controllers/notificationController.js rename to server/src/controllers/v1/notificationController.js index e00dd5b6f..37c2b4e60 --- a/server/src/controllers/notificationController.js +++ b/server/src/controllers/v1/notificationController.js @@ -1,4 +1,4 @@ -import { createNotificationBodyValidation } from "../validation/joi.js"; +import { createNotificationBodyValidation } from "../../validation/joi.js"; import BaseController from "./baseController.js"; const SERVICE_NAME = "NotificationController"; diff --git a/server/src/controllers/queueController.js b/server/src/controllers/v1/queueController.js similarity index 100% rename from server/src/controllers/queueController.js rename to server/src/controllers/v1/queueController.js diff --git a/server/src/controllers/settingsController.js b/server/src/controllers/v1/settingsController.js old mode 100755 new mode 100644 similarity index 95% rename from server/src/controllers/settingsController.js rename to server/src/controllers/v1/settingsController.js index 7da724d23..3db73a05a --- a/server/src/controllers/settingsController.js +++ b/server/src/controllers/v1/settingsController.js @@ -1,5 +1,5 @@ -import { updateAppSettingsBodyValidation } from "../validation/joi.js"; -import { sendTestEmailBodyValidation } from "../validation/joi.js"; +import { updateAppSettingsBodyValidation } from "../../validation/joi.js"; +import { sendTestEmailBodyValidation } from "../../validation/joi.js"; import BaseController from "./baseController.js"; const SERVICE_NAME = "SettingsController"; diff --git a/server/src/controllers/statusPageController.js b/server/src/controllers/v1/statusPageController.js old mode 100755 new mode 100644 similarity index 99% rename from server/src/controllers/statusPageController.js rename to server/src/controllers/v1/statusPageController.js index a46688590..de6a1ca7c --- a/server/src/controllers/statusPageController.js +++ b/server/src/controllers/v1/statusPageController.js @@ -1,4 +1,4 @@ -import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation } from "../validation/joi.js"; +import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation } from "../../validation/joi.js"; import BaseController from "./baseController.js"; const SERVICE_NAME = "statusPageController"; diff --git a/server/src/routes/v1/monitorRoute.js b/server/src/routes/v1/monitorRoute.js index f8f219b83..dde72e256 100755 --- a/server/src/routes/v1/monitorRoute.js +++ b/server/src/routes/v1/monitorRoute.js @@ -1,7 +1,7 @@ import { Router } from "express"; import { isAllowed } from "../../middleware/isAllowed.js"; import multer from "multer"; -import { fetchMonitorCertificate } from "../../controllers/controllerUtils.js"; +import { fetchMonitorCertificate } from "../../controllers/v1/controllerUtils.js"; const upload = multer({ storage: multer.memoryStorage(), // Store file in memory as Buffer }); From 0b646ee64f5c6edff0857c2fdaf2812a64edf2bd Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 22 Sep 2025 13:26:45 -0700 Subject: [PATCH 004/400] update docker files to build --- docker/dev/build_images.sh | 3 +-- docker/dev/server.Dockerfile | 4 +++- docker/dist-arm/server.Dockerfile | 8 ++++--- docker/dist-mono/docker-compose-local.yaml | 27 ++++++++++++++++++++++ docker/dist-mono/server.Dockerfile | 7 ++++-- docker/dist/server.Dockerfile | 4 +++- docker/prod/server.Dockerfile | 2 ++ docker/staging/server.Dockerfile | 4 +++- 8 files changed, 49 insertions(+), 10 deletions(-) create mode 100755 docker/dist-mono/docker-compose-local.yaml diff --git a/docker/dev/build_images.sh b/docker/dev/build_images.sh index e94be9fa5..eb43e31d8 100755 --- a/docker/dev/build_images.sh +++ b/docker/dev/build_images.sh @@ -5,11 +5,10 @@ cd "$(dirname "$0")" cd ../.. # Define service names and their corresponding Dockerfiles in parallel arrays -services=("uptime_client" "uptime_mongo" "uptime_redis" "uptime_server") +services=("uptime_client" "uptime_mongo" "uptime_server") dockerfiles=( "./docker/dev/client.Dockerfile" "./docker/dev/mongoDB.Dockerfile" - "./docker/dev/redis.Dockerfile" "./docker/dev/server.Dockerfile" ) diff --git a/docker/dev/server.Dockerfile b/docker/dev/server.Dockerfile index d24bff63f..2a6ebcb7c 100755 --- a/docker/dev/server.Dockerfile +++ b/docker/dev/server.Dockerfile @@ -13,6 +13,8 @@ RUN npm install COPY ./server/ ./ +RUN npm run build + EXPOSE 52345 -CMD ["node", "src/index.js"] \ No newline at end of file +CMD ["node", "dist/index.js"] \ No newline at end of file diff --git a/docker/dist-arm/server.Dockerfile b/docker/dist-arm/server.Dockerfile index e47db9a74..91ccd427e 100644 --- a/docker/dist-arm/server.Dockerfile +++ b/docker/dist-arm/server.Dockerfile @@ -35,10 +35,12 @@ RUN npm install COPY server ./ -COPY --from=frontend-build /app/client/dist ./public - RUN chmod +x ./scripts/inject-vars.sh +RUN npm run build + +COPY --from=frontend-build /app/client/dist ./public + EXPOSE 52345 -CMD ["sh", "-c", "./scripts/inject-vars.sh && node ./src/index.js"] +CMD ["sh", "-c", "./scripts/inject-vars.sh && node ./dist/index.js"] diff --git a/docker/dist-mono/docker-compose-local.yaml b/docker/dist-mono/docker-compose-local.yaml new file mode 100755 index 000000000..a5616695b --- /dev/null +++ b/docker/dist-mono/docker-compose-local.yaml @@ -0,0 +1,27 @@ +services: + server: + image: mono_server + restart: always + ports: + - "52345:52345" + environment: + - UPTIME_APP_API_BASE_URL=http://localhost:52345/api/v1 + - UPTIME_APP_CLIENT_HOST=http://localhost + - DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db + - CLIENT_HOST=http://localhost + - JWT_SECRET=my_secret + depends_on: + - mongodb + mongodb: + image: mono_mongo + restart: always + command: ["mongod", "--quiet", "--bind_ip_all"] + volumes: + - ./mongo/data:/data/db + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet"] + interval: 5s + timeout: 30s + start_period: 0s + start_interval: 1s + retries: 30 diff --git a/docker/dist-mono/server.Dockerfile b/docker/dist-mono/server.Dockerfile index 405f9e6ac..84b7127ed 100644 --- a/docker/dist-mono/server.Dockerfile +++ b/docker/dist-mono/server.Dockerfile @@ -19,12 +19,15 @@ WORKDIR /app/server COPY server ./ -COPY --from=frontend-build /app/client/dist ./public RUN npm install +RUN npm run build + +COPY --from=frontend-build /app/client/dist ./public + RUN chmod +x ./scripts/inject-vars.sh EXPOSE 52345 -CMD ./scripts/inject-vars.sh && node ./src/index.js +CMD ./scripts/inject-vars.sh && node ./dist/index.js diff --git a/docker/dist/server.Dockerfile b/docker/dist/server.Dockerfile index d24bff63f..2a6ebcb7c 100755 --- a/docker/dist/server.Dockerfile +++ b/docker/dist/server.Dockerfile @@ -13,6 +13,8 @@ RUN npm install COPY ./server/ ./ +RUN npm run build + EXPOSE 52345 -CMD ["node", "src/index.js"] \ No newline at end of file +CMD ["node", "dist/index.js"] \ No newline at end of file diff --git a/docker/prod/server.Dockerfile b/docker/prod/server.Dockerfile index b78de2e33..9d9800e7b 100755 --- a/docker/prod/server.Dockerfile +++ b/docker/prod/server.Dockerfile @@ -15,6 +15,8 @@ RUN npm install COPY ./server ./ +RUN npm run build + EXPOSE 52345 CMD ["node", "src/index.js"] \ No newline at end of file diff --git a/docker/staging/server.Dockerfile b/docker/staging/server.Dockerfile index b78de2e33..7341b1e9b 100755 --- a/docker/staging/server.Dockerfile +++ b/docker/staging/server.Dockerfile @@ -15,6 +15,8 @@ RUN npm install COPY ./server ./ +RUN npm run build + EXPOSE 52345 -CMD ["node", "src/index.js"] \ No newline at end of file +CMD ["node", "dist/index.js"] \ No newline at end of file From 11ff99613ae3f0f95143ca2ce64d1712d1758c85 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 22 Sep 2025 15:56:41 -0700 Subject: [PATCH 005/400] implement auth routes, add user migration --- server/nodemon.json | 4 +- server/package-lock.json | 117 +++++++++++++++ server/package.json | 4 +- server/src/config/controllers.js | 6 + server/src/config/routes.js | 10 ++ server/src/config/services.js | 15 +- server/src/controllers/v2/AuthController.ts | 142 ++++++++++++++++++ server/src/db/models/InviteToken.js | 35 ----- server/src/db/models/auth/Invite.ts | 68 +++++++++ server/src/db/models/auth/Role.ts | 45 ++++++ .../src/db/models/{User.js => auth/User.ts} | 82 ++++++++-- server/src/db/models/index.ts | 7 + .../db/mongo/migration/0002_migrateUsers.ts | 49 ++++++ server/src/db/mongo/migration/index.js | 2 + server/src/db/mongo/modules/inviteModule.js | 12 +- server/src/middleware/v2/VerifyToken.ts | 21 +++ server/src/middleware/verifyTeamAccess.js | 2 + server/src/routes/v2/auth.ts | 31 ++++ server/src/service/v2/business/AuthService.ts | 141 +++++++++++++++++ .../src/service/v2/business/InviteService.ts | 61 ++++++++ server/src/utils/ApiError.ts | 15 ++ server/src/utils/JWTUtils.ts | 22 +++ 22 files changed, 828 insertions(+), 63 deletions(-) create mode 100644 server/src/controllers/v2/AuthController.ts delete mode 100755 server/src/db/models/InviteToken.js create mode 100755 server/src/db/models/auth/Invite.ts create mode 100644 server/src/db/models/auth/Role.ts rename server/src/db/models/{User.js => auth/User.ts} (55%) create mode 100644 server/src/db/models/index.ts create mode 100644 server/src/db/mongo/migration/0002_migrateUsers.ts create mode 100644 server/src/middleware/v2/VerifyToken.ts create mode 100644 server/src/routes/v2/auth.ts create mode 100644 server/src/service/v2/business/AuthService.ts create mode 100644 server/src/service/v2/business/InviteService.ts create mode 100644 server/src/utils/ApiError.ts create mode 100644 server/src/utils/JWTUtils.ts diff --git a/server/nodemon.json b/server/nodemon.json index 88d6ac686..7371b4904 100755 --- a/server/nodemon.json +++ b/server/nodemon.json @@ -1,5 +1,5 @@ { "ignore": ["src/locales/*", "*.log", "node_modules/*"], - "watch": ["src/**/*.js", "*.json"], - "ext": "js,json" + "watch": ["src/**/*.js", "src/**/*.ts", "*.json"], + "ext": "ts,js,json" } diff --git a/server/package-lock.json b/server/package-lock.json index 06c1c30b3..e21b28382 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -45,6 +45,8 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/express": "5.0.3", + "@types/jsonwebtoken": "9.0.10", "c8": "10.1.3", "chai": "5.2.0", "eslint": "^9.17.0", @@ -1678,6 +1680,27 @@ "node": ">=10.13.0" } }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1685,12 +1708,44 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/express": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", + "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", "license": "MIT" }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -1705,6 +1760,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.3.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", @@ -1714,12 +1794,49 @@ "undici-types": "~7.10.0" } }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/relateurl": { "version": "0.2.33", "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.33.tgz", "integrity": "sha512-bTQCKsVbIdzLqZhLkF5fcJQreE4y1ro4DIyVrlDNSCJRRwHhB8Z+4zXXa8jN6eDvc2HbRsEYgbvrnGvi54EpSw==", "license": "MIT" }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", diff --git a/server/package.json b/server/package.json index 637c785cc..5c655af9d 100755 --- a/server/package.json +++ b/server/package.json @@ -2,7 +2,7 @@ "name": "server", "version": "1.0.0", "description": "", - "main": "index.js", + "main": "dist/index.js", "type": "module", "scripts": { "test": "c8 mocha", @@ -54,6 +54,8 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/express": "5.0.3", + "@types/jsonwebtoken": "9.0.10", "c8": "10.1.3", "chai": "5.2.0", "eslint": "^9.17.0", diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js index 5c54579eb..250d04c4b 100644 --- a/server/src/config/controllers.js +++ b/server/src/config/controllers.js @@ -15,6 +15,9 @@ import StatusPageController from "../controllers/v1/statusPageController.js"; import NotificationController from "../controllers/v1/notificationController.js"; import DiagnosticController from "../controllers/v1/diagnosticController.js"; +// V2 +import AuthV2Controller from "../controllers/v2/AuthController.js"; + export const initializeControllers = (services) => { const controllers = {}; const commonDependencies = createCommonDependencies(services.db, services.errorService, services.logger, services.stringService); @@ -62,5 +65,8 @@ export const initializeControllers = (services) => { diagnosticService: services.diagnosticService, }); + // V2 + controllers.authV2Controller = new AuthV2Controller(services.authV2Service, services.inviteV2Service); + return controllers; }; diff --git a/server/src/config/routes.js b/server/src/config/routes.js index 2ec93a2dc..5ef622f37 100644 --- a/server/src/config/routes.js +++ b/server/src/config/routes.js @@ -13,6 +13,8 @@ import LogRoutes from "../routes/v1/logRoutes.js"; import DiagnosticRoutes from "../routes/v1//diagnosticRoute.js"; import NotificationRoutes from "../routes/v1/notificationRoute.js"; +import authV2 from "../routes/v2/auth.js"; + export const setupRoutes = (app, controllers) => { const authRoutes = new AuthRoutes(controllers.authController); const monitorRoutes = new MonitorRoutes(controllers.monitorController); @@ -37,4 +39,12 @@ export const setupRoutes = (app, controllers) => { app.use("/api/v1/status-page", statusPageRoutes.getRouter()); app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter()); app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter()); + + // ******************* + // V2 Routes + // ******************* + + const v2AuthRoutes = new authV2(controllers.authV2Controller); + + app.use("/api/v2/auth", authApiLimiter, v2AuthRoutes.getRouter()); }; diff --git a/server/src/config/services.js b/server/src/config/services.js index 08c2525f3..b2b4ac430 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -48,8 +48,8 @@ import { ParseBoolean } from "../utils/utils.js"; // Models import Check from "../db/models/Check.js"; import Monitor from "../db/models/Monitor.js"; -import User from "../db/models/User.js"; -import InviteToken from "../db/models/InviteToken.js"; +import { User } from "../db/models/auth/User.js"; +import { Invite } from "../db/models/auth/Invite.js"; import StatusPage from "../db/models/StatusPage.js"; import Team from "../db/models/Team.js"; import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; @@ -68,6 +68,10 @@ import NotificationModule from "../db/mongo/modules/notificationModule.js"; import RecoveryModule from "../db/mongo/modules/recoveryModule.js"; import SettingsModule from "../db/mongo/modules/settingsModule.js"; +// v2 +import AuthV2Service from "../service/v2/business/AuthService.js"; +import InviteV2Service from "../service/v2/business/InviteService.js"; + export const initializeServices = async ({ logger, envSettings, settingsService }) => { const serviceRegistry = new ServiceRegistry({ logger }); ServiceRegistry.instance = serviceRegistry; @@ -79,7 +83,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService // Create DB const checkModule = new CheckModule({ logger, Check, Monitor, User }); - const inviteModule = new InviteModule({ InviteToken, crypto, stringService }); + const inviteModule = new InviteModule({ Invite, crypto, stringService }); const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService }); const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); @@ -208,6 +212,9 @@ export const initializeServices = async ({ logger, envSettings, settingsService games, }); + const authV2Service = new AuthV2Service(); + const inviteV2Service = new InviteV2Service(); + const services = { settingsService, translationService, @@ -227,6 +234,8 @@ export const initializeServices = async ({ logger, envSettings, settingsService monitorService, errorService, logger, + authV2Service, + inviteV2Service, }; Object.values(services).forEach((service) => { diff --git a/server/src/controllers/v2/AuthController.ts b/server/src/controllers/v2/AuthController.ts new file mode 100644 index 000000000..a9d1dd203 --- /dev/null +++ b/server/src/controllers/v2/AuthController.ts @@ -0,0 +1,142 @@ +import { Request, Response, NextFunction } from "express"; +import { encode, decode } from "../../utils/JWTUtils.js"; +import AuthService from "../../service/v2/business/AuthService.js"; +import ApiError from "../../utils/ApiError.js"; +import InviteService from "../../service/v2/business/InviteService.js"; +import { IInvite } from "../../db/models/index.js"; + +export interface IAuthController { + register(req: Request, res: Response, next: NextFunction): Promise; + registerWithInvite(req: Request, res: Response, next: NextFunction): Promise; + login(req: Request, res: Response, next: NextFunction): Promise; + logout(req: Request, res: Response): void; + me(req: Request, res: Response, next: NextFunction): void; +} + +class AuthController implements IAuthController { + private authService: AuthService; + private inviteService: InviteService; + constructor(authService: AuthService, inviteService: InviteService) { + this.authService = authService; + this.inviteService = inviteService; + } + + register = async (req: Request, res: Response, next: NextFunction) => { + try { + const { email, firstName, lastName, password } = req.body; + + if (!email || !firstName || !lastName || !password) { + throw new Error("Email, firstName, lastName, and password are required"); + } + + const result = await this.authService.register({ + email, + firstName, + lastName, + password, + }); + + const token = encode(result); + + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week + }); + + res.status(201).json({ + message: "User created successfully", + }); + } catch (error) { + next(error); + } + }; + + registerWithInvite = async (req: Request, res: Response, next: NextFunction) => { + try { + const token = req.params.token; + if (!token) { + throw new ApiError("Invite token is required", 400); + } + + const invite: IInvite = await this.inviteService.get(token); + + const { firstName, lastName, password } = req.body; + const email = invite?.email; + const roles = invite?.roles; + + if (!email || !firstName || !lastName || !password || !roles || roles.length === 0) { + throw new Error("Email, firstName, lastName, password, and roles are required"); + } + + const result = await this.authService.registerWithInvite({ + email, + firstName, + lastName, + password, + roles, + }); + + if (!result) { + throw new Error("Registration failed"); + } + + await this.inviteService.delete(invite._id.toString()); + + const jwt = encode(result); + + res.cookie("token", jwt, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week + }); + + res.status(201).json({ message: "User created successfully" }); + } catch (error) { + next(error); + } + }; + + login = async (req: Request, res: Response, next: NextFunction) => { + try { + const { email, password } = req.body; + // Validation + if (!email || !password) { + throw new ApiError("Email and password are required", 400); + } + const result = await this.authService.login({ email, password }); + + const token = encode(result); + + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week + }); + + res.status(200).json({ + message: "Login successful", + }); + } catch (error) { + next(error); + } + }; + + logout = (req: Request, res: Response) => { + res.clearCookie("token", { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + }); + res.status(200).json({ message: "Logout successful" }); + }; + + me = (req: Request, res: Response, next: NextFunction) => { + return res.status(200).json({ message: "OK" }); + }; +} + +export default AuthController; diff --git a/server/src/db/models/InviteToken.js b/server/src/db/models/InviteToken.js deleted file mode 100755 index 079600647..000000000 --- a/server/src/db/models/InviteToken.js +++ /dev/null @@ -1,35 +0,0 @@ -import mongoose from "mongoose"; -const InviteTokenSchema = mongoose.Schema( - { - email: { - type: String, - required: true, - unique: true, - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - required: true, - }, - role: { - type: Array, - required: true, - default: ["user"], - }, - token: { - type: String, - required: true, - }, - expiry: { - type: Date, - default: Date.now, - expires: 3600, - }, - }, - { - timestamps: true, - } -); - -export default mongoose.model("InviteToken", InviteTokenSchema); diff --git a/server/src/db/models/auth/Invite.ts b/server/src/db/models/auth/Invite.ts new file mode 100755 index 000000000..148ff65cb --- /dev/null +++ b/server/src/db/models/auth/Invite.ts @@ -0,0 +1,68 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; + +export interface IInvite extends Document { + // v1 + _id: Types.ObjectId; + email: string; + teamId: Types.ObjectId; + role: string[]; + token: string; + expiry: Date; + createdAt: Date; + updatedAt: Date; + + // v2 + tokenHash: string; + roles: Types.ObjectId[]; + createdBy: Types.ObjectId; + updatedBy: Types.ObjectId; +} + +const InviteSchema = new Schema( + { + email: { + type: String, + required: true, + unique: true, + trim: true, + lowercase: true, + }, + teamId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Team", + immutable: true, + required: true, + }, + role: { + type: [String], + required: true, + default: ["user"], + }, + token: { + type: String, + required: true, + }, + expiry: { + type: Date, + default: Date.now, + expires: 3600, + }, + + // v2 + roles: [ + { + type: Schema.Types.ObjectId, + ref: "Role", + required: true, + }, + ], + tokenHash: { type: String, required: true, unique: true }, + createdBy: { type: Schema.Types.ObjectId, ref: "User", required: true }, + updatedBy: { type: Schema.Types.ObjectId, ref: "User", required: true }, + }, + { + timestamps: true, + } +); + +export const Invite = mongoose.model("Invite", InviteSchema); diff --git a/server/src/db/models/auth/Role.ts b/server/src/db/models/auth/Role.ts new file mode 100644 index 000000000..e7ffb2f7d --- /dev/null +++ b/server/src/db/models/auth/Role.ts @@ -0,0 +1,45 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; + +export interface IRole extends Document { + _id: Types.ObjectId; + name: string; + description?: string; + permissions: string[]; + isActive: boolean; + createdAt: Date; + updatedAt: Date; +} + +const roleSchema = new Schema( + { + name: { + type: String, + required: true, + trim: true, + maxlength: 50, + }, + description: { + type: String, + trim: true, + maxlength: 200, + }, + + permissions: [ + { + type: String, + required: true, + }, + ], + isActive: { + type: Boolean, + default: true, + }, + }, + { + timestamps: true, + } +); + +roleSchema.index({ name: 1 }, { unique: true }); + +export const Role = mongoose.model("Role", roleSchema); diff --git a/server/src/db/models/User.js b/server/src/db/models/auth/User.ts similarity index 55% rename from server/src/db/models/User.js rename to server/src/db/models/auth/User.ts index 9368d98b1..82c058ea2 100755 --- a/server/src/db/models/User.js +++ b/server/src/db/models/auth/User.ts @@ -1,11 +1,47 @@ -import mongoose from "mongoose"; -import bcrypt from "bcryptjs"; -import logger from "../../utils/logger.js"; -import Monitor from "./Monitor.js"; -import Team from "./Team.js"; -import Notification from "./Notification.js"; +import mongoose, { Schema, Document, Types } from "mongoose"; -const UserSchema = mongoose.Schema( +import bcrypt from "bcryptjs"; +import logger from "../../../utils/logger.js"; +import Monitor from "../Monitor.js"; +import Team from "../Team.js"; +import Notification from "../Notification.js"; +import { subscribe } from "diagnostics_channel"; + +export const RoleTypes = ["user", "admin", "superadmin", "demo"] as const; +export type RoleType = (typeof RoleTypes)[number]; + +export interface ITokenizedUser { + sub: string; + roles: string[]; +} + +export interface IUser extends Document { + // V1 + _id: Types.ObjectId; + firstName: string; + lastName: string; + email: string; + password: string; + avatarImage: string; + profileImage: { + data: Buffer; + contentType: string; + }; + isActive: boolean; + isVerified: boolean; + role?: RoleType[]; + teamId: Types.ObjectId; + checkTTL: number; + createdAt: Date; + updatedAt: Date; + comparePassword: (submittedPassword: string) => Promise; + // V2 + roles: Types.ObjectId[]; + lastLoginAt?: Date; + version: number; +} + +const UserSchema = new Schema( { firstName: { type: String, @@ -22,7 +58,7 @@ const UserSchema = mongoose.Schema( }, password: { type: String, - required: true, + required: false, }, avatarImage: { type: String, @@ -41,8 +77,8 @@ const UserSchema = mongoose.Schema( }, role: { type: [String], - default: "user", - enum: ["user", "admin", "superadmin", "demo"], + default: ["user"], + enum: RoleTypes, }, teamId: { type: mongoose.Schema.Types.ObjectId, @@ -52,6 +88,22 @@ const UserSchema = mongoose.Schema( checkTTL: { type: Number, }, + + // v2 + roles: [ + { + type: Schema.Types.ObjectId, + ref: "Role", + default: [], + }, + ], + lastLoginAt: { + type: Date, + }, + version: { + type: Number, + default: 1, + }, }, { timestamps: true, @@ -69,7 +121,7 @@ UserSchema.pre("save", function (next) { UserSchema.pre("findOneAndUpdate", function (next) { const update = this.getUpdate(); - if ("password" in update) { + if (update && "password" in update) { const salt = bcrypt.genSaltSync(10); update.password = bcrypt.hashSync(update.password, salt); } @@ -92,15 +144,13 @@ UserSchema.pre("findOneAndDelete", async function (next) { } next(); } catch (error) { - next(error); + next(error as Error); } }); -UserSchema.methods.comparePassword = async function (submittedPassword) { +UserSchema.methods.comparePassword = async function (submittedPassword: string) { const res = await bcrypt.compare(submittedPassword, this.password); return res; }; -const User = mongoose.model("User", UserSchema); - -export default User; +export const User = mongoose.model("User", UserSchema); diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts new file mode 100644 index 000000000..0969eafc6 --- /dev/null +++ b/server/src/db/models/index.ts @@ -0,0 +1,7 @@ +export { User } from "./auth/User.js"; +export type { IUser } from "./auth/User.js"; +export type { ITokenizedUser } from "./auth/User.js"; +export { Role } from "./auth/Role.js"; +export type { IRole } from "./auth/Role.js"; +export type { IInvite } from "./auth/Invite.js"; +export { Invite } from "./auth/Invite.js"; diff --git a/server/src/db/mongo/migration/0002_migrateUsers.ts b/server/src/db/mongo/migration/0002_migrateUsers.ts new file mode 100644 index 000000000..196ac6793 --- /dev/null +++ b/server/src/db/mongo/migration/0002_migrateUsers.ts @@ -0,0 +1,49 @@ +import { User, Role } from "../../models/index.js"; +import { DEFAULT_ROLES } from "../../../service/v2/business/AuthService.js"; +async function migrateUsers() { + try { + // Create roles if they don't exist + const roleCount = await Role.countDocuments(); + let roles; + if (roleCount === 0) { + const rolePromises = DEFAULT_ROLES.map((roleData) => + new Role({ + ...roleData, + }).save() + ); + roles = await Promise.all(rolePromises); + } else { + roles = await Role.find(); + } + + // Migrate users + const users = await User.find({ + $or: [{ version: { $exists: false } }, { version: { $lt: 2 } }], + }); + for (const user of users) { + for (const role of user.role || []) { + if (role === "superadmin") { + const superAdminRole = roles.find((role) => role.name === "SuperAdmin"); + user.roles.push(superAdminRole!._id); + } + if (role === "admin") { + const managerRole = roles.find((role) => role.name === "Manager"); + user.roles.push(managerRole!._id); + } + if (role === "user") { + const memberRole = roles.find((role) => role.name === "Member"); + user.roles.push(memberRole!._id); + } + } + user.version = 2; + await user.save(); + } + + return true; + } catch (err) { + console.error("Migration error:", err); + return false; + } +} + +export { migrateUsers }; diff --git a/server/src/db/mongo/migration/index.js b/server/src/db/mongo/migration/index.js index 4bf56895a..2a1da4148 100644 --- a/server/src/db/mongo/migration/index.js +++ b/server/src/db/mongo/migration/index.js @@ -1,7 +1,9 @@ import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js"; +import { migrateUsers } from "./0002_migrateUsers.js"; const runMigrations = async () => { await migrateStatusWindowThreshold(); + await migrateUsers(); }; export { runMigrations }; diff --git a/server/src/db/mongo/modules/inviteModule.js b/server/src/db/mongo/modules/inviteModule.js index 444e51efd..513524fe5 100755 --- a/server/src/db/mongo/modules/inviteModule.js +++ b/server/src/db/mongo/modules/inviteModule.js @@ -1,17 +1,17 @@ const SERVICE_NAME = "inviteModule"; class InviteModule { - constructor({ InviteToken, crypto, stringService }) { - this.InviteToken = InviteToken; + constructor({ Invite, crypto, stringService }) { + this.Invite = Invite; this.crypto = crypto; this.stringService = stringService; } requestInviteToken = async (userData) => { try { - await this.InviteToken.deleteMany({ email: userData.email }); + await this.Invite.deleteMany({ email: userData.email }); userData.token = this.crypto.randomBytes(32).toString("hex"); - let inviteToken = new this.InviteToken(userData); + let inviteToken = new this.Invite(userData); await inviteToken.save(); return inviteToken; } catch (error) { @@ -23,7 +23,7 @@ class InviteModule { getInviteToken = async (token) => { try { - const invite = await this.InviteToken.findOne({ + const invite = await this.Invite.findOne({ token, }); if (invite === null) { @@ -38,7 +38,7 @@ class InviteModule { }; getInviteTokenAndDelete = async (token) => { try { - const invite = await this.InviteToken.findOneAndDelete({ + const invite = await this.Invite.findOneAndDelete({ token, }); if (invite === null) { diff --git a/server/src/middleware/v2/VerifyToken.ts b/server/src/middleware/v2/VerifyToken.ts new file mode 100644 index 000000000..7dabd51cc --- /dev/null +++ b/server/src/middleware/v2/VerifyToken.ts @@ -0,0 +1,21 @@ +import { Request, Response, NextFunction } from "express"; +import { decode } from "../../utils/JWTUtils.js"; +import ApiError from "../../utils/ApiError.js"; +const verifyToken = (req: Request, res: Response, next: NextFunction) => { + const token = req.cookies.token; + + if (!token) { + const error = new ApiError("No token provided", 401); + return next(error); + } + + try { + const decoded = decode(token); + req.user = decoded; + next(); + } catch (error) { + next(error); + } +}; + +export { verifyToken }; diff --git a/server/src/middleware/verifyTeamAccess.js b/server/src/middleware/verifyTeamAccess.js index 60647bdc2..a548cbf23 100644 --- a/server/src/middleware/verifyTeamAccess.js +++ b/server/src/middleware/verifyTeamAccess.js @@ -3,6 +3,8 @@ const SERVICE_NAME = "verifyTeamAccess"; const verifyTeamAccess = (Model, paramName) => { return async (req, res, next) => { try { + const user = req.user; + console.log(user); const documentId = req.params[paramName]; const doc = await Model.findById(documentId); diff --git a/server/src/routes/v2/auth.ts b/server/src/routes/v2/auth.ts new file mode 100644 index 000000000..5a1b8ad63 --- /dev/null +++ b/server/src/routes/v2/auth.ts @@ -0,0 +1,31 @@ +import { Router } from "express"; + +import express from "express"; +import AuthController from "../../controllers/v2/AuthController.js"; +import { verifyToken } from "../../middleware/v2/VerifyToken.js"; + +const router = express.Router(); + +class AuthRoutes { + private controller: AuthController; + private router: Router; + constructor(authController: AuthController) { + this.controller = authController; + this.router = Router(); + this.initRoutes(); + } + + initRoutes = () => { + this.router.post("/register", this.controller.register); + this.router.post("/register/invite/:token", this.controller.registerWithInvite); + this.router.post("/login", this.controller.login); + this.router.post("/logout", this.controller.logout); + this.router.get("/me", verifyToken, this.controller.me); + }; + + getRouter() { + return this.router; + } +} + +export default AuthRoutes; diff --git a/server/src/service/v2/business/AuthService.ts b/server/src/service/v2/business/AuthService.ts new file mode 100644 index 000000000..ae22b981d --- /dev/null +++ b/server/src/service/v2/business/AuthService.ts @@ -0,0 +1,141 @@ +import bcrypt from "bcryptjs"; +import { User, Role, ITokenizedUser } from "../../../db/models/index.js"; +import ApiError from "../../../utils/ApiError.js"; +import { Types } from "mongoose"; + +export const DEFAULT_ROLES = [ + { + name: "SuperAdmin", + description: "Super admin with all permissions", + permissions: ["*"], + isSystem: true, + }, + { + name: "Admin", + description: "Admin with full permissions", + permissions: ["monitor.*", "users.*"], + isSystem: true, + }, + { + name: "Manager", + description: "Can manage users", + permissions: ["users.create", "users.update", "monitors.*"], + isSystem: true, + }, + { + name: "Member", + description: "Basic team member", + permissions: ["users.update", "monitors.create", "monitors.view", "monitors.update"], + isSystem: true, + }, +]; + +export type RegisterData = { + email: string; + firstName: string; + lastName: string; + password: string; + roles?: Types.ObjectId[]; // Optional roles for invite-based registration +}; + +export type LoginData = { + email: string; + password: string; +}; + +export type AuthResult = ITokenizedUser; + +export interface IAuthService { + register(signupData: RegisterData): Promise; + registerWithInvite(signupData: RegisterData): Promise; + login(loginData: LoginData): Promise; +} + +class AuthService implements IAuthService { + constructor() {} + + async register(signupData: RegisterData): Promise { + const userCount = await User.countDocuments(); + + if (userCount > 0) { + throw new Error("Registration is closed. Please request an invite."); + } + + const { email, firstName, lastName, password } = signupData; + + // Create all default roles + const rolePromises = DEFAULT_ROLES.map((roleData) => + new Role({ + ...roleData, + }).save() + ); + const roles = await Promise.all(rolePromises); + + // Find admin role and assign to first user + const superAdminRole = roles.find((role) => role.name === "SuperAdmin"); + + const user = new User({ + email, + firstName, + lastName, + password, + roles: [superAdminRole!._id], + }); + + const savedUser = await user.save(); + return { + sub: savedUser._id.toString(), + roles: savedUser.roles.map((role) => role.toString()), + }; + } + + async registerWithInvite(signupData: RegisterData): Promise { + const { email, firstName, lastName, password, roles } = signupData; + + const user = new User({ + email, + firstName, + lastName, + password, + roles: roles || [], + }); + try { + const savedUser = await user.save(); + return { + sub: savedUser._id.toString(), + roles: savedUser.roles.map((role) => role.toString()), + }; + } catch (error: any) { + if (error?.code === 11000) { + const dupError = new ApiError("Email already in use", 409); + dupError.stack = error?.stack; + throw dupError; + } + throw error; + } + } + + async login(loginData: LoginData): Promise { + const { email, password } = loginData; + + // Find user by email + const user = await User.findOne({ email }); + + if (!user) { + throw new Error("Invalid email or password"); + } + + // Check password + const isPasswordValid = await bcrypt.compare(password, user.passwordHash); + if (!isPasswordValid) { + throw new Error("Invalid email or password"); + } + + return { + sub: user._id.toString(), + roles: user.roles.map((role) => role.toString()), + }; + } +} + +export default AuthService; diff --git a/server/src/service/v2/business/InviteService.ts b/server/src/service/v2/business/InviteService.ts new file mode 100644 index 000000000..f8bd9e2db --- /dev/null +++ b/server/src/service/v2/business/InviteService.ts @@ -0,0 +1,61 @@ +import crypto from "node:crypto"; +import { ITokenizedUser, IInvite, Invite } from "../../../db/models/index.js"; +import ApiError from "../../../utils/ApiError.js"; + +export interface IInviteService { + create: (tokenizedUser: ITokenizedUser, invite: IInvite) => Promise<{ token: string }>; + getAll: () => Promise; + get: (tokenHash: string) => Promise; + delete: (id: string) => Promise; +} + +class InviteService implements IInviteService { + constructor() {} + + create = async (tokenizedUser: ITokenizedUser, inviteData: IInvite) => { + const token = crypto.randomBytes(32).toString("hex"); + const tokenHash = crypto.createHash("sha256").update(token).digest("hex"); + try { + const invite = await Invite.create({ + ...inviteData, + tokenHash, + createdBy: tokenizedUser.sub, + updatedBy: tokenizedUser.sub, + }); + if (!invite) { + throw new ApiError("Failed to create invite", 500); + } + return { token }; + } catch (error: any) { + if (error?.code === 11000) { + const dupError = new ApiError("Invite with this email already exists", 409); + dupError.stack = error?.stack; + throw dupError; + } + throw error; + } + }; + + get = async (token: string) => { + const tokenHash = crypto.createHash("sha256").update(token).digest("hex"); + const invite = await Invite.findOne({ tokenHash }); + if (!invite) { + throw new ApiError("Invite not found", 404); + } + return invite; + }; + + getAll = async () => { + return Invite.find(); + }; + + delete = async (id: string) => { + const result = await Invite.deleteOne({ _id: id }); + if (!result.deletedCount) { + throw new ApiError("Invite not found", 404); + } + return result.deletedCount === 1; + }; +} + +export default InviteService; diff --git a/server/src/utils/ApiError.ts b/server/src/utils/ApiError.ts new file mode 100644 index 000000000..313d01c2a --- /dev/null +++ b/server/src/utils/ApiError.ts @@ -0,0 +1,15 @@ +class ApiError extends Error { + public status: number; + + constructor(message: string, status: number = 500) { + super(message); + this.status = status; + this.name = this.constructor.name; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + } +} + +export default ApiError; diff --git a/server/src/utils/JWTUtils.ts b/server/src/utils/JWTUtils.ts new file mode 100644 index 000000000..4eebb78a0 --- /dev/null +++ b/server/src/utils/JWTUtils.ts @@ -0,0 +1,22 @@ +import jwt from "jsonwebtoken"; +import { AuthResult } from "../service/v2/business/AuthService.js"; + +const encode = (data: AuthResult): string => { + const secret = process.env.JWT_SECRET; + if (!secret) { + throw new Error("JWT_SECRET is not defined"); + } + const token = jwt.sign(data, secret, { expiresIn: "99d" }); + return token; +}; + +const decode = (token: string): AuthResult => { + const secret = process.env.JWT_SECRET; + if (!secret) { + throw new Error("JWT_SECRET is not defined"); + } + const decoded = jwt.verify(token, secret) as AuthResult; + return decoded; +}; + +export { encode, decode }; From 003039400896c85eb4af8c2210de4b4e40b5ed78 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 23 Sep 2025 11:34:25 -0700 Subject: [PATCH 006/400] add migrations table --- server/src/db/models/index.ts | 2 ++ server/src/db/models/migration/Migration.ts | 34 +++++++++++++++++++ ...s => 0001_migrateStatusWindowThreshold.ts} | 1 + .../db/mongo/migration/0002_migrateUsers.ts | 13 +++++-- server/src/db/mongo/migration/index.js | 9 ----- server/src/db/mongo/migration/index.ts | 25 ++++++++++++++ 6 files changed, 72 insertions(+), 12 deletions(-) create mode 100644 server/src/db/models/migration/Migration.ts rename server/src/db/mongo/migration/{0001_migrateStatusWindowThreshold.js => 0001_migrateStatusWindowThreshold.ts} (90%) delete mode 100644 server/src/db/mongo/migration/index.js create mode 100644 server/src/db/mongo/migration/index.ts diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index 0969eafc6..537633bfd 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -5,3 +5,5 @@ export { Role } from "./auth/Role.js"; export type { IRole } from "./auth/Role.js"; export type { IInvite } from "./auth/Invite.js"; export { Invite } from "./auth/Invite.js"; +export { Migration } from "./migration/Migration.js"; +export type { IMigration } from "./migration/Migration.js"; diff --git a/server/src/db/models/migration/Migration.ts b/server/src/db/models/migration/Migration.ts new file mode 100644 index 000000000..cf76bb48e --- /dev/null +++ b/server/src/db/models/migration/Migration.ts @@ -0,0 +1,34 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; + +export interface IMigration extends Document { + _id: Types.ObjectId; + name: string; + runAt: Date; + success: boolean; + createdAt: Date; + updatedAt: Date; +} + +const migrationSchema = new Schema( + { + name: { + type: String, + required: true, + }, + runAt: { + type: Date, + required: false, + }, + success: { + type: Boolean, + required: false, + }, + }, + { + timestamps: true, + } +); + +migrationSchema.index({ name: 1 }, { unique: true }); + +export const Migration = mongoose.model("Migration", migrationSchema); diff --git a/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.js b/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.ts similarity index 90% rename from server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.js rename to server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.ts index 8f6988de4..b44487e87 100644 --- a/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.js +++ b/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.ts @@ -1,4 +1,5 @@ import Monitor from "../../models/Monitor.js"; +export const MIGRATION_NAME = "0001_migrateStatusWindowThreshold"; async function migrateStatusWindowThreshold() { try { const monitors = await Monitor.find({ statusWindowThreshold: { $lt: 1 } }); diff --git a/server/src/db/mongo/migration/0002_migrateUsers.ts b/server/src/db/mongo/migration/0002_migrateUsers.ts index 196ac6793..a7cdb76fc 100644 --- a/server/src/db/mongo/migration/0002_migrateUsers.ts +++ b/server/src/db/mongo/migration/0002_migrateUsers.ts @@ -1,5 +1,8 @@ import { User, Role } from "../../models/index.js"; +import { Types } from "mongoose"; + import { DEFAULT_ROLES } from "../../../service/v2/business/AuthService.js"; +export const MIGRATION_NAME = "0002_migrateUsers"; async function migrateUsers() { try { // Create roles if they don't exist @@ -21,20 +24,24 @@ async function migrateUsers() { $or: [{ version: { $exists: false } }, { version: { $lt: 2 } }], }); for (const user of users) { + const newRoleIds: Types.ObjectId[] = []; + for (const role of user.role || []) { if (role === "superadmin") { const superAdminRole = roles.find((role) => role.name === "SuperAdmin"); - user.roles.push(superAdminRole!._id); + newRoleIds.push(superAdminRole!._id); } if (role === "admin") { const managerRole = roles.find((role) => role.name === "Manager"); - user.roles.push(managerRole!._id); + newRoleIds.push(managerRole!._id); } if (role === "user") { const memberRole = roles.find((role) => role.name === "Member"); - user.roles.push(memberRole!._id); + newRoleIds.push(memberRole!._id); } } + const merged = [...new Set([...user.roles, ...newRoleIds])]; + user.roles = merged; user.version = 2; await user.save(); } diff --git a/server/src/db/mongo/migration/index.js b/server/src/db/mongo/migration/index.js deleted file mode 100644 index 2a1da4148..000000000 --- a/server/src/db/mongo/migration/index.js +++ /dev/null @@ -1,9 +0,0 @@ -import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js"; -import { migrateUsers } from "./0002_migrateUsers.js"; - -const runMigrations = async () => { - await migrateStatusWindowThreshold(); - await migrateUsers(); -}; - -export { runMigrations }; diff --git a/server/src/db/mongo/migration/index.ts b/server/src/db/mongo/migration/index.ts new file mode 100644 index 000000000..293e0905f --- /dev/null +++ b/server/src/db/mongo/migration/index.ts @@ -0,0 +1,25 @@ +import { migrateStatusWindowThreshold, MIGRATION_NAME as MIGRATION_0001 } from "./0001_migrateStatusWindowThreshold.js"; +import { migrateUsers, MIGRATION_NAME as MIGRATION_0002 } from "./0002_migrateUsers.js"; +import { Migration, IMigration } from "../../models/index.js"; + +const runMigrations = async () => { + const migrations = [ + { name: MIGRATION_0001, migration: migrateStatusWindowThreshold }, + { name: MIGRATION_0002, migration: migrateUsers }, + ]; + + const migrationNames = migrations.map((m) => m.name); + + const appliedMigrations = await Migration.find({ name: { $in: migrationNames }, success: true }); + + const migrationsToRun = migrations.filter((m) => !appliedMigrations.some((am: IMigration) => am.name === m.name)); + + for (const { name, migration } of migrationsToRun) { + console.log(`Running migration: ${name}`); + const success = await migration(); + await Migration.updateOne({ name }, { $set: { runAt: new Date(), success } }, { upsert: true }); + console.log(`Migration ${name} completed with status: ${success ? "success" : "failure"}`); + } +}; + +export { runMigrations }; From f945a2701151f9b9cd7337f43550973e345af82d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 23 Sep 2025 11:59:02 -0700 Subject: [PATCH 007/400] move v1 to v1 dir, add build workflow --- .github/workflows/build-check.yml | 23 +++++++++ server/src/app.js | 10 ++-- server/src/config/routes.js | 4 +- server/src/config/services.js | 36 +++++++------- server/src/controllers/v1/baseController.js | 2 +- server/src/index.js | 2 +- .../src/middleware/{ => v1}/handleErrors.js | 6 +-- server/src/middleware/{ => v1}/isAllowed.js | 6 +-- .../middleware/{ => v1}/languageMiddleware.js | 2 +- server/src/middleware/{ => v1}/rateLimiter.js | 0 .../middleware/{ => v1}/responseHandler.js | 0 .../src/middleware/{ => v1}/sanitization.js | 0 server/src/middleware/{ => v1}/verifyJWT.js | 6 +-- .../middleware/{ => v1}/verifyOwnership.js | 6 +-- .../middleware/{ => v1}/verifySuperAdmin.js | 2 +- .../middleware/{ => v1}/verifyTeamAccess.js | 0 server/src/routes/v1/authRoute.js | 4 +- server/src/routes/v1/checkRoute.js | 2 +- server/src/routes/v1/diagnosticRoute.js | 4 +- server/src/routes/v1/inviteRoute.js | 4 +- server/src/routes/v1/logRoutes.js | 2 +- server/src/routes/v1/monitorRoute.js | 2 +- server/src/routes/v1/queueRoute.js | 2 +- server/src/routes/v1/settingsRoute.js | 2 +- server/src/routes/v1/statusPageRoute.js | 2 +- .../service/{ => v1}/business/checkService.js | 0 .../{ => v1}/business/diagnosticService.js | 0 .../{ => v1}/business/inviteService.js | 0 .../business/maintenanceWindowService.js | 0 .../{ => v1}/business/monitorService.js | 2 +- .../service/{ => v1}/business/userService.js | 0 .../SuperSimpleQueue/SuperSimpleQueue.js | 0 .../SuperSimpleQueueHelper.js | 10 ++-- .../{ => v1}/infrastructure/bufferService.js | 0 .../{ => v1}/infrastructure/emailService.js | 0 .../{ => v1}/infrastructure/errorService.js | 0 .../{ => v1}/infrastructure/networkService.js | 0 .../infrastructure/notificationService.js | 0 .../infrastructure/notificationUtils.js | 0 .../{ => v1}/infrastructure/statusService.js | 2 +- .../{ => v1}/system/serviceRegistry.js | 0 .../{ => v1}/system/settingsService.js | 0 .../service/{ => v1}/system/stringService.js | 0 .../{ => v1}/system/translationService.js | 0 server/src/service/v2/business/AuthService.ts | 47 +++++++++++++++++-- server/src/types/express.d.ts | 10 ++++ 46 files changed, 136 insertions(+), 64 deletions(-) create mode 100644 .github/workflows/build-check.yml rename server/src/middleware/{ => v1}/handleErrors.js (71%) rename server/src/middleware/{ => v1}/isAllowed.js (87%) rename server/src/middleware/{ => v1}/languageMiddleware.js (93%) rename server/src/middleware/{ => v1}/rateLimiter.js (100%) rename server/src/middleware/{ => v1}/responseHandler.js (100%) rename server/src/middleware/{ => v1}/sanitization.js (100%) rename server/src/middleware/{ => v1}/verifyJWT.js (87%) rename server/src/middleware/{ => v1}/verifyOwnership.js (88%) rename server/src/middleware/{ => v1}/verifySuperAdmin.js (97%) rename server/src/middleware/{ => v1}/verifyTeamAccess.js (100%) rename server/src/service/{ => v1}/business/checkService.js (100%) rename server/src/service/{ => v1}/business/diagnosticService.js (100%) rename server/src/service/{ => v1}/business/inviteService.js (100%) rename server/src/service/{ => v1}/business/maintenanceWindowService.js (100%) rename server/src/service/{ => v1}/business/monitorService.js (99%) rename server/src/service/{ => v1}/business/userService.js (100%) rename server/src/service/{ => v1}/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js (100%) rename server/src/service/{ => v1}/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js (90%) rename server/src/service/{ => v1}/infrastructure/bufferService.js (100%) rename server/src/service/{ => v1}/infrastructure/emailService.js (100%) rename server/src/service/{ => v1}/infrastructure/errorService.js (100%) rename server/src/service/{ => v1}/infrastructure/networkService.js (100%) rename server/src/service/{ => v1}/infrastructure/notificationService.js (100%) rename server/src/service/{ => v1}/infrastructure/notificationUtils.js (100%) rename server/src/service/{ => v1}/infrastructure/statusService.js (99%) rename server/src/service/{ => v1}/system/serviceRegistry.js (100%) rename server/src/service/{ => v1}/system/settingsService.js (100%) rename server/src/service/{ => v1}/system/stringService.js (100%) rename server/src/service/{ => v1}/system/translationService.js (100%) create mode 100644 server/src/types/express.d.ts diff --git a/.github/workflows/build-check.yml b/.github/workflows/build-check.yml new file mode 100644 index 000000000..206a652d5 --- /dev/null +++ b/.github/workflows/build-check.yml @@ -0,0 +1,23 @@ +name: Build Check (Server) +on: + pull_request: + workflow_dispatch: + +jobs: + build-server: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install server dependencies + working-directory: server + run: npm install + + - name: Check server formatting + working-directory: server + run: npm run build diff --git a/server/src/app.js b/server/src/app.js index 9cabda6b6..253baee88 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -1,16 +1,16 @@ import express from "express"; import path from "path"; -import { responseHandler } from "./middleware/responseHandler.js"; +import { responseHandler } from "./middleware/v1/responseHandler.js"; import cors from "cors"; import helmet from "helmet"; import compression from "compression"; import cookieParser from "cookie-parser"; -import languageMiddleware from "./middleware/languageMiddleware.js"; +import languageMiddleware from "./middleware/v1/languageMiddleware.js"; import swaggerUi from "swagger-ui-express"; -import { handleErrors } from "./middleware/handleErrors.js"; +import { handleErrors } from "./middleware/v1/handleErrors.js"; import { setupRoutes } from "./config/routes.js"; -import { generalApiLimiter } from "./middleware/rateLimiter.js"; -import { sanitizeBody, sanitizeQuery } from "./middleware/sanitization.js"; +import { generalApiLimiter } from "./middleware/v1/rateLimiter.js"; +import { sanitizeBody, sanitizeQuery } from "./middleware/v1/sanitization.js"; export const createApp = ({ services, controllers, envSettings, frontendPath, openApiSpec }) => { const allowedOrigin = envSettings.clientHost; diff --git a/server/src/config/routes.js b/server/src/config/routes.js index 5ef622f37..2068cf021 100644 --- a/server/src/config/routes.js +++ b/server/src/config/routes.js @@ -1,5 +1,5 @@ -import { verifyJWT } from "../middleware/verifyJWT.js"; -import { authApiLimiter } from "../middleware/rateLimiter.js"; +import { verifyJWT } from "../middleware/v1/verifyJWT.js"; +import { authApiLimiter } from "../middleware/v1/rateLimiter.js"; import AuthRoutes from "../routes/v1/authRoute.js"; import InviteRoutes from "../routes/v1//inviteRoute.js"; diff --git a/server/src/config/services.js b/server/src/config/services.js index b2b4ac430..c5ab7fa02 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -1,22 +1,22 @@ -import ServiceRegistry from "../service/system/serviceRegistry.js"; -import TranslationService from "../service/system/translationService.js"; -import StringService from "../service/system/stringService.js"; +import ServiceRegistry from "../service/v1/system/serviceRegistry.js"; +import TranslationService from "../service/v1/system/translationService.js"; +import StringService from "../service/v1/system/stringService.js"; import MongoDB from "../db/mongo/MongoDB.js"; -import NetworkService from "../service/infrastructure/networkService.js"; -import EmailService from "../service/infrastructure/emailService.js"; -import BufferService from "../service/infrastructure/bufferService.js"; -import StatusService from "../service/infrastructure/statusService.js"; -import NotificationUtils from "../service/infrastructure/notificationUtils.js"; -import NotificationService from "../service/infrastructure/notificationService.js"; -import ErrorService from "../service/infrastructure/errorService.js"; -import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; -import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; -import UserService from "../service/business/userService.js"; -import CheckService from "../service/business/checkService.js"; -import DiagnosticService from "../service/business/diagnosticService.js"; -import InviteService from "../service/business/inviteService.js"; -import MaintenanceWindowService from "../service/business/maintenanceWindowService.js"; -import MonitorService from "../service/business/monitorService.js"; +import NetworkService from "../service/v1/infrastructure/networkService.js"; +import EmailService from "../service/v1/infrastructure/emailService.js"; +import BufferService from "../service/v1/infrastructure/bufferService.js"; +import StatusService from "../service/v1/infrastructure/statusService.js"; +import NotificationUtils from "../service/v1/infrastructure/notificationUtils.js"; +import NotificationService from "../service/v1/infrastructure/notificationService.js"; +import ErrorService from "../service/v1/infrastructure/errorService.js"; +import SuperSimpleQueueHelper from "../service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; +import SuperSimpleQueue from "../service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; +import UserService from "../service/v1/business/userService.js"; +import CheckService from "../service/v1/business/checkService.js"; +import DiagnosticService from "../service/v1/business/diagnosticService.js"; +import InviteService from "../service/v1/business/inviteService.js"; +import MaintenanceWindowService from "../service/v1/business/maintenanceWindowService.js"; +import MonitorService from "../service/v1/business/monitorService.js"; import papaparse from "papaparse"; import axios from "axios"; import got from "got"; diff --git a/server/src/controllers/v1/baseController.js b/server/src/controllers/v1/baseController.js index 314905c82..e8612cec5 100644 --- a/server/src/controllers/v1/baseController.js +++ b/server/src/controllers/v1/baseController.js @@ -1,4 +1,4 @@ -import { AppError } from "../../service/infrastructure/errorService.js"; +import { AppError } from "../../service/v1/infrastructure/errorService.js"; export const createCommonDependencies = (db, errorService, logger, stringService) => { return { diff --git a/server/src/index.js b/server/src/index.js index d1ff649eb..a4e477d5f 100755 --- a/server/src/index.js +++ b/server/src/index.js @@ -7,7 +7,7 @@ import path from "path"; import fs from "fs"; import Logger from "./utils/logger.js"; -import SettingsService from "./service/system/settingsService.js"; +import SettingsService from "./service/v1/system/settingsService.js"; import AppSettings from "./db/models/AppSettings.js"; const SERVICE_NAME = "Server"; diff --git a/server/src/middleware/handleErrors.js b/server/src/middleware/v1/handleErrors.js similarity index 71% rename from server/src/middleware/handleErrors.js rename to server/src/middleware/v1/handleErrors.js index b0ded5ab7..43671fbe2 100755 --- a/server/src/middleware/handleErrors.js +++ b/server/src/middleware/v1/handleErrors.js @@ -1,6 +1,6 @@ -import { logger } from "../utils/logger.js"; -import ServiceRegistry from "../service/system/serviceRegistry.js"; -import StringService from "../service/system/stringService.js"; +import { logger } from "../../utils/logger.js"; +import ServiceRegistry from "../../service/v1/system/serviceRegistry.js"; +import StringService from "../../service/v1/system/stringService.js"; const handleErrors = (error, req, res, next) => { const status = error.status || 500; diff --git a/server/src/middleware/isAllowed.js b/server/src/middleware/v1/isAllowed.js similarity index 87% rename from server/src/middleware/isAllowed.js rename to server/src/middleware/v1/isAllowed.js index 5d61974be..cc240f531 100755 --- a/server/src/middleware/isAllowed.js +++ b/server/src/middleware/v1/isAllowed.js @@ -1,9 +1,9 @@ import jwt from "jsonwebtoken"; const TOKEN_PREFIX = "Bearer "; const SERVICE_NAME = "allowedRoles"; -import ServiceRegistry from "../service/system/serviceRegistry.js"; -import StringService from "../service/system/stringService.js"; -import SettingsService from "../service/system/settingsService.js"; +import ServiceRegistry from "../../service/v1/system/serviceRegistry.js"; +import StringService from "../../service/v1/system/stringService.js"; +import SettingsService from "../../service/v1/system/settingsService.js"; const isAllowed = (allowedRoles) => { return (req, res, next) => { diff --git a/server/src/middleware/languageMiddleware.js b/server/src/middleware/v1/languageMiddleware.js similarity index 93% rename from server/src/middleware/languageMiddleware.js rename to server/src/middleware/v1/languageMiddleware.js index 6293dc837..819858a21 100755 --- a/server/src/middleware/languageMiddleware.js +++ b/server/src/middleware/v1/languageMiddleware.js @@ -1,4 +1,4 @@ -import { logger } from "../utils/logger.js"; +import { logger } from "../../utils/logger.js"; const languageMiddleware = (stringService, translationService) => async (req, res, next) => { try { diff --git a/server/src/middleware/rateLimiter.js b/server/src/middleware/v1/rateLimiter.js similarity index 100% rename from server/src/middleware/rateLimiter.js rename to server/src/middleware/v1/rateLimiter.js diff --git a/server/src/middleware/responseHandler.js b/server/src/middleware/v1/responseHandler.js similarity index 100% rename from server/src/middleware/responseHandler.js rename to server/src/middleware/v1/responseHandler.js diff --git a/server/src/middleware/sanitization.js b/server/src/middleware/v1/sanitization.js similarity index 100% rename from server/src/middleware/sanitization.js rename to server/src/middleware/v1/sanitization.js diff --git a/server/src/middleware/verifyJWT.js b/server/src/middleware/v1/verifyJWT.js similarity index 87% rename from server/src/middleware/verifyJWT.js rename to server/src/middleware/v1/verifyJWT.js index d01db0a97..b57391dfd 100755 --- a/server/src/middleware/verifyJWT.js +++ b/server/src/middleware/v1/verifyJWT.js @@ -1,7 +1,7 @@ import jwt from "jsonwebtoken"; -import ServiceRegistry from "../service/system/serviceRegistry.js"; -import SettingsService from "../service/system/settingsService.js"; -import StringService from "../service/system/stringService.js"; +import ServiceRegistry from "../../service/v1/system/serviceRegistry.js"; +import SettingsService from "../../service/v1/system/settingsService.js"; +import StringService from "../../service/v1/system/stringService.js"; const SERVICE_NAME = "verifyJWT"; const TOKEN_PREFIX = "Bearer "; diff --git a/server/src/middleware/verifyOwnership.js b/server/src/middleware/v1/verifyOwnership.js similarity index 88% rename from server/src/middleware/verifyOwnership.js rename to server/src/middleware/v1/verifyOwnership.js index 4431b0c26..741fb6436 100755 --- a/server/src/middleware/verifyOwnership.js +++ b/server/src/middleware/v1/verifyOwnership.js @@ -1,6 +1,6 @@ -import { logger } from "../utils/logger.js"; -import ServiceRegistry from "../service/system/serviceRegistry.js"; -import StringService from "../service/system/stringService.js"; +import { logger } from "../../utils/logger.js"; +import ServiceRegistry from "../../service/v1/system/serviceRegistry.js"; +import StringService from "../../service/v1/system/stringService.js"; import { ObjectId } from "mongodb"; const SERVICE_NAME = "verifyOwnership"; diff --git a/server/src/middleware/verifySuperAdmin.js b/server/src/middleware/v1/verifySuperAdmin.js similarity index 97% rename from server/src/middleware/verifySuperAdmin.js rename to server/src/middleware/v1/verifySuperAdmin.js index a5a93eede..41ac0a3e0 100755 --- a/server/src/middleware/verifySuperAdmin.js +++ b/server/src/middleware/v1/verifySuperAdmin.js @@ -1,5 +1,5 @@ const jwt = require("jsonwebtoken"); -const logger = require("../utils/logger"); +const logger = require("../../utils/logger.js"); const SERVICE_NAME = "verifyAdmin"; const TOKEN_PREFIX = "Bearer "; import ServiceRegistry from "../service/serviceRegistry.js"; diff --git a/server/src/middleware/verifyTeamAccess.js b/server/src/middleware/v1/verifyTeamAccess.js similarity index 100% rename from server/src/middleware/verifyTeamAccess.js rename to server/src/middleware/v1/verifyTeamAccess.js diff --git a/server/src/routes/v1/authRoute.js b/server/src/routes/v1/authRoute.js index f69f94f43..d9b036b05 100755 --- a/server/src/routes/v1/authRoute.js +++ b/server/src/routes/v1/authRoute.js @@ -1,6 +1,6 @@ import { Router } from "express"; -import { verifyJWT } from "../../middleware/verifyJWT.js"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { verifyJWT } from "../../middleware/v1/verifyJWT.js"; +import { isAllowed } from "../../middleware/v1/isAllowed.js"; import multer from "multer"; const upload = multer(); diff --git a/server/src/routes/v1/checkRoute.js b/server/src/routes/v1/checkRoute.js index 673fe51e0..78f6b8a42 100755 --- a/server/src/routes/v1/checkRoute.js +++ b/server/src/routes/v1/checkRoute.js @@ -1,6 +1,6 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "../../middleware/v1/isAllowed.js"; class CheckRoutes { constructor(checkController) { diff --git a/server/src/routes/v1/diagnosticRoute.js b/server/src/routes/v1/diagnosticRoute.js index 1b7cc06be..2cf481e08 100755 --- a/server/src/routes/v1/diagnosticRoute.js +++ b/server/src/routes/v1/diagnosticRoute.js @@ -1,6 +1,6 @@ import { Router } from "express"; -import { verifyJWT } from "../../middleware/verifyJWT.js"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { verifyJWT } from "../../middleware/v1/verifyJWT.js"; +import { isAllowed } from "../../middleware/v1/isAllowed.js"; class DiagnosticRoutes { constructor(diagnosticController) { diff --git a/server/src/routes/v1/inviteRoute.js b/server/src/routes/v1/inviteRoute.js index 74fcfeb91..a47550536 100755 --- a/server/src/routes/v1/inviteRoute.js +++ b/server/src/routes/v1/inviteRoute.js @@ -1,6 +1,6 @@ import { Router } from "express"; -import { verifyJWT } from "../../middleware/verifyJWT.js"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { verifyJWT } from "../../middleware/v1/verifyJWT.js"; +import { isAllowed } from "../../middleware/v1/isAllowed.js"; class InviteRoutes { constructor(inviteController) { diff --git a/server/src/routes/v1/logRoutes.js b/server/src/routes/v1/logRoutes.js index feabe669a..43da8b75a 100755 --- a/server/src/routes/v1/logRoutes.js +++ b/server/src/routes/v1/logRoutes.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "../../middleware/v1/isAllowed.js"; class LogRoutes { constructor(logController) { this.router = Router(); diff --git a/server/src/routes/v1/monitorRoute.js b/server/src/routes/v1/monitorRoute.js index dde72e256..107964f6b 100755 --- a/server/src/routes/v1/monitorRoute.js +++ b/server/src/routes/v1/monitorRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "../../middleware/v1/isAllowed.js"; import multer from "multer"; import { fetchMonitorCertificate } from "../../controllers/v1/controllerUtils.js"; const upload = multer({ diff --git a/server/src/routes/v1/queueRoute.js b/server/src/routes/v1/queueRoute.js index 59e1cb744..c6cfa05da 100755 --- a/server/src/routes/v1/queueRoute.js +++ b/server/src/routes/v1/queueRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "../../middleware/v1/isAllowed.js"; class QueueRoutes { constructor(queueController) { this.router = Router(); diff --git a/server/src/routes/v1/settingsRoute.js b/server/src/routes/v1/settingsRoute.js index 76556e923..e88186af2 100755 --- a/server/src/routes/v1/settingsRoute.js +++ b/server/src/routes/v1/settingsRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "../../middleware/v1/isAllowed.js"; class SettingsRoutes { constructor(settingsController) { diff --git a/server/src/routes/v1/statusPageRoute.js b/server/src/routes/v1/statusPageRoute.js index 8300066a8..2c3b0b265 100755 --- a/server/src/routes/v1/statusPageRoute.js +++ b/server/src/routes/v1/statusPageRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { verifyJWT } from "../../middleware/verifyJWT.js"; +import { verifyJWT } from "../../middleware/v1/verifyJWT.js"; import multer from "multer"; const upload = multer(); diff --git a/server/src/service/business/checkService.js b/server/src/service/v1/business/checkService.js similarity index 100% rename from server/src/service/business/checkService.js rename to server/src/service/v1/business/checkService.js diff --git a/server/src/service/business/diagnosticService.js b/server/src/service/v1/business/diagnosticService.js similarity index 100% rename from server/src/service/business/diagnosticService.js rename to server/src/service/v1/business/diagnosticService.js diff --git a/server/src/service/business/inviteService.js b/server/src/service/v1/business/inviteService.js similarity index 100% rename from server/src/service/business/inviteService.js rename to server/src/service/v1/business/inviteService.js diff --git a/server/src/service/business/maintenanceWindowService.js b/server/src/service/v1/business/maintenanceWindowService.js similarity index 100% rename from server/src/service/business/maintenanceWindowService.js rename to server/src/service/v1/business/maintenanceWindowService.js diff --git a/server/src/service/business/monitorService.js b/server/src/service/v1/business/monitorService.js similarity index 99% rename from server/src/service/business/monitorService.js rename to server/src/service/v1/business/monitorService.js index 637870b71..061234102 100644 --- a/server/src/service/business/monitorService.js +++ b/server/src/service/v1/business/monitorService.js @@ -1,4 +1,4 @@ -import { createMonitorsBodyValidation } from "../../validation/joi.js"; +import { createMonitorsBodyValidation } from "../../../validation/joi.js"; const SERVICE_NAME = "MonitorService"; class MonitorService { diff --git a/server/src/service/business/userService.js b/server/src/service/v1/business/userService.js similarity index 100% rename from server/src/service/business/userService.js rename to server/src/service/v1/business/userService.js diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js b/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js similarity index 100% rename from server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js rename to server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js b/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js similarity index 90% rename from server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js rename to server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js index b348e2534..2833a59f5 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js +++ b/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js @@ -5,11 +5,11 @@ class SuperSimpleQueueHelper { /** * @param {{ - * db: import("../database").Database, - * logger: import("../logger").Logger, - * networkService: import("../networkService").NetworkService, - * statusService: import("../statusService").StatusService, - * notificationService: import("../notificationService").NotificationService + * db: import("../database.js").Database, + * logger: import("../logger.js").Logger, + * networkService: import("../networkService.js").NetworkService, + * statusService: import("../statusService.js").StatusService, + * notificationService: import("../notificationService.js").NotificationService * }} */ constructor({ db, logger, networkService, statusService, notificationService }) { diff --git a/server/src/service/infrastructure/bufferService.js b/server/src/service/v1/infrastructure/bufferService.js similarity index 100% rename from server/src/service/infrastructure/bufferService.js rename to server/src/service/v1/infrastructure/bufferService.js diff --git a/server/src/service/infrastructure/emailService.js b/server/src/service/v1/infrastructure/emailService.js similarity index 100% rename from server/src/service/infrastructure/emailService.js rename to server/src/service/v1/infrastructure/emailService.js diff --git a/server/src/service/infrastructure/errorService.js b/server/src/service/v1/infrastructure/errorService.js similarity index 100% rename from server/src/service/infrastructure/errorService.js rename to server/src/service/v1/infrastructure/errorService.js diff --git a/server/src/service/infrastructure/networkService.js b/server/src/service/v1/infrastructure/networkService.js similarity index 100% rename from server/src/service/infrastructure/networkService.js rename to server/src/service/v1/infrastructure/networkService.js diff --git a/server/src/service/infrastructure/notificationService.js b/server/src/service/v1/infrastructure/notificationService.js similarity index 100% rename from server/src/service/infrastructure/notificationService.js rename to server/src/service/v1/infrastructure/notificationService.js diff --git a/server/src/service/infrastructure/notificationUtils.js b/server/src/service/v1/infrastructure/notificationUtils.js similarity index 100% rename from server/src/service/infrastructure/notificationUtils.js rename to server/src/service/v1/infrastructure/notificationUtils.js diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/v1/infrastructure/statusService.js similarity index 99% rename from server/src/service/infrastructure/statusService.js rename to server/src/service/v1/infrastructure/statusService.js index 72fa359d7..983541cd9 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/v1/infrastructure/statusService.js @@ -1,4 +1,4 @@ -import MonitorStats from "../../db/models/MonitorStats.js"; +import MonitorStats from "../../../db/models/MonitorStats.js"; const SERVICE_NAME = "StatusService"; class StatusService { diff --git a/server/src/service/system/serviceRegistry.js b/server/src/service/v1/system/serviceRegistry.js similarity index 100% rename from server/src/service/system/serviceRegistry.js rename to server/src/service/v1/system/serviceRegistry.js diff --git a/server/src/service/system/settingsService.js b/server/src/service/v1/system/settingsService.js similarity index 100% rename from server/src/service/system/settingsService.js rename to server/src/service/v1/system/settingsService.js diff --git a/server/src/service/system/stringService.js b/server/src/service/v1/system/stringService.js similarity index 100% rename from server/src/service/system/stringService.js rename to server/src/service/v1/system/stringService.js diff --git a/server/src/service/system/translationService.js b/server/src/service/v1/system/translationService.js similarity index 100% rename from server/src/service/system/translationService.js rename to server/src/service/v1/system/translationService.js diff --git a/server/src/service/v2/business/AuthService.ts b/server/src/service/v2/business/AuthService.ts index ae22b981d..52f586dbf 100644 --- a/server/src/service/v2/business/AuthService.ts +++ b/server/src/service/v2/business/AuthService.ts @@ -3,6 +3,45 @@ import { User, Role, ITokenizedUser } from "../../../db/models/index.js"; import ApiError from "../../../utils/ApiError.js"; import { Types } from "mongoose"; +export const PERMISSIONS = { + users: { + all: "users.*", + create: "users.create", + view: "users.view", + update: "users.update", + delete: "users.delete", + }, + + monitors: { + all: "monitors.*", + create: "monitors.create", + view: "monitors.view", + update: "monitors.update", + delete: "monitors.delete", + }, + notifications: { + all: "notifications.*", + create: "notifications.create", + view: "notifications.view", + update: "notifications.update", + delete: "notifications.delete", + }, + checks: { + all: "checks.*", + create: "checks.create", + view: "checks.view", + update: "checks.update", + delete: "checks.delete", + }, + statusPages: { + all: "statusPages.*", + create: "statusPages.create", + view: "statusPages.view", + update: "statusPages.update", + delete: "statusPages.delete", + }, +}; + export const DEFAULT_ROLES = [ { name: "SuperAdmin", @@ -13,19 +52,19 @@ export const DEFAULT_ROLES = [ { name: "Admin", description: "Admin with full permissions", - permissions: ["monitor.*", "users.*"], + permissions: [PERMISSIONS.monitors.all, PERMISSIONS.users.all], isSystem: true, }, { name: "Manager", description: "Can manage users", - permissions: ["users.create", "users.update", "monitors.*"], + permissions: [PERMISSIONS.users.create, PERMISSIONS.users.update, PERMISSIONS.monitors.all], isSystem: true, }, { name: "Member", description: "Basic team member", - permissions: ["users.update", "monitors.create", "monitors.view", "monitors.update"], + permissions: [PERMISSIONS.users.update, PERMISSIONS.monitors.create, PERMISSIONS.monitors.view, PERMISSIONS.monitors.update], isSystem: true, }, ]; @@ -126,7 +165,7 @@ class AuthService implements IAuthService { } // Check password - const isPasswordValid = await bcrypt.compare(password, user.passwordHash); + const isPasswordValid = await bcrypt.compare(password, user.password); if (!isPasswordValid) { throw new Error("Invalid email or password"); } diff --git a/server/src/types/express.d.ts b/server/src/types/express.d.ts new file mode 100644 index 000000000..7c78a053c --- /dev/null +++ b/server/src/types/express.d.ts @@ -0,0 +1,10 @@ +import { ITokenizedUser } from "../db/models/index.ts"; + +declare global { + namespace Express { + interface Request { + user?: ITokenizedUser; + resource?: any; + } + } +} From 003d419a6453167b81b6037b8ae6ae3f004f10ca Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 23 Sep 2025 14:05:16 -0700 Subject: [PATCH 008/400] Revert "Merge pull request #2974 from bluewave-labs/fix/cleanup" This reverts commit 7368cb2567c0e992cfebe05efddcda110ff16e34, reversing changes made to 72882fded9b57b1d0f76a521e12afea9f63135ce. --- .github/workflows/build-check.yml | 23 --------- server/src/app.js | 10 ++-- server/src/config/routes.js | 4 +- server/src/config/services.js | 36 +++++++------- server/src/controllers/v1/baseController.js | 2 +- server/src/index.js | 2 +- .../src/middleware/{v1 => }/handleErrors.js | 6 +-- server/src/middleware/{v1 => }/isAllowed.js | 6 +-- .../middleware/{v1 => }/languageMiddleware.js | 2 +- server/src/middleware/{v1 => }/rateLimiter.js | 0 .../middleware/{v1 => }/responseHandler.js | 0 .../src/middleware/{v1 => }/sanitization.js | 0 server/src/middleware/{v1 => }/verifyJWT.js | 6 +-- .../middleware/{v1 => }/verifyOwnership.js | 6 +-- .../middleware/{v1 => }/verifySuperAdmin.js | 2 +- .../middleware/{v1 => }/verifyTeamAccess.js | 0 server/src/routes/v1/authRoute.js | 4 +- server/src/routes/v1/checkRoute.js | 2 +- server/src/routes/v1/diagnosticRoute.js | 4 +- server/src/routes/v1/inviteRoute.js | 4 +- server/src/routes/v1/logRoutes.js | 2 +- server/src/routes/v1/monitorRoute.js | 2 +- server/src/routes/v1/queueRoute.js | 2 +- server/src/routes/v1/settingsRoute.js | 2 +- server/src/routes/v1/statusPageRoute.js | 2 +- .../service/{v1 => }/business/checkService.js | 0 .../{v1 => }/business/diagnosticService.js | 0 .../{v1 => }/business/inviteService.js | 0 .../business/maintenanceWindowService.js | 0 .../{v1 => }/business/monitorService.js | 2 +- .../service/{v1 => }/business/userService.js | 0 .../SuperSimpleQueue/SuperSimpleQueue.js | 0 .../SuperSimpleQueueHelper.js | 10 ++-- .../{v1 => }/infrastructure/bufferService.js | 0 .../{v1 => }/infrastructure/emailService.js | 0 .../{v1 => }/infrastructure/errorService.js | 0 .../{v1 => }/infrastructure/networkService.js | 0 .../infrastructure/notificationService.js | 0 .../infrastructure/notificationUtils.js | 0 .../{v1 => }/infrastructure/statusService.js | 2 +- .../{v1 => }/system/serviceRegistry.js | 0 .../{v1 => }/system/settingsService.js | 0 .../service/{v1 => }/system/stringService.js | 0 .../{v1 => }/system/translationService.js | 0 server/src/service/v2/business/AuthService.ts | 47 ++----------------- server/src/types/express.d.ts | 10 ---- 46 files changed, 64 insertions(+), 136 deletions(-) delete mode 100644 .github/workflows/build-check.yml rename server/src/middleware/{v1 => }/handleErrors.js (71%) rename server/src/middleware/{v1 => }/isAllowed.js (87%) rename server/src/middleware/{v1 => }/languageMiddleware.js (93%) rename server/src/middleware/{v1 => }/rateLimiter.js (100%) rename server/src/middleware/{v1 => }/responseHandler.js (100%) rename server/src/middleware/{v1 => }/sanitization.js (100%) rename server/src/middleware/{v1 => }/verifyJWT.js (87%) rename server/src/middleware/{v1 => }/verifyOwnership.js (88%) rename server/src/middleware/{v1 => }/verifySuperAdmin.js (97%) rename server/src/middleware/{v1 => }/verifyTeamAccess.js (100%) rename server/src/service/{v1 => }/business/checkService.js (100%) rename server/src/service/{v1 => }/business/diagnosticService.js (100%) rename server/src/service/{v1 => }/business/inviteService.js (100%) rename server/src/service/{v1 => }/business/maintenanceWindowService.js (100%) rename server/src/service/{v1 => }/business/monitorService.js (99%) rename server/src/service/{v1 => }/business/userService.js (100%) rename server/src/service/{v1 => }/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js (100%) rename server/src/service/{v1 => }/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js (90%) rename server/src/service/{v1 => }/infrastructure/bufferService.js (100%) rename server/src/service/{v1 => }/infrastructure/emailService.js (100%) rename server/src/service/{v1 => }/infrastructure/errorService.js (100%) rename server/src/service/{v1 => }/infrastructure/networkService.js (100%) rename server/src/service/{v1 => }/infrastructure/notificationService.js (100%) rename server/src/service/{v1 => }/infrastructure/notificationUtils.js (100%) rename server/src/service/{v1 => }/infrastructure/statusService.js (99%) rename server/src/service/{v1 => }/system/serviceRegistry.js (100%) rename server/src/service/{v1 => }/system/settingsService.js (100%) rename server/src/service/{v1 => }/system/stringService.js (100%) rename server/src/service/{v1 => }/system/translationService.js (100%) delete mode 100644 server/src/types/express.d.ts diff --git a/.github/workflows/build-check.yml b/.github/workflows/build-check.yml deleted file mode 100644 index 206a652d5..000000000 --- a/.github/workflows/build-check.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Build Check (Server) -on: - pull_request: - workflow_dispatch: - -jobs: - build-server: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Set up Node - uses: actions/setup-node@v4 - with: - node-version: 22 - - - name: Install server dependencies - working-directory: server - run: npm install - - - name: Check server formatting - working-directory: server - run: npm run build diff --git a/server/src/app.js b/server/src/app.js index 253baee88..9cabda6b6 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -1,16 +1,16 @@ import express from "express"; import path from "path"; -import { responseHandler } from "./middleware/v1/responseHandler.js"; +import { responseHandler } from "./middleware/responseHandler.js"; import cors from "cors"; import helmet from "helmet"; import compression from "compression"; import cookieParser from "cookie-parser"; -import languageMiddleware from "./middleware/v1/languageMiddleware.js"; +import languageMiddleware from "./middleware/languageMiddleware.js"; import swaggerUi from "swagger-ui-express"; -import { handleErrors } from "./middleware/v1/handleErrors.js"; +import { handleErrors } from "./middleware/handleErrors.js"; import { setupRoutes } from "./config/routes.js"; -import { generalApiLimiter } from "./middleware/v1/rateLimiter.js"; -import { sanitizeBody, sanitizeQuery } from "./middleware/v1/sanitization.js"; +import { generalApiLimiter } from "./middleware/rateLimiter.js"; +import { sanitizeBody, sanitizeQuery } from "./middleware/sanitization.js"; export const createApp = ({ services, controllers, envSettings, frontendPath, openApiSpec }) => { const allowedOrigin = envSettings.clientHost; diff --git a/server/src/config/routes.js b/server/src/config/routes.js index 2068cf021..5ef622f37 100644 --- a/server/src/config/routes.js +++ b/server/src/config/routes.js @@ -1,5 +1,5 @@ -import { verifyJWT } from "../middleware/v1/verifyJWT.js"; -import { authApiLimiter } from "../middleware/v1/rateLimiter.js"; +import { verifyJWT } from "../middleware/verifyJWT.js"; +import { authApiLimiter } from "../middleware/rateLimiter.js"; import AuthRoutes from "../routes/v1/authRoute.js"; import InviteRoutes from "../routes/v1//inviteRoute.js"; diff --git a/server/src/config/services.js b/server/src/config/services.js index c5ab7fa02..b2b4ac430 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -1,22 +1,22 @@ -import ServiceRegistry from "../service/v1/system/serviceRegistry.js"; -import TranslationService from "../service/v1/system/translationService.js"; -import StringService from "../service/v1/system/stringService.js"; +import ServiceRegistry from "../service/system/serviceRegistry.js"; +import TranslationService from "../service/system/translationService.js"; +import StringService from "../service/system/stringService.js"; import MongoDB from "../db/mongo/MongoDB.js"; -import NetworkService from "../service/v1/infrastructure/networkService.js"; -import EmailService from "../service/v1/infrastructure/emailService.js"; -import BufferService from "../service/v1/infrastructure/bufferService.js"; -import StatusService from "../service/v1/infrastructure/statusService.js"; -import NotificationUtils from "../service/v1/infrastructure/notificationUtils.js"; -import NotificationService from "../service/v1/infrastructure/notificationService.js"; -import ErrorService from "../service/v1/infrastructure/errorService.js"; -import SuperSimpleQueueHelper from "../service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; -import SuperSimpleQueue from "../service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; -import UserService from "../service/v1/business/userService.js"; -import CheckService from "../service/v1/business/checkService.js"; -import DiagnosticService from "../service/v1/business/diagnosticService.js"; -import InviteService from "../service/v1/business/inviteService.js"; -import MaintenanceWindowService from "../service/v1/business/maintenanceWindowService.js"; -import MonitorService from "../service/v1/business/monitorService.js"; +import NetworkService from "../service/infrastructure/networkService.js"; +import EmailService from "../service/infrastructure/emailService.js"; +import BufferService from "../service/infrastructure/bufferService.js"; +import StatusService from "../service/infrastructure/statusService.js"; +import NotificationUtils from "../service/infrastructure/notificationUtils.js"; +import NotificationService from "../service/infrastructure/notificationService.js"; +import ErrorService from "../service/infrastructure/errorService.js"; +import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; +import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; +import UserService from "../service/business/userService.js"; +import CheckService from "../service/business/checkService.js"; +import DiagnosticService from "../service/business/diagnosticService.js"; +import InviteService from "../service/business/inviteService.js"; +import MaintenanceWindowService from "../service/business/maintenanceWindowService.js"; +import MonitorService from "../service/business/monitorService.js"; import papaparse from "papaparse"; import axios from "axios"; import got from "got"; diff --git a/server/src/controllers/v1/baseController.js b/server/src/controllers/v1/baseController.js index e8612cec5..314905c82 100644 --- a/server/src/controllers/v1/baseController.js +++ b/server/src/controllers/v1/baseController.js @@ -1,4 +1,4 @@ -import { AppError } from "../../service/v1/infrastructure/errorService.js"; +import { AppError } from "../../service/infrastructure/errorService.js"; export const createCommonDependencies = (db, errorService, logger, stringService) => { return { diff --git a/server/src/index.js b/server/src/index.js index a4e477d5f..d1ff649eb 100755 --- a/server/src/index.js +++ b/server/src/index.js @@ -7,7 +7,7 @@ import path from "path"; import fs from "fs"; import Logger from "./utils/logger.js"; -import SettingsService from "./service/v1/system/settingsService.js"; +import SettingsService from "./service/system/settingsService.js"; import AppSettings from "./db/models/AppSettings.js"; const SERVICE_NAME = "Server"; diff --git a/server/src/middleware/v1/handleErrors.js b/server/src/middleware/handleErrors.js similarity index 71% rename from server/src/middleware/v1/handleErrors.js rename to server/src/middleware/handleErrors.js index 43671fbe2..b0ded5ab7 100755 --- a/server/src/middleware/v1/handleErrors.js +++ b/server/src/middleware/handleErrors.js @@ -1,6 +1,6 @@ -import { logger } from "../../utils/logger.js"; -import ServiceRegistry from "../../service/v1/system/serviceRegistry.js"; -import StringService from "../../service/v1/system/stringService.js"; +import { logger } from "../utils/logger.js"; +import ServiceRegistry from "../service/system/serviceRegistry.js"; +import StringService from "../service/system/stringService.js"; const handleErrors = (error, req, res, next) => { const status = error.status || 500; diff --git a/server/src/middleware/v1/isAllowed.js b/server/src/middleware/isAllowed.js similarity index 87% rename from server/src/middleware/v1/isAllowed.js rename to server/src/middleware/isAllowed.js index cc240f531..5d61974be 100755 --- a/server/src/middleware/v1/isAllowed.js +++ b/server/src/middleware/isAllowed.js @@ -1,9 +1,9 @@ import jwt from "jsonwebtoken"; const TOKEN_PREFIX = "Bearer "; const SERVICE_NAME = "allowedRoles"; -import ServiceRegistry from "../../service/v1/system/serviceRegistry.js"; -import StringService from "../../service/v1/system/stringService.js"; -import SettingsService from "../../service/v1/system/settingsService.js"; +import ServiceRegistry from "../service/system/serviceRegistry.js"; +import StringService from "../service/system/stringService.js"; +import SettingsService from "../service/system/settingsService.js"; const isAllowed = (allowedRoles) => { return (req, res, next) => { diff --git a/server/src/middleware/v1/languageMiddleware.js b/server/src/middleware/languageMiddleware.js similarity index 93% rename from server/src/middleware/v1/languageMiddleware.js rename to server/src/middleware/languageMiddleware.js index 819858a21..6293dc837 100755 --- a/server/src/middleware/v1/languageMiddleware.js +++ b/server/src/middleware/languageMiddleware.js @@ -1,4 +1,4 @@ -import { logger } from "../../utils/logger.js"; +import { logger } from "../utils/logger.js"; const languageMiddleware = (stringService, translationService) => async (req, res, next) => { try { diff --git a/server/src/middleware/v1/rateLimiter.js b/server/src/middleware/rateLimiter.js similarity index 100% rename from server/src/middleware/v1/rateLimiter.js rename to server/src/middleware/rateLimiter.js diff --git a/server/src/middleware/v1/responseHandler.js b/server/src/middleware/responseHandler.js similarity index 100% rename from server/src/middleware/v1/responseHandler.js rename to server/src/middleware/responseHandler.js diff --git a/server/src/middleware/v1/sanitization.js b/server/src/middleware/sanitization.js similarity index 100% rename from server/src/middleware/v1/sanitization.js rename to server/src/middleware/sanitization.js diff --git a/server/src/middleware/v1/verifyJWT.js b/server/src/middleware/verifyJWT.js similarity index 87% rename from server/src/middleware/v1/verifyJWT.js rename to server/src/middleware/verifyJWT.js index b57391dfd..d01db0a97 100755 --- a/server/src/middleware/v1/verifyJWT.js +++ b/server/src/middleware/verifyJWT.js @@ -1,7 +1,7 @@ import jwt from "jsonwebtoken"; -import ServiceRegistry from "../../service/v1/system/serviceRegistry.js"; -import SettingsService from "../../service/v1/system/settingsService.js"; -import StringService from "../../service/v1/system/stringService.js"; +import ServiceRegistry from "../service/system/serviceRegistry.js"; +import SettingsService from "../service/system/settingsService.js"; +import StringService from "../service/system/stringService.js"; const SERVICE_NAME = "verifyJWT"; const TOKEN_PREFIX = "Bearer "; diff --git a/server/src/middleware/v1/verifyOwnership.js b/server/src/middleware/verifyOwnership.js similarity index 88% rename from server/src/middleware/v1/verifyOwnership.js rename to server/src/middleware/verifyOwnership.js index 741fb6436..4431b0c26 100755 --- a/server/src/middleware/v1/verifyOwnership.js +++ b/server/src/middleware/verifyOwnership.js @@ -1,6 +1,6 @@ -import { logger } from "../../utils/logger.js"; -import ServiceRegistry from "../../service/v1/system/serviceRegistry.js"; -import StringService from "../../service/v1/system/stringService.js"; +import { logger } from "../utils/logger.js"; +import ServiceRegistry from "../service/system/serviceRegistry.js"; +import StringService from "../service/system/stringService.js"; import { ObjectId } from "mongodb"; const SERVICE_NAME = "verifyOwnership"; diff --git a/server/src/middleware/v1/verifySuperAdmin.js b/server/src/middleware/verifySuperAdmin.js similarity index 97% rename from server/src/middleware/v1/verifySuperAdmin.js rename to server/src/middleware/verifySuperAdmin.js index 41ac0a3e0..a5a93eede 100755 --- a/server/src/middleware/v1/verifySuperAdmin.js +++ b/server/src/middleware/verifySuperAdmin.js @@ -1,5 +1,5 @@ const jwt = require("jsonwebtoken"); -const logger = require("../../utils/logger.js"); +const logger = require("../utils/logger"); const SERVICE_NAME = "verifyAdmin"; const TOKEN_PREFIX = "Bearer "; import ServiceRegistry from "../service/serviceRegistry.js"; diff --git a/server/src/middleware/v1/verifyTeamAccess.js b/server/src/middleware/verifyTeamAccess.js similarity index 100% rename from server/src/middleware/v1/verifyTeamAccess.js rename to server/src/middleware/verifyTeamAccess.js diff --git a/server/src/routes/v1/authRoute.js b/server/src/routes/v1/authRoute.js index d9b036b05..f69f94f43 100755 --- a/server/src/routes/v1/authRoute.js +++ b/server/src/routes/v1/authRoute.js @@ -1,6 +1,6 @@ import { Router } from "express"; -import { verifyJWT } from "../../middleware/v1/verifyJWT.js"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; +import { verifyJWT } from "../../middleware/verifyJWT.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; import multer from "multer"; const upload = multer(); diff --git a/server/src/routes/v1/checkRoute.js b/server/src/routes/v1/checkRoute.js index 78f6b8a42..673fe51e0 100755 --- a/server/src/routes/v1/checkRoute.js +++ b/server/src/routes/v1/checkRoute.js @@ -1,6 +1,6 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; class CheckRoutes { constructor(checkController) { diff --git a/server/src/routes/v1/diagnosticRoute.js b/server/src/routes/v1/diagnosticRoute.js index 2cf481e08..1b7cc06be 100755 --- a/server/src/routes/v1/diagnosticRoute.js +++ b/server/src/routes/v1/diagnosticRoute.js @@ -1,6 +1,6 @@ import { Router } from "express"; -import { verifyJWT } from "../../middleware/v1/verifyJWT.js"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; +import { verifyJWT } from "../../middleware/verifyJWT.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; class DiagnosticRoutes { constructor(diagnosticController) { diff --git a/server/src/routes/v1/inviteRoute.js b/server/src/routes/v1/inviteRoute.js index a47550536..74fcfeb91 100755 --- a/server/src/routes/v1/inviteRoute.js +++ b/server/src/routes/v1/inviteRoute.js @@ -1,6 +1,6 @@ import { Router } from "express"; -import { verifyJWT } from "../../middleware/v1/verifyJWT.js"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; +import { verifyJWT } from "../../middleware/verifyJWT.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; class InviteRoutes { constructor(inviteController) { diff --git a/server/src/routes/v1/logRoutes.js b/server/src/routes/v1/logRoutes.js index 43da8b75a..feabe669a 100755 --- a/server/src/routes/v1/logRoutes.js +++ b/server/src/routes/v1/logRoutes.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; class LogRoutes { constructor(logController) { this.router = Router(); diff --git a/server/src/routes/v1/monitorRoute.js b/server/src/routes/v1/monitorRoute.js index 107964f6b..dde72e256 100755 --- a/server/src/routes/v1/monitorRoute.js +++ b/server/src/routes/v1/monitorRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; import multer from "multer"; import { fetchMonitorCertificate } from "../../controllers/v1/controllerUtils.js"; const upload = multer({ diff --git a/server/src/routes/v1/queueRoute.js b/server/src/routes/v1/queueRoute.js index c6cfa05da..59e1cb744 100755 --- a/server/src/routes/v1/queueRoute.js +++ b/server/src/routes/v1/queueRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; class QueueRoutes { constructor(queueController) { this.router = Router(); diff --git a/server/src/routes/v1/settingsRoute.js b/server/src/routes/v1/settingsRoute.js index e88186af2..76556e923 100755 --- a/server/src/routes/v1/settingsRoute.js +++ b/server/src/routes/v1/settingsRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; class SettingsRoutes { constructor(settingsController) { diff --git a/server/src/routes/v1/statusPageRoute.js b/server/src/routes/v1/statusPageRoute.js index 2c3b0b265..8300066a8 100755 --- a/server/src/routes/v1/statusPageRoute.js +++ b/server/src/routes/v1/statusPageRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { verifyJWT } from "../../middleware/v1/verifyJWT.js"; +import { verifyJWT } from "../../middleware/verifyJWT.js"; import multer from "multer"; const upload = multer(); diff --git a/server/src/service/v1/business/checkService.js b/server/src/service/business/checkService.js similarity index 100% rename from server/src/service/v1/business/checkService.js rename to server/src/service/business/checkService.js diff --git a/server/src/service/v1/business/diagnosticService.js b/server/src/service/business/diagnosticService.js similarity index 100% rename from server/src/service/v1/business/diagnosticService.js rename to server/src/service/business/diagnosticService.js diff --git a/server/src/service/v1/business/inviteService.js b/server/src/service/business/inviteService.js similarity index 100% rename from server/src/service/v1/business/inviteService.js rename to server/src/service/business/inviteService.js diff --git a/server/src/service/v1/business/maintenanceWindowService.js b/server/src/service/business/maintenanceWindowService.js similarity index 100% rename from server/src/service/v1/business/maintenanceWindowService.js rename to server/src/service/business/maintenanceWindowService.js diff --git a/server/src/service/v1/business/monitorService.js b/server/src/service/business/monitorService.js similarity index 99% rename from server/src/service/v1/business/monitorService.js rename to server/src/service/business/monitorService.js index 061234102..637870b71 100644 --- a/server/src/service/v1/business/monitorService.js +++ b/server/src/service/business/monitorService.js @@ -1,4 +1,4 @@ -import { createMonitorsBodyValidation } from "../../../validation/joi.js"; +import { createMonitorsBodyValidation } from "../../validation/joi.js"; const SERVICE_NAME = "MonitorService"; class MonitorService { diff --git a/server/src/service/v1/business/userService.js b/server/src/service/business/userService.js similarity index 100% rename from server/src/service/v1/business/userService.js rename to server/src/service/business/userService.js diff --git a/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js similarity index 100% rename from server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js rename to server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js diff --git a/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js similarity index 90% rename from server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js rename to server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js index 2833a59f5..b348e2534 100644 --- a/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js @@ -5,11 +5,11 @@ class SuperSimpleQueueHelper { /** * @param {{ - * db: import("../database.js").Database, - * logger: import("../logger.js").Logger, - * networkService: import("../networkService.js").NetworkService, - * statusService: import("../statusService.js").StatusService, - * notificationService: import("../notificationService.js").NotificationService + * db: import("../database").Database, + * logger: import("../logger").Logger, + * networkService: import("../networkService").NetworkService, + * statusService: import("../statusService").StatusService, + * notificationService: import("../notificationService").NotificationService * }} */ constructor({ db, logger, networkService, statusService, notificationService }) { diff --git a/server/src/service/v1/infrastructure/bufferService.js b/server/src/service/infrastructure/bufferService.js similarity index 100% rename from server/src/service/v1/infrastructure/bufferService.js rename to server/src/service/infrastructure/bufferService.js diff --git a/server/src/service/v1/infrastructure/emailService.js b/server/src/service/infrastructure/emailService.js similarity index 100% rename from server/src/service/v1/infrastructure/emailService.js rename to server/src/service/infrastructure/emailService.js diff --git a/server/src/service/v1/infrastructure/errorService.js b/server/src/service/infrastructure/errorService.js similarity index 100% rename from server/src/service/v1/infrastructure/errorService.js rename to server/src/service/infrastructure/errorService.js diff --git a/server/src/service/v1/infrastructure/networkService.js b/server/src/service/infrastructure/networkService.js similarity index 100% rename from server/src/service/v1/infrastructure/networkService.js rename to server/src/service/infrastructure/networkService.js diff --git a/server/src/service/v1/infrastructure/notificationService.js b/server/src/service/infrastructure/notificationService.js similarity index 100% rename from server/src/service/v1/infrastructure/notificationService.js rename to server/src/service/infrastructure/notificationService.js diff --git a/server/src/service/v1/infrastructure/notificationUtils.js b/server/src/service/infrastructure/notificationUtils.js similarity index 100% rename from server/src/service/v1/infrastructure/notificationUtils.js rename to server/src/service/infrastructure/notificationUtils.js diff --git a/server/src/service/v1/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js similarity index 99% rename from server/src/service/v1/infrastructure/statusService.js rename to server/src/service/infrastructure/statusService.js index 983541cd9..72fa359d7 100755 --- a/server/src/service/v1/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -1,4 +1,4 @@ -import MonitorStats from "../../../db/models/MonitorStats.js"; +import MonitorStats from "../../db/models/MonitorStats.js"; const SERVICE_NAME = "StatusService"; class StatusService { diff --git a/server/src/service/v1/system/serviceRegistry.js b/server/src/service/system/serviceRegistry.js similarity index 100% rename from server/src/service/v1/system/serviceRegistry.js rename to server/src/service/system/serviceRegistry.js diff --git a/server/src/service/v1/system/settingsService.js b/server/src/service/system/settingsService.js similarity index 100% rename from server/src/service/v1/system/settingsService.js rename to server/src/service/system/settingsService.js diff --git a/server/src/service/v1/system/stringService.js b/server/src/service/system/stringService.js similarity index 100% rename from server/src/service/v1/system/stringService.js rename to server/src/service/system/stringService.js diff --git a/server/src/service/v1/system/translationService.js b/server/src/service/system/translationService.js similarity index 100% rename from server/src/service/v1/system/translationService.js rename to server/src/service/system/translationService.js diff --git a/server/src/service/v2/business/AuthService.ts b/server/src/service/v2/business/AuthService.ts index 52f586dbf..ae22b981d 100644 --- a/server/src/service/v2/business/AuthService.ts +++ b/server/src/service/v2/business/AuthService.ts @@ -3,45 +3,6 @@ import { User, Role, ITokenizedUser } from "../../../db/models/index.js"; import ApiError from "../../../utils/ApiError.js"; import { Types } from "mongoose"; -export const PERMISSIONS = { - users: { - all: "users.*", - create: "users.create", - view: "users.view", - update: "users.update", - delete: "users.delete", - }, - - monitors: { - all: "monitors.*", - create: "monitors.create", - view: "monitors.view", - update: "monitors.update", - delete: "monitors.delete", - }, - notifications: { - all: "notifications.*", - create: "notifications.create", - view: "notifications.view", - update: "notifications.update", - delete: "notifications.delete", - }, - checks: { - all: "checks.*", - create: "checks.create", - view: "checks.view", - update: "checks.update", - delete: "checks.delete", - }, - statusPages: { - all: "statusPages.*", - create: "statusPages.create", - view: "statusPages.view", - update: "statusPages.update", - delete: "statusPages.delete", - }, -}; - export const DEFAULT_ROLES = [ { name: "SuperAdmin", @@ -52,19 +13,19 @@ export const DEFAULT_ROLES = [ { name: "Admin", description: "Admin with full permissions", - permissions: [PERMISSIONS.monitors.all, PERMISSIONS.users.all], + permissions: ["monitor.*", "users.*"], isSystem: true, }, { name: "Manager", description: "Can manage users", - permissions: [PERMISSIONS.users.create, PERMISSIONS.users.update, PERMISSIONS.monitors.all], + permissions: ["users.create", "users.update", "monitors.*"], isSystem: true, }, { name: "Member", description: "Basic team member", - permissions: [PERMISSIONS.users.update, PERMISSIONS.monitors.create, PERMISSIONS.monitors.view, PERMISSIONS.monitors.update], + permissions: ["users.update", "monitors.create", "monitors.view", "monitors.update"], isSystem: true, }, ]; @@ -165,7 +126,7 @@ class AuthService implements IAuthService { } // Check password - const isPasswordValid = await bcrypt.compare(password, user.password); + const isPasswordValid = await bcrypt.compare(password, user.passwordHash); if (!isPasswordValid) { throw new Error("Invalid email or password"); } diff --git a/server/src/types/express.d.ts b/server/src/types/express.d.ts deleted file mode 100644 index 7c78a053c..000000000 --- a/server/src/types/express.d.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { ITokenizedUser } from "../db/models/index.ts"; - -declare global { - namespace Express { - interface Request { - user?: ITokenizedUser; - resource?: any; - } - } -} From 990aed9f2e01c459135d6de835b084d452fe4abe Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 23 Sep 2025 14:05:23 -0700 Subject: [PATCH 009/400] Revert "Merge pull request #2973 from bluewave-labs/feat/v2/auth" This reverts commit 72882fded9b57b1d0f76a521e12afea9f63135ce, reversing changes made to bfb299cfcaa4dcfaff68daed9f4629bc8fcce554. --- server/nodemon.json | 4 +- server/package-lock.json | 117 --------------- server/package.json | 4 +- server/src/config/controllers.js | 6 - server/src/config/routes.js | 10 -- server/src/config/services.js | 15 +- server/src/controllers/v2/AuthController.ts | 142 ------------------ server/src/db/models/InviteToken.js | 35 +++++ .../src/db/models/{auth/User.ts => User.js} | 80 ++-------- server/src/db/models/auth/Invite.ts | 68 --------- server/src/db/models/auth/Role.ts | 45 ------ server/src/db/models/index.ts | 9 -- server/src/db/models/migration/Migration.ts | 34 ----- ...s => 0001_migrateStatusWindowThreshold.js} | 1 - .../db/mongo/migration/0002_migrateUsers.ts | 56 ------- server/src/db/mongo/migration/index.js | 7 + server/src/db/mongo/migration/index.ts | 25 --- server/src/db/mongo/modules/inviteModule.js | 12 +- server/src/middleware/v2/VerifyToken.ts | 21 --- server/src/middleware/verifyTeamAccess.js | 2 - server/src/routes/v2/auth.ts | 31 ---- server/src/service/v2/business/AuthService.ts | 141 ----------------- .../src/service/v2/business/InviteService.ts | 61 -------- server/src/utils/ApiError.ts | 15 -- server/src/utils/JWTUtils.ts | 22 --- 25 files changed, 69 insertions(+), 894 deletions(-) delete mode 100644 server/src/controllers/v2/AuthController.ts create mode 100755 server/src/db/models/InviteToken.js rename server/src/db/models/{auth/User.ts => User.js} (55%) delete mode 100755 server/src/db/models/auth/Invite.ts delete mode 100644 server/src/db/models/auth/Role.ts delete mode 100644 server/src/db/models/index.ts delete mode 100644 server/src/db/models/migration/Migration.ts rename server/src/db/mongo/migration/{0001_migrateStatusWindowThreshold.ts => 0001_migrateStatusWindowThreshold.js} (90%) delete mode 100644 server/src/db/mongo/migration/0002_migrateUsers.ts create mode 100644 server/src/db/mongo/migration/index.js delete mode 100644 server/src/db/mongo/migration/index.ts delete mode 100644 server/src/middleware/v2/VerifyToken.ts delete mode 100644 server/src/routes/v2/auth.ts delete mode 100644 server/src/service/v2/business/AuthService.ts delete mode 100644 server/src/service/v2/business/InviteService.ts delete mode 100644 server/src/utils/ApiError.ts delete mode 100644 server/src/utils/JWTUtils.ts diff --git a/server/nodemon.json b/server/nodemon.json index 7371b4904..88d6ac686 100755 --- a/server/nodemon.json +++ b/server/nodemon.json @@ -1,5 +1,5 @@ { "ignore": ["src/locales/*", "*.log", "node_modules/*"], - "watch": ["src/**/*.js", "src/**/*.ts", "*.json"], - "ext": "ts,js,json" + "watch": ["src/**/*.js", "*.json"], + "ext": "js,json" } diff --git a/server/package-lock.json b/server/package-lock.json index e21b28382..06c1c30b3 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -45,8 +45,6 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", - "@types/express": "5.0.3", - "@types/jsonwebtoken": "9.0.10", "c8": "10.1.3", "chai": "5.2.0", "eslint": "^9.17.0", @@ -1680,27 +1678,6 @@ "node": ">=10.13.0" } }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -1708,44 +1685,12 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/express": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", - "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^5.0.0", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", - "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", "license": "MIT" }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -1760,31 +1705,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/jsonwebtoken": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", - "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/ms": "*", - "@types/node": "*" - } - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/node": { "version": "24.3.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", @@ -1794,49 +1714,12 @@ "undici-types": "~7.10.0" } }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT" - }, "node_modules/@types/relateurl": { "version": "0.2.33", "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.33.tgz", "integrity": "sha512-bTQCKsVbIdzLqZhLkF5fcJQreE4y1ro4DIyVrlDNSCJRRwHhB8Z+4zXXa8jN6eDvc2HbRsEYgbvrnGvi54EpSw==", "license": "MIT" }, - "node_modules/@types/send": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", diff --git a/server/package.json b/server/package.json index 5c655af9d..637c785cc 100755 --- a/server/package.json +++ b/server/package.json @@ -2,7 +2,7 @@ "name": "server", "version": "1.0.0", "description": "", - "main": "dist/index.js", + "main": "index.js", "type": "module", "scripts": { "test": "c8 mocha", @@ -54,8 +54,6 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", - "@types/express": "5.0.3", - "@types/jsonwebtoken": "9.0.10", "c8": "10.1.3", "chai": "5.2.0", "eslint": "^9.17.0", diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js index 250d04c4b..5c54579eb 100644 --- a/server/src/config/controllers.js +++ b/server/src/config/controllers.js @@ -15,9 +15,6 @@ import StatusPageController from "../controllers/v1/statusPageController.js"; import NotificationController from "../controllers/v1/notificationController.js"; import DiagnosticController from "../controllers/v1/diagnosticController.js"; -// V2 -import AuthV2Controller from "../controllers/v2/AuthController.js"; - export const initializeControllers = (services) => { const controllers = {}; const commonDependencies = createCommonDependencies(services.db, services.errorService, services.logger, services.stringService); @@ -65,8 +62,5 @@ export const initializeControllers = (services) => { diagnosticService: services.diagnosticService, }); - // V2 - controllers.authV2Controller = new AuthV2Controller(services.authV2Service, services.inviteV2Service); - return controllers; }; diff --git a/server/src/config/routes.js b/server/src/config/routes.js index 5ef622f37..2ec93a2dc 100644 --- a/server/src/config/routes.js +++ b/server/src/config/routes.js @@ -13,8 +13,6 @@ import LogRoutes from "../routes/v1/logRoutes.js"; import DiagnosticRoutes from "../routes/v1//diagnosticRoute.js"; import NotificationRoutes from "../routes/v1/notificationRoute.js"; -import authV2 from "../routes/v2/auth.js"; - export const setupRoutes = (app, controllers) => { const authRoutes = new AuthRoutes(controllers.authController); const monitorRoutes = new MonitorRoutes(controllers.monitorController); @@ -39,12 +37,4 @@ export const setupRoutes = (app, controllers) => { app.use("/api/v1/status-page", statusPageRoutes.getRouter()); app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter()); app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter()); - - // ******************* - // V2 Routes - // ******************* - - const v2AuthRoutes = new authV2(controllers.authV2Controller); - - app.use("/api/v2/auth", authApiLimiter, v2AuthRoutes.getRouter()); }; diff --git a/server/src/config/services.js b/server/src/config/services.js index b2b4ac430..08c2525f3 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -48,8 +48,8 @@ import { ParseBoolean } from "../utils/utils.js"; // Models import Check from "../db/models/Check.js"; import Monitor from "../db/models/Monitor.js"; -import { User } from "../db/models/auth/User.js"; -import { Invite } from "../db/models/auth/Invite.js"; +import User from "../db/models/User.js"; +import InviteToken from "../db/models/InviteToken.js"; import StatusPage from "../db/models/StatusPage.js"; import Team from "../db/models/Team.js"; import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; @@ -68,10 +68,6 @@ import NotificationModule from "../db/mongo/modules/notificationModule.js"; import RecoveryModule from "../db/mongo/modules/recoveryModule.js"; import SettingsModule from "../db/mongo/modules/settingsModule.js"; -// v2 -import AuthV2Service from "../service/v2/business/AuthService.js"; -import InviteV2Service from "../service/v2/business/InviteService.js"; - export const initializeServices = async ({ logger, envSettings, settingsService }) => { const serviceRegistry = new ServiceRegistry({ logger }); ServiceRegistry.instance = serviceRegistry; @@ -83,7 +79,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService // Create DB const checkModule = new CheckModule({ logger, Check, Monitor, User }); - const inviteModule = new InviteModule({ Invite, crypto, stringService }); + const inviteModule = new InviteModule({ InviteToken, crypto, stringService }); const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService }); const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); @@ -212,9 +208,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService games, }); - const authV2Service = new AuthV2Service(); - const inviteV2Service = new InviteV2Service(); - const services = { settingsService, translationService, @@ -234,8 +227,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService monitorService, errorService, logger, - authV2Service, - inviteV2Service, }; Object.values(services).forEach((service) => { diff --git a/server/src/controllers/v2/AuthController.ts b/server/src/controllers/v2/AuthController.ts deleted file mode 100644 index a9d1dd203..000000000 --- a/server/src/controllers/v2/AuthController.ts +++ /dev/null @@ -1,142 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { encode, decode } from "../../utils/JWTUtils.js"; -import AuthService from "../../service/v2/business/AuthService.js"; -import ApiError from "../../utils/ApiError.js"; -import InviteService from "../../service/v2/business/InviteService.js"; -import { IInvite } from "../../db/models/index.js"; - -export interface IAuthController { - register(req: Request, res: Response, next: NextFunction): Promise; - registerWithInvite(req: Request, res: Response, next: NextFunction): Promise; - login(req: Request, res: Response, next: NextFunction): Promise; - logout(req: Request, res: Response): void; - me(req: Request, res: Response, next: NextFunction): void; -} - -class AuthController implements IAuthController { - private authService: AuthService; - private inviteService: InviteService; - constructor(authService: AuthService, inviteService: InviteService) { - this.authService = authService; - this.inviteService = inviteService; - } - - register = async (req: Request, res: Response, next: NextFunction) => { - try { - const { email, firstName, lastName, password } = req.body; - - if (!email || !firstName || !lastName || !password) { - throw new Error("Email, firstName, lastName, and password are required"); - } - - const result = await this.authService.register({ - email, - firstName, - lastName, - password, - }); - - const token = encode(result); - - res.cookie("token", token, { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week - }); - - res.status(201).json({ - message: "User created successfully", - }); - } catch (error) { - next(error); - } - }; - - registerWithInvite = async (req: Request, res: Response, next: NextFunction) => { - try { - const token = req.params.token; - if (!token) { - throw new ApiError("Invite token is required", 400); - } - - const invite: IInvite = await this.inviteService.get(token); - - const { firstName, lastName, password } = req.body; - const email = invite?.email; - const roles = invite?.roles; - - if (!email || !firstName || !lastName || !password || !roles || roles.length === 0) { - throw new Error("Email, firstName, lastName, password, and roles are required"); - } - - const result = await this.authService.registerWithInvite({ - email, - firstName, - lastName, - password, - roles, - }); - - if (!result) { - throw new Error("Registration failed"); - } - - await this.inviteService.delete(invite._id.toString()); - - const jwt = encode(result); - - res.cookie("token", jwt, { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week - }); - - res.status(201).json({ message: "User created successfully" }); - } catch (error) { - next(error); - } - }; - - login = async (req: Request, res: Response, next: NextFunction) => { - try { - const { email, password } = req.body; - // Validation - if (!email || !password) { - throw new ApiError("Email and password are required", 400); - } - const result = await this.authService.login({ email, password }); - - const token = encode(result); - - res.cookie("token", token, { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week - }); - - res.status(200).json({ - message: "Login successful", - }); - } catch (error) { - next(error); - } - }; - - logout = (req: Request, res: Response) => { - res.clearCookie("token", { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - }); - res.status(200).json({ message: "Logout successful" }); - }; - - me = (req: Request, res: Response, next: NextFunction) => { - return res.status(200).json({ message: "OK" }); - }; -} - -export default AuthController; diff --git a/server/src/db/models/InviteToken.js b/server/src/db/models/InviteToken.js new file mode 100755 index 000000000..079600647 --- /dev/null +++ b/server/src/db/models/InviteToken.js @@ -0,0 +1,35 @@ +import mongoose from "mongoose"; +const InviteTokenSchema = mongoose.Schema( + { + email: { + type: String, + required: true, + unique: true, + }, + teamId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Team", + immutable: true, + required: true, + }, + role: { + type: Array, + required: true, + default: ["user"], + }, + token: { + type: String, + required: true, + }, + expiry: { + type: Date, + default: Date.now, + expires: 3600, + }, + }, + { + timestamps: true, + } +); + +export default mongoose.model("InviteToken", InviteTokenSchema); diff --git a/server/src/db/models/auth/User.ts b/server/src/db/models/User.js similarity index 55% rename from server/src/db/models/auth/User.ts rename to server/src/db/models/User.js index 82c058ea2..9368d98b1 100755 --- a/server/src/db/models/auth/User.ts +++ b/server/src/db/models/User.js @@ -1,47 +1,11 @@ -import mongoose, { Schema, Document, Types } from "mongoose"; - +import mongoose from "mongoose"; import bcrypt from "bcryptjs"; -import logger from "../../../utils/logger.js"; -import Monitor from "../Monitor.js"; -import Team from "../Team.js"; -import Notification from "../Notification.js"; -import { subscribe } from "diagnostics_channel"; +import logger from "../../utils/logger.js"; +import Monitor from "./Monitor.js"; +import Team from "./Team.js"; +import Notification from "./Notification.js"; -export const RoleTypes = ["user", "admin", "superadmin", "demo"] as const; -export type RoleType = (typeof RoleTypes)[number]; - -export interface ITokenizedUser { - sub: string; - roles: string[]; -} - -export interface IUser extends Document { - // V1 - _id: Types.ObjectId; - firstName: string; - lastName: string; - email: string; - password: string; - avatarImage: string; - profileImage: { - data: Buffer; - contentType: string; - }; - isActive: boolean; - isVerified: boolean; - role?: RoleType[]; - teamId: Types.ObjectId; - checkTTL: number; - createdAt: Date; - updatedAt: Date; - comparePassword: (submittedPassword: string) => Promise; - // V2 - roles: Types.ObjectId[]; - lastLoginAt?: Date; - version: number; -} - -const UserSchema = new Schema( +const UserSchema = mongoose.Schema( { firstName: { type: String, @@ -58,7 +22,7 @@ const UserSchema = new Schema( }, password: { type: String, - required: false, + required: true, }, avatarImage: { type: String, @@ -77,8 +41,8 @@ const UserSchema = new Schema( }, role: { type: [String], - default: ["user"], - enum: RoleTypes, + default: "user", + enum: ["user", "admin", "superadmin", "demo"], }, teamId: { type: mongoose.Schema.Types.ObjectId, @@ -88,22 +52,6 @@ const UserSchema = new Schema( checkTTL: { type: Number, }, - - // v2 - roles: [ - { - type: Schema.Types.ObjectId, - ref: "Role", - default: [], - }, - ], - lastLoginAt: { - type: Date, - }, - version: { - type: Number, - default: 1, - }, }, { timestamps: true, @@ -121,7 +69,7 @@ UserSchema.pre("save", function (next) { UserSchema.pre("findOneAndUpdate", function (next) { const update = this.getUpdate(); - if (update && "password" in update) { + if ("password" in update) { const salt = bcrypt.genSaltSync(10); update.password = bcrypt.hashSync(update.password, salt); } @@ -144,13 +92,15 @@ UserSchema.pre("findOneAndDelete", async function (next) { } next(); } catch (error) { - next(error as Error); + next(error); } }); -UserSchema.methods.comparePassword = async function (submittedPassword: string) { +UserSchema.methods.comparePassword = async function (submittedPassword) { const res = await bcrypt.compare(submittedPassword, this.password); return res; }; -export const User = mongoose.model("User", UserSchema); +const User = mongoose.model("User", UserSchema); + +export default User; diff --git a/server/src/db/models/auth/Invite.ts b/server/src/db/models/auth/Invite.ts deleted file mode 100755 index 148ff65cb..000000000 --- a/server/src/db/models/auth/Invite.ts +++ /dev/null @@ -1,68 +0,0 @@ -import mongoose, { Schema, Document, Types } from "mongoose"; - -export interface IInvite extends Document { - // v1 - _id: Types.ObjectId; - email: string; - teamId: Types.ObjectId; - role: string[]; - token: string; - expiry: Date; - createdAt: Date; - updatedAt: Date; - - // v2 - tokenHash: string; - roles: Types.ObjectId[]; - createdBy: Types.ObjectId; - updatedBy: Types.ObjectId; -} - -const InviteSchema = new Schema( - { - email: { - type: String, - required: true, - unique: true, - trim: true, - lowercase: true, - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - required: true, - }, - role: { - type: [String], - required: true, - default: ["user"], - }, - token: { - type: String, - required: true, - }, - expiry: { - type: Date, - default: Date.now, - expires: 3600, - }, - - // v2 - roles: [ - { - type: Schema.Types.ObjectId, - ref: "Role", - required: true, - }, - ], - tokenHash: { type: String, required: true, unique: true }, - createdBy: { type: Schema.Types.ObjectId, ref: "User", required: true }, - updatedBy: { type: Schema.Types.ObjectId, ref: "User", required: true }, - }, - { - timestamps: true, - } -); - -export const Invite = mongoose.model("Invite", InviteSchema); diff --git a/server/src/db/models/auth/Role.ts b/server/src/db/models/auth/Role.ts deleted file mode 100644 index e7ffb2f7d..000000000 --- a/server/src/db/models/auth/Role.ts +++ /dev/null @@ -1,45 +0,0 @@ -import mongoose, { Schema, Document, Types } from "mongoose"; - -export interface IRole extends Document { - _id: Types.ObjectId; - name: string; - description?: string; - permissions: string[]; - isActive: boolean; - createdAt: Date; - updatedAt: Date; -} - -const roleSchema = new Schema( - { - name: { - type: String, - required: true, - trim: true, - maxlength: 50, - }, - description: { - type: String, - trim: true, - maxlength: 200, - }, - - permissions: [ - { - type: String, - required: true, - }, - ], - isActive: { - type: Boolean, - default: true, - }, - }, - { - timestamps: true, - } -); - -roleSchema.index({ name: 1 }, { unique: true }); - -export const Role = mongoose.model("Role", roleSchema); diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts deleted file mode 100644 index 537633bfd..000000000 --- a/server/src/db/models/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -export { User } from "./auth/User.js"; -export type { IUser } from "./auth/User.js"; -export type { ITokenizedUser } from "./auth/User.js"; -export { Role } from "./auth/Role.js"; -export type { IRole } from "./auth/Role.js"; -export type { IInvite } from "./auth/Invite.js"; -export { Invite } from "./auth/Invite.js"; -export { Migration } from "./migration/Migration.js"; -export type { IMigration } from "./migration/Migration.js"; diff --git a/server/src/db/models/migration/Migration.ts b/server/src/db/models/migration/Migration.ts deleted file mode 100644 index cf76bb48e..000000000 --- a/server/src/db/models/migration/Migration.ts +++ /dev/null @@ -1,34 +0,0 @@ -import mongoose, { Schema, Document, Types } from "mongoose"; - -export interface IMigration extends Document { - _id: Types.ObjectId; - name: string; - runAt: Date; - success: boolean; - createdAt: Date; - updatedAt: Date; -} - -const migrationSchema = new Schema( - { - name: { - type: String, - required: true, - }, - runAt: { - type: Date, - required: false, - }, - success: { - type: Boolean, - required: false, - }, - }, - { - timestamps: true, - } -); - -migrationSchema.index({ name: 1 }, { unique: true }); - -export const Migration = mongoose.model("Migration", migrationSchema); diff --git a/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.ts b/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.js similarity index 90% rename from server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.ts rename to server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.js index b44487e87..8f6988de4 100644 --- a/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.ts +++ b/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.js @@ -1,5 +1,4 @@ import Monitor from "../../models/Monitor.js"; -export const MIGRATION_NAME = "0001_migrateStatusWindowThreshold"; async function migrateStatusWindowThreshold() { try { const monitors = await Monitor.find({ statusWindowThreshold: { $lt: 1 } }); diff --git a/server/src/db/mongo/migration/0002_migrateUsers.ts b/server/src/db/mongo/migration/0002_migrateUsers.ts deleted file mode 100644 index a7cdb76fc..000000000 --- a/server/src/db/mongo/migration/0002_migrateUsers.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { User, Role } from "../../models/index.js"; -import { Types } from "mongoose"; - -import { DEFAULT_ROLES } from "../../../service/v2/business/AuthService.js"; -export const MIGRATION_NAME = "0002_migrateUsers"; -async function migrateUsers() { - try { - // Create roles if they don't exist - const roleCount = await Role.countDocuments(); - let roles; - if (roleCount === 0) { - const rolePromises = DEFAULT_ROLES.map((roleData) => - new Role({ - ...roleData, - }).save() - ); - roles = await Promise.all(rolePromises); - } else { - roles = await Role.find(); - } - - // Migrate users - const users = await User.find({ - $or: [{ version: { $exists: false } }, { version: { $lt: 2 } }], - }); - for (const user of users) { - const newRoleIds: Types.ObjectId[] = []; - - for (const role of user.role || []) { - if (role === "superadmin") { - const superAdminRole = roles.find((role) => role.name === "SuperAdmin"); - newRoleIds.push(superAdminRole!._id); - } - if (role === "admin") { - const managerRole = roles.find((role) => role.name === "Manager"); - newRoleIds.push(managerRole!._id); - } - if (role === "user") { - const memberRole = roles.find((role) => role.name === "Member"); - newRoleIds.push(memberRole!._id); - } - } - const merged = [...new Set([...user.roles, ...newRoleIds])]; - user.roles = merged; - user.version = 2; - await user.save(); - } - - return true; - } catch (err) { - console.error("Migration error:", err); - return false; - } -} - -export { migrateUsers }; diff --git a/server/src/db/mongo/migration/index.js b/server/src/db/mongo/migration/index.js new file mode 100644 index 000000000..4bf56895a --- /dev/null +++ b/server/src/db/mongo/migration/index.js @@ -0,0 +1,7 @@ +import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js"; + +const runMigrations = async () => { + await migrateStatusWindowThreshold(); +}; + +export { runMigrations }; diff --git a/server/src/db/mongo/migration/index.ts b/server/src/db/mongo/migration/index.ts deleted file mode 100644 index 293e0905f..000000000 --- a/server/src/db/mongo/migration/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { migrateStatusWindowThreshold, MIGRATION_NAME as MIGRATION_0001 } from "./0001_migrateStatusWindowThreshold.js"; -import { migrateUsers, MIGRATION_NAME as MIGRATION_0002 } from "./0002_migrateUsers.js"; -import { Migration, IMigration } from "../../models/index.js"; - -const runMigrations = async () => { - const migrations = [ - { name: MIGRATION_0001, migration: migrateStatusWindowThreshold }, - { name: MIGRATION_0002, migration: migrateUsers }, - ]; - - const migrationNames = migrations.map((m) => m.name); - - const appliedMigrations = await Migration.find({ name: { $in: migrationNames }, success: true }); - - const migrationsToRun = migrations.filter((m) => !appliedMigrations.some((am: IMigration) => am.name === m.name)); - - for (const { name, migration } of migrationsToRun) { - console.log(`Running migration: ${name}`); - const success = await migration(); - await Migration.updateOne({ name }, { $set: { runAt: new Date(), success } }, { upsert: true }); - console.log(`Migration ${name} completed with status: ${success ? "success" : "failure"}`); - } -}; - -export { runMigrations }; diff --git a/server/src/db/mongo/modules/inviteModule.js b/server/src/db/mongo/modules/inviteModule.js index 513524fe5..444e51efd 100755 --- a/server/src/db/mongo/modules/inviteModule.js +++ b/server/src/db/mongo/modules/inviteModule.js @@ -1,17 +1,17 @@ const SERVICE_NAME = "inviteModule"; class InviteModule { - constructor({ Invite, crypto, stringService }) { - this.Invite = Invite; + constructor({ InviteToken, crypto, stringService }) { + this.InviteToken = InviteToken; this.crypto = crypto; this.stringService = stringService; } requestInviteToken = async (userData) => { try { - await this.Invite.deleteMany({ email: userData.email }); + await this.InviteToken.deleteMany({ email: userData.email }); userData.token = this.crypto.randomBytes(32).toString("hex"); - let inviteToken = new this.Invite(userData); + let inviteToken = new this.InviteToken(userData); await inviteToken.save(); return inviteToken; } catch (error) { @@ -23,7 +23,7 @@ class InviteModule { getInviteToken = async (token) => { try { - const invite = await this.Invite.findOne({ + const invite = await this.InviteToken.findOne({ token, }); if (invite === null) { @@ -38,7 +38,7 @@ class InviteModule { }; getInviteTokenAndDelete = async (token) => { try { - const invite = await this.Invite.findOneAndDelete({ + const invite = await this.InviteToken.findOneAndDelete({ token, }); if (invite === null) { diff --git a/server/src/middleware/v2/VerifyToken.ts b/server/src/middleware/v2/VerifyToken.ts deleted file mode 100644 index 7dabd51cc..000000000 --- a/server/src/middleware/v2/VerifyToken.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import { decode } from "../../utils/JWTUtils.js"; -import ApiError from "../../utils/ApiError.js"; -const verifyToken = (req: Request, res: Response, next: NextFunction) => { - const token = req.cookies.token; - - if (!token) { - const error = new ApiError("No token provided", 401); - return next(error); - } - - try { - const decoded = decode(token); - req.user = decoded; - next(); - } catch (error) { - next(error); - } -}; - -export { verifyToken }; diff --git a/server/src/middleware/verifyTeamAccess.js b/server/src/middleware/verifyTeamAccess.js index a548cbf23..60647bdc2 100644 --- a/server/src/middleware/verifyTeamAccess.js +++ b/server/src/middleware/verifyTeamAccess.js @@ -3,8 +3,6 @@ const SERVICE_NAME = "verifyTeamAccess"; const verifyTeamAccess = (Model, paramName) => { return async (req, res, next) => { try { - const user = req.user; - console.log(user); const documentId = req.params[paramName]; const doc = await Model.findById(documentId); diff --git a/server/src/routes/v2/auth.ts b/server/src/routes/v2/auth.ts deleted file mode 100644 index 5a1b8ad63..000000000 --- a/server/src/routes/v2/auth.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Router } from "express"; - -import express from "express"; -import AuthController from "../../controllers/v2/AuthController.js"; -import { verifyToken } from "../../middleware/v2/VerifyToken.js"; - -const router = express.Router(); - -class AuthRoutes { - private controller: AuthController; - private router: Router; - constructor(authController: AuthController) { - this.controller = authController; - this.router = Router(); - this.initRoutes(); - } - - initRoutes = () => { - this.router.post("/register", this.controller.register); - this.router.post("/register/invite/:token", this.controller.registerWithInvite); - this.router.post("/login", this.controller.login); - this.router.post("/logout", this.controller.logout); - this.router.get("/me", verifyToken, this.controller.me); - }; - - getRouter() { - return this.router; - } -} - -export default AuthRoutes; diff --git a/server/src/service/v2/business/AuthService.ts b/server/src/service/v2/business/AuthService.ts deleted file mode 100644 index ae22b981d..000000000 --- a/server/src/service/v2/business/AuthService.ts +++ /dev/null @@ -1,141 +0,0 @@ -import bcrypt from "bcryptjs"; -import { User, Role, ITokenizedUser } from "../../../db/models/index.js"; -import ApiError from "../../../utils/ApiError.js"; -import { Types } from "mongoose"; - -export const DEFAULT_ROLES = [ - { - name: "SuperAdmin", - description: "Super admin with all permissions", - permissions: ["*"], - isSystem: true, - }, - { - name: "Admin", - description: "Admin with full permissions", - permissions: ["monitor.*", "users.*"], - isSystem: true, - }, - { - name: "Manager", - description: "Can manage users", - permissions: ["users.create", "users.update", "monitors.*"], - isSystem: true, - }, - { - name: "Member", - description: "Basic team member", - permissions: ["users.update", "monitors.create", "monitors.view", "monitors.update"], - isSystem: true, - }, -]; - -export type RegisterData = { - email: string; - firstName: string; - lastName: string; - password: string; - roles?: Types.ObjectId[]; // Optional roles for invite-based registration -}; - -export type LoginData = { - email: string; - password: string; -}; - -export type AuthResult = ITokenizedUser; - -export interface IAuthService { - register(signupData: RegisterData): Promise; - registerWithInvite(signupData: RegisterData): Promise; - login(loginData: LoginData): Promise; -} - -class AuthService implements IAuthService { - constructor() {} - - async register(signupData: RegisterData): Promise { - const userCount = await User.countDocuments(); - - if (userCount > 0) { - throw new Error("Registration is closed. Please request an invite."); - } - - const { email, firstName, lastName, password } = signupData; - - // Create all default roles - const rolePromises = DEFAULT_ROLES.map((roleData) => - new Role({ - ...roleData, - }).save() - ); - const roles = await Promise.all(rolePromises); - - // Find admin role and assign to first user - const superAdminRole = roles.find((role) => role.name === "SuperAdmin"); - - const user = new User({ - email, - firstName, - lastName, - password, - roles: [superAdminRole!._id], - }); - - const savedUser = await user.save(); - return { - sub: savedUser._id.toString(), - roles: savedUser.roles.map((role) => role.toString()), - }; - } - - async registerWithInvite(signupData: RegisterData): Promise { - const { email, firstName, lastName, password, roles } = signupData; - - const user = new User({ - email, - firstName, - lastName, - password, - roles: roles || [], - }); - try { - const savedUser = await user.save(); - return { - sub: savedUser._id.toString(), - roles: savedUser.roles.map((role) => role.toString()), - }; - } catch (error: any) { - if (error?.code === 11000) { - const dupError = new ApiError("Email already in use", 409); - dupError.stack = error?.stack; - throw dupError; - } - throw error; - } - } - - async login(loginData: LoginData): Promise { - const { email, password } = loginData; - - // Find user by email - const user = await User.findOne({ email }); - - if (!user) { - throw new Error("Invalid email or password"); - } - - // Check password - const isPasswordValid = await bcrypt.compare(password, user.passwordHash); - if (!isPasswordValid) { - throw new Error("Invalid email or password"); - } - - return { - sub: user._id.toString(), - roles: user.roles.map((role) => role.toString()), - }; - } -} - -export default AuthService; diff --git a/server/src/service/v2/business/InviteService.ts b/server/src/service/v2/business/InviteService.ts deleted file mode 100644 index f8bd9e2db..000000000 --- a/server/src/service/v2/business/InviteService.ts +++ /dev/null @@ -1,61 +0,0 @@ -import crypto from "node:crypto"; -import { ITokenizedUser, IInvite, Invite } from "../../../db/models/index.js"; -import ApiError from "../../../utils/ApiError.js"; - -export interface IInviteService { - create: (tokenizedUser: ITokenizedUser, invite: IInvite) => Promise<{ token: string }>; - getAll: () => Promise; - get: (tokenHash: string) => Promise; - delete: (id: string) => Promise; -} - -class InviteService implements IInviteService { - constructor() {} - - create = async (tokenizedUser: ITokenizedUser, inviteData: IInvite) => { - const token = crypto.randomBytes(32).toString("hex"); - const tokenHash = crypto.createHash("sha256").update(token).digest("hex"); - try { - const invite = await Invite.create({ - ...inviteData, - tokenHash, - createdBy: tokenizedUser.sub, - updatedBy: tokenizedUser.sub, - }); - if (!invite) { - throw new ApiError("Failed to create invite", 500); - } - return { token }; - } catch (error: any) { - if (error?.code === 11000) { - const dupError = new ApiError("Invite with this email already exists", 409); - dupError.stack = error?.stack; - throw dupError; - } - throw error; - } - }; - - get = async (token: string) => { - const tokenHash = crypto.createHash("sha256").update(token).digest("hex"); - const invite = await Invite.findOne({ tokenHash }); - if (!invite) { - throw new ApiError("Invite not found", 404); - } - return invite; - }; - - getAll = async () => { - return Invite.find(); - }; - - delete = async (id: string) => { - const result = await Invite.deleteOne({ _id: id }); - if (!result.deletedCount) { - throw new ApiError("Invite not found", 404); - } - return result.deletedCount === 1; - }; -} - -export default InviteService; diff --git a/server/src/utils/ApiError.ts b/server/src/utils/ApiError.ts deleted file mode 100644 index 313d01c2a..000000000 --- a/server/src/utils/ApiError.ts +++ /dev/null @@ -1,15 +0,0 @@ -class ApiError extends Error { - public status: number; - - constructor(message: string, status: number = 500) { - super(message); - this.status = status; - this.name = this.constructor.name; - - if (Error.captureStackTrace) { - Error.captureStackTrace(this, this.constructor); - } - } -} - -export default ApiError; diff --git a/server/src/utils/JWTUtils.ts b/server/src/utils/JWTUtils.ts deleted file mode 100644 index 4eebb78a0..000000000 --- a/server/src/utils/JWTUtils.ts +++ /dev/null @@ -1,22 +0,0 @@ -import jwt from "jsonwebtoken"; -import { AuthResult } from "../service/v2/business/AuthService.js"; - -const encode = (data: AuthResult): string => { - const secret = process.env.JWT_SECRET; - if (!secret) { - throw new Error("JWT_SECRET is not defined"); - } - const token = jwt.sign(data, secret, { expiresIn: "99d" }); - return token; -}; - -const decode = (token: string): AuthResult => { - const secret = process.env.JWT_SECRET; - if (!secret) { - throw new Error("JWT_SECRET is not defined"); - } - const decoded = jwt.verify(token, secret) as AuthResult; - return decoded; -}; - -export { encode, decode }; From 1383d8b59acb7db9852c410b3838d15cdeb7d693 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 23 Sep 2025 14:12:17 -0700 Subject: [PATCH 010/400] add workflow --- .github/workflows/check-build.yml | 23 +++++++++++++++++++ ...s => 0001_migrateStatusWindowThreshold.ts} | 0 .../db/mongo/migration/{index.js => index.ts} | 0 server/tsconfig.json | 2 +- 4 files changed, 24 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/check-build.yml rename server/src/db/mongo/migration/{0001_migrateStatusWindowThreshold.js => 0001_migrateStatusWindowThreshold.ts} (100%) rename server/src/db/mongo/migration/{index.js => index.ts} (100%) diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml new file mode 100644 index 000000000..8417b823f --- /dev/null +++ b/.github/workflows/check-build.yml @@ -0,0 +1,23 @@ +name: Build Check ( Server) +on: + pull_request: + workflow_dispatch: + +jobs: + build-server: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install server dependencies + working-directory: server + run: npm install + + - name: Check server build + working-directory: server + run: npm run build diff --git a/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.js b/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.ts similarity index 100% rename from server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.js rename to server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.ts diff --git a/server/src/db/mongo/migration/index.js b/server/src/db/mongo/migration/index.ts similarity index 100% rename from server/src/db/mongo/migration/index.js rename to server/src/db/mongo/migration/index.ts diff --git a/server/tsconfig.json b/server/tsconfig.json index 3aa773ab1..260ccb964 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -22,5 +22,5 @@ "allowJs": true, "checkJs": false }, - "exclude": ["node_modules", "dist", "**/*.config.js"] + "exclude": ["node_modules", "dist/", "**/*.config.js"] } From b3b6f1d834ce64623d3c9a4c9953de79b633db62 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 23 Sep 2025 14:23:45 -0700 Subject: [PATCH 011/400] move all v1 files to v1 dirs --- server/src/app.js | 10 +- server/src/config/routes.js | 4 +- server/src/config/services.js | 78 +-- server/src/controllers/v1/baseController.js | 2 +- .../src/controllers/v2/MonitorController.ts | 157 ++++++ server/src/db/{mongo => v1}/MongoDB.js | 2 +- .../0001_migrateStatusWindowThreshold.ts | 2 +- .../src/db/{mongo => v1}/migration/index.ts | 0 server/src/db/{ => v1}/models/AppSettings.js | 0 server/src/db/{ => v1}/models/Check.js | 0 server/src/db/{ => v1}/models/InviteToken.js | 0 .../db/{ => v1}/models/MaintenanceWindow.js | 0 server/src/db/{ => v1}/models/Monitor.js | 0 server/src/db/{ => v1}/models/MonitorStats.js | 0 server/src/db/{ => v1}/models/Notification.js | 0 .../src/db/{ => v1}/models/RecoveryToken.js | 0 server/src/db/{ => v1}/models/StatusPage.js | 0 server/src/db/{ => v1}/models/Team.js | 0 server/src/db/{ => v1}/models/User.js | 1 - .../db/{mongo => v1}/modules/checkModule.js | 0 .../modules/checkModuleQueries.js | 0 .../db/{mongo => v1}/modules/inviteModule.js | 0 .../modules/maintenanceWindowModule.js | 0 .../db/{mongo => v1}/modules/monitorModule.js | 0 .../modules/monitorModuleQueries.js | 0 .../modules/notificationModule.js | 0 .../{mongo => v1}/modules/recoveryModule.js | 0 .../{mongo => v1}/modules/settingsModule.js | 0 .../{mongo => v1}/modules/statusPageModule.js | 0 .../db/{mongo => v1}/modules/userModule.js | 0 server/src/db/{mongo => v1}/utils/seedDb.js | 4 +- server/src/index.js | 4 +- .../src/middleware/{ => v1}/handleErrors.js | 6 +- server/src/middleware/{ => v1}/isAllowed.js | 6 +- .../middleware/{ => v1}/languageMiddleware.js | 2 +- server/src/middleware/{ => v1}/rateLimiter.js | 0 .../middleware/{ => v1}/responseHandler.js | 0 .../src/middleware/{ => v1}/sanitization.js | 0 server/src/middleware/{ => v1}/verifyJWT.js | 6 +- .../middleware/{ => v1}/verifyOwnership.js | 6 +- .../middleware/{ => v1}/verifySuperAdmin.js | 2 +- .../middleware/{ => v1}/verifyTeamAccess.js | 0 server/src/routes/v1/authRoute.js | 4 +- server/src/routes/v1/checkRoute.js | 2 +- server/src/routes/v1/diagnosticRoute.js | 4 +- server/src/routes/v1/inviteRoute.js | 4 +- server/src/routes/v1/logRoutes.js | 2 +- .../src/routes/v1/maintenanceWindowRoute.js | 2 +- server/src/routes/v1/monitorRoute.js | 2 +- server/src/routes/v1/queueRoute.js | 2 +- server/src/routes/v1/settingsRoute.js | 2 +- server/src/routes/v1/statusPageRoute.js | 2 +- .../service/{ => v1}/business/checkService.js | 0 .../{ => v1}/business/diagnosticService.js | 0 .../{ => v1}/business/inviteService.js | 0 .../business/maintenanceWindowService.js | 0 .../{ => v1}/business/monitorService.js | 2 +- .../service/{ => v1}/business/userService.js | 0 .../SuperSimpleQueue/SuperSimpleQueue.js | 0 .../SuperSimpleQueueHelper.js | 10 +- .../{ => v1}/infrastructure/bufferService.js | 0 .../{ => v1}/infrastructure/emailService.js | 0 .../{ => v1}/infrastructure/errorService.js | 0 .../{ => v1}/infrastructure/networkService.js | 0 .../infrastructure/notificationService.js | 0 .../infrastructure/notificationUtils.js | 0 .../{ => v1}/infrastructure/statusService.js | 2 +- .../{ => v1}/system/serviceRegistry.js | 0 .../{ => v1}/system/settingsService.js | 0 .../service/{ => v1}/system/stringService.js | 0 .../{ => v1}/system/translationService.js | 0 .../src/service/v2/business/CheckService.ts | 132 +++++ .../src/service/v2/business/MonitorService.ts | 464 ++++++++++++++++++ 73 files changed, 840 insertions(+), 88 deletions(-) create mode 100644 server/src/controllers/v2/MonitorController.ts rename server/src/db/{mongo => v1}/MongoDB.js (97%) rename server/src/db/{mongo => v1}/migration/0001_migrateStatusWindowThreshold.ts (92%) rename server/src/db/{mongo => v1}/migration/index.ts (100%) rename server/src/db/{ => v1}/models/AppSettings.js (100%) rename server/src/db/{ => v1}/models/Check.js (100%) rename server/src/db/{ => v1}/models/InviteToken.js (100%) rename server/src/db/{ => v1}/models/MaintenanceWindow.js (100%) rename server/src/db/{ => v1}/models/Monitor.js (100%) rename server/src/db/{ => v1}/models/MonitorStats.js (100%) rename server/src/db/{ => v1}/models/Notification.js (100%) rename server/src/db/{ => v1}/models/RecoveryToken.js (100%) rename server/src/db/{ => v1}/models/StatusPage.js (100%) rename server/src/db/{ => v1}/models/Team.js (100%) rename server/src/db/{ => v1}/models/User.js (98%) rename server/src/db/{mongo => v1}/modules/checkModule.js (100%) rename server/src/db/{mongo => v1}/modules/checkModuleQueries.js (100%) rename server/src/db/{mongo => v1}/modules/inviteModule.js (100%) rename server/src/db/{mongo => v1}/modules/maintenanceWindowModule.js (100%) rename server/src/db/{mongo => v1}/modules/monitorModule.js (100%) rename server/src/db/{mongo => v1}/modules/monitorModuleQueries.js (100%) rename server/src/db/{mongo => v1}/modules/notificationModule.js (100%) rename server/src/db/{mongo => v1}/modules/recoveryModule.js (100%) rename server/src/db/{mongo => v1}/modules/settingsModule.js (100%) rename server/src/db/{mongo => v1}/modules/statusPageModule.js (100%) rename server/src/db/{mongo => v1}/modules/userModule.js (100%) rename server/src/db/{mongo => v1}/utils/seedDb.js (95%) rename server/src/middleware/{ => v1}/handleErrors.js (71%) rename server/src/middleware/{ => v1}/isAllowed.js (87%) rename server/src/middleware/{ => v1}/languageMiddleware.js (93%) rename server/src/middleware/{ => v1}/rateLimiter.js (100%) rename server/src/middleware/{ => v1}/responseHandler.js (100%) rename server/src/middleware/{ => v1}/sanitization.js (100%) rename server/src/middleware/{ => v1}/verifyJWT.js (87%) rename server/src/middleware/{ => v1}/verifyOwnership.js (88%) rename server/src/middleware/{ => v1}/verifySuperAdmin.js (97%) rename server/src/middleware/{ => v1}/verifyTeamAccess.js (100%) rename server/src/service/{ => v1}/business/checkService.js (100%) rename server/src/service/{ => v1}/business/diagnosticService.js (100%) rename server/src/service/{ => v1}/business/inviteService.js (100%) rename server/src/service/{ => v1}/business/maintenanceWindowService.js (100%) rename server/src/service/{ => v1}/business/monitorService.js (99%) rename server/src/service/{ => v1}/business/userService.js (100%) rename server/src/service/{ => v1}/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js (100%) rename server/src/service/{ => v1}/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js (90%) rename server/src/service/{ => v1}/infrastructure/bufferService.js (100%) rename server/src/service/{ => v1}/infrastructure/emailService.js (100%) rename server/src/service/{ => v1}/infrastructure/errorService.js (100%) rename server/src/service/{ => v1}/infrastructure/networkService.js (100%) rename server/src/service/{ => v1}/infrastructure/notificationService.js (100%) rename server/src/service/{ => v1}/infrastructure/notificationUtils.js (100%) rename server/src/service/{ => v1}/infrastructure/statusService.js (99%) rename server/src/service/{ => v1}/system/serviceRegistry.js (100%) rename server/src/service/{ => v1}/system/settingsService.js (100%) rename server/src/service/{ => v1}/system/stringService.js (100%) rename server/src/service/{ => v1}/system/translationService.js (100%) create mode 100644 server/src/service/v2/business/CheckService.ts create mode 100644 server/src/service/v2/business/MonitorService.ts diff --git a/server/src/app.js b/server/src/app.js index 9cabda6b6..253baee88 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -1,16 +1,16 @@ import express from "express"; import path from "path"; -import { responseHandler } from "./middleware/responseHandler.js"; +import { responseHandler } from "./middleware/v1/responseHandler.js"; import cors from "cors"; import helmet from "helmet"; import compression from "compression"; import cookieParser from "cookie-parser"; -import languageMiddleware from "./middleware/languageMiddleware.js"; +import languageMiddleware from "./middleware/v1/languageMiddleware.js"; import swaggerUi from "swagger-ui-express"; -import { handleErrors } from "./middleware/handleErrors.js"; +import { handleErrors } from "./middleware/v1/handleErrors.js"; import { setupRoutes } from "./config/routes.js"; -import { generalApiLimiter } from "./middleware/rateLimiter.js"; -import { sanitizeBody, sanitizeQuery } from "./middleware/sanitization.js"; +import { generalApiLimiter } from "./middleware/v1/rateLimiter.js"; +import { sanitizeBody, sanitizeQuery } from "./middleware/v1/sanitization.js"; export const createApp = ({ services, controllers, envSettings, frontendPath, openApiSpec }) => { const allowedOrigin = envSettings.clientHost; diff --git a/server/src/config/routes.js b/server/src/config/routes.js index 2ec93a2dc..40bf8e994 100644 --- a/server/src/config/routes.js +++ b/server/src/config/routes.js @@ -1,5 +1,5 @@ -import { verifyJWT } from "../middleware/verifyJWT.js"; -import { authApiLimiter } from "../middleware/rateLimiter.js"; +import { verifyJWT } from "../middleware/v1/verifyJWT.js"; +import { authApiLimiter } from "../middleware/v1/rateLimiter.js"; import AuthRoutes from "../routes/v1/authRoute.js"; import InviteRoutes from "../routes/v1//inviteRoute.js"; diff --git a/server/src/config/services.js b/server/src/config/services.js index 08c2525f3..22f978071 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -1,22 +1,22 @@ -import ServiceRegistry from "../service/system/serviceRegistry.js"; -import TranslationService from "../service/system/translationService.js"; -import StringService from "../service/system/stringService.js"; -import MongoDB from "../db/mongo/MongoDB.js"; -import NetworkService from "../service/infrastructure/networkService.js"; -import EmailService from "../service/infrastructure/emailService.js"; -import BufferService from "../service/infrastructure/bufferService.js"; -import StatusService from "../service/infrastructure/statusService.js"; -import NotificationUtils from "../service/infrastructure/notificationUtils.js"; -import NotificationService from "../service/infrastructure/notificationService.js"; -import ErrorService from "../service/infrastructure/errorService.js"; -import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; -import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; -import UserService from "../service/business/userService.js"; -import CheckService from "../service/business/checkService.js"; -import DiagnosticService from "../service/business/diagnosticService.js"; -import InviteService from "../service/business/inviteService.js"; -import MaintenanceWindowService from "../service/business/maintenanceWindowService.js"; -import MonitorService from "../service/business/monitorService.js"; +import ServiceRegistry from "../service/v1/system/serviceRegistry.js"; +import TranslationService from "../service/v1/system/translationService.js"; +import StringService from "../service/v1/system/stringService.js"; +import MongoDB from "../db/v1/MongoDB.js"; +import NetworkService from "../service/v1/infrastructure/networkService.js"; +import EmailService from "../service/v1/infrastructure/emailService.js"; +import BufferService from "../service/v1/infrastructure/bufferService.js"; +import StatusService from "../service/v1/infrastructure/statusService.js"; +import NotificationUtils from "../service/v1/infrastructure/notificationUtils.js"; +import NotificationService from "../service/v1/infrastructure/notificationService.js"; +import ErrorService from "../service/v1/infrastructure/errorService.js"; +import SuperSimpleQueueHelper from "../service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; +import SuperSimpleQueue from "../service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; +import UserService from "../service/v1/business/userService.js"; +import CheckService from "../service/v1/business/checkService.js"; +import DiagnosticService from "../service/v1/business/diagnosticService.js"; +import InviteService from "../service/v1/business/inviteService.js"; +import MaintenanceWindowService from "../service/v1/business/maintenanceWindowService.js"; +import MonitorService from "../service/v1/business/monitorService.js"; import papaparse from "papaparse"; import axios from "axios"; import got from "got"; @@ -46,27 +46,27 @@ import { GenerateAvatarImage } from "../utils/imageProcessing.js"; import { ParseBoolean } from "../utils/utils.js"; // Models -import Check from "../db/models/Check.js"; -import Monitor from "../db/models/Monitor.js"; -import User from "../db/models/User.js"; -import InviteToken from "../db/models/InviteToken.js"; -import StatusPage from "../db/models/StatusPage.js"; -import Team from "../db/models/Team.js"; -import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; -import MonitorStats from "../db/models/MonitorStats.js"; -import Notification from "../db/models/Notification.js"; -import RecoveryToken from "../db/models/RecoveryToken.js"; -import AppSettings from "../db/models/AppSettings.js"; +import Check from "../db/v1/models/Check.js"; +import Monitor from "../db/v1/models/Monitor.js"; +import User from "../db/v1/models/User.js"; +import InviteToken from "../db/v1/models/InviteToken.js"; +import StatusPage from "../db/v1/models/StatusPage.js"; +import Team from "../db/v1/models/Team.js"; +import MaintenanceWindow from "../db/v1/models/MaintenanceWindow.js"; +import MonitorStats from "../db/v1/models/MonitorStats.js"; +import Notification from "../db/v1/models/Notification.js"; +import RecoveryToken from "../db/v1/models/RecoveryToken.js"; +import AppSettings from "../db/v1/models/AppSettings.js"; -import InviteModule from "../db/mongo/modules/inviteModule.js"; -import CheckModule from "../db/mongo/modules/checkModule.js"; -import StatusPageModule from "../db/mongo/modules/statusPageModule.js"; -import UserModule from "../db/mongo/modules/userModule.js"; -import MaintenanceWindowModule from "../db/mongo/modules/maintenanceWindowModule.js"; -import MonitorModule from "../db/mongo/modules/monitorModule.js"; -import NotificationModule from "../db/mongo/modules/notificationModule.js"; -import RecoveryModule from "../db/mongo/modules/recoveryModule.js"; -import SettingsModule from "../db/mongo/modules/settingsModule.js"; +import InviteModule from "../db/v1/modules/inviteModule.js"; +import CheckModule from "../db/v1/modules/checkModule.js"; +import StatusPageModule from "../db/v1/modules/statusPageModule.js"; +import UserModule from "../db/v1/modules//userModule.js"; +import MaintenanceWindowModule from "../db/v1/modules/maintenanceWindowModule.js"; +import MonitorModule from "../db/v1/modules/monitorModule.js"; +import NotificationModule from "../db/v1/modules/notificationModule.js"; +import RecoveryModule from "../db/v1/modules/recoveryModule.js"; +import SettingsModule from "../db/v1/modules/settingsModule.js"; export const initializeServices = async ({ logger, envSettings, settingsService }) => { const serviceRegistry = new ServiceRegistry({ logger }); diff --git a/server/src/controllers/v1/baseController.js b/server/src/controllers/v1/baseController.js index 314905c82..e8612cec5 100644 --- a/server/src/controllers/v1/baseController.js +++ b/server/src/controllers/v1/baseController.js @@ -1,4 +1,4 @@ -import { AppError } from "../../service/infrastructure/errorService.js"; +import { AppError } from "../../service/v1/infrastructure/errorService.js"; export const createCommonDependencies = (db, errorService, logger, stringService) => { return { diff --git a/server/src/controllers/v2/MonitorController.ts b/server/src/controllers/v2/MonitorController.ts new file mode 100644 index 000000000..6493b7f44 --- /dev/null +++ b/server/src/controllers/v2/MonitorController.ts @@ -0,0 +1,157 @@ +import { Request, Response, NextFunction } from "express"; +import ApiError from "../../utils/ApiError.js"; +import MonitorService from "../../service/v2/business/MonitorService.js"; +import { MonitorType } from "../../db/v1/models/Monitor.js"; +class MonitorController { + private monitorService: MonitorService; + constructor(monitorService: MonitorService) { + this.monitorService = monitorService; + } + + create = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const monitor = await this.monitorService.create(tokenizedUser, req.body); + res.status(201).json({ + message: "Monitor created successfully", + data: monitor, + }); + } catch (error) { + next(error); + } + }; + + get = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const id = req.params.id; + if (!id) { + throw new ApiError("Monitor ID is required", 400); + } + + const range = req.query.range; + if (!range || typeof range !== "string") throw new ApiError("Range query parameter is required", 400); + + let monitor; + + const status = req.query.status; + if (status && typeof status !== "string") { + throw new ApiError("Status query parameter must be a string", 400); + } + + if (req.query.embedChecks === "true") { + monitor = await this.monitorService.getEmbedChecks(id, range, status); + } else { + monitor = await this.monitorService.get(id); + } + + res.status(200).json({ + message: "Monitor retrieved successfully", + data: monitor, + }); + } catch (error) { + next(error); + } + }; + + getAll = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + let monitors; + if (req.query.embedChecks === "true") { + const page = Math.max(1, Number(req.query.page) || 1); + const limit = Math.max(1, Number(req.query.limit) || 10); + const type: MonitorType[] = req.query.type as MonitorType[]; + + monitors = await this.monitorService.getAllEmbedChecks(page, limit, type); + } else { + monitors = await this.monitorService.getAll(); + } + + res.status(200).json({ + message: "Monitors retrieved successfully", + data: monitors, + }); + } catch (error) { + next(error); + } + }; + + toggleActive = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const id = req.params.id; + if (!id) { + throw new ApiError("Monitor ID is required", 400); + } + + const monitor = await this.monitorService.toggleActive(id, tokenizedUser); + res.status(200).json({ + message: "Monitor paused/unpaused successfully", + data: monitor, + }); + } catch (error) { + next(error); + } + }; + + update = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const id = req.params.id; + if (!id) { + throw new ApiError("Monitor ID is required", 400); + } + + const monitor = await this.monitorService.update(tokenizedUser, id, req.body); + res.status(200).json({ + message: "Monitor updated successfully", + data: monitor, + }); + } catch (error) { + next(error); + } + }; + + delete = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + const id = req.params.id; + if (!id) { + throw new ApiError("Monitor ID is required", 400); + } + await this.monitorService.delete(id); + + res.status(200).json({ + message: "Monitor deleted successfully", + }); + } catch (error) { + next(error); + } + }; +} + +export default MonitorController; diff --git a/server/src/db/mongo/MongoDB.js b/server/src/db/v1/MongoDB.js similarity index 97% rename from server/src/db/mongo/MongoDB.js rename to server/src/db/v1/MongoDB.js index 74fcbd89f..3bdae3525 100755 --- a/server/src/db/mongo/MongoDB.js +++ b/server/src/db/v1/MongoDB.js @@ -1,5 +1,5 @@ import mongoose from "mongoose"; -import AppSettings from "../models/AppSettings.js"; +import AppSettings from "./models/AppSettings.js"; import { runMigrations } from "./migration/index.js"; class MongoDB { static SERVICE_NAME = "MongoDB"; diff --git a/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.ts b/server/src/db/v1/migration/0001_migrateStatusWindowThreshold.ts similarity index 92% rename from server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.ts rename to server/src/db/v1/migration/0001_migrateStatusWindowThreshold.ts index 8f6988de4..857d36381 100644 --- a/server/src/db/mongo/migration/0001_migrateStatusWindowThreshold.ts +++ b/server/src/db/v1/migration/0001_migrateStatusWindowThreshold.ts @@ -1,4 +1,4 @@ -import Monitor from "../../models/Monitor.js"; +import Monitor from "../models/Monitor.js"; async function migrateStatusWindowThreshold() { try { const monitors = await Monitor.find({ statusWindowThreshold: { $lt: 1 } }); diff --git a/server/src/db/mongo/migration/index.ts b/server/src/db/v1/migration/index.ts similarity index 100% rename from server/src/db/mongo/migration/index.ts rename to server/src/db/v1/migration/index.ts diff --git a/server/src/db/models/AppSettings.js b/server/src/db/v1/models/AppSettings.js similarity index 100% rename from server/src/db/models/AppSettings.js rename to server/src/db/v1/models/AppSettings.js diff --git a/server/src/db/models/Check.js b/server/src/db/v1/models/Check.js similarity index 100% rename from server/src/db/models/Check.js rename to server/src/db/v1/models/Check.js diff --git a/server/src/db/models/InviteToken.js b/server/src/db/v1/models/InviteToken.js similarity index 100% rename from server/src/db/models/InviteToken.js rename to server/src/db/v1/models/InviteToken.js diff --git a/server/src/db/models/MaintenanceWindow.js b/server/src/db/v1/models/MaintenanceWindow.js similarity index 100% rename from server/src/db/models/MaintenanceWindow.js rename to server/src/db/v1/models/MaintenanceWindow.js diff --git a/server/src/db/models/Monitor.js b/server/src/db/v1/models/Monitor.js similarity index 100% rename from server/src/db/models/Monitor.js rename to server/src/db/v1/models/Monitor.js diff --git a/server/src/db/models/MonitorStats.js b/server/src/db/v1/models/MonitorStats.js similarity index 100% rename from server/src/db/models/MonitorStats.js rename to server/src/db/v1/models/MonitorStats.js diff --git a/server/src/db/models/Notification.js b/server/src/db/v1/models/Notification.js similarity index 100% rename from server/src/db/models/Notification.js rename to server/src/db/v1/models/Notification.js diff --git a/server/src/db/models/RecoveryToken.js b/server/src/db/v1/models/RecoveryToken.js similarity index 100% rename from server/src/db/models/RecoveryToken.js rename to server/src/db/v1/models/RecoveryToken.js diff --git a/server/src/db/models/StatusPage.js b/server/src/db/v1/models/StatusPage.js similarity index 100% rename from server/src/db/models/StatusPage.js rename to server/src/db/v1/models/StatusPage.js diff --git a/server/src/db/models/Team.js b/server/src/db/v1/models/Team.js similarity index 100% rename from server/src/db/models/Team.js rename to server/src/db/v1/models/Team.js diff --git a/server/src/db/models/User.js b/server/src/db/v1/models/User.js similarity index 98% rename from server/src/db/models/User.js rename to server/src/db/v1/models/User.js index 9368d98b1..bf3012b26 100755 --- a/server/src/db/models/User.js +++ b/server/src/db/v1/models/User.js @@ -1,6 +1,5 @@ import mongoose from "mongoose"; import bcrypt from "bcryptjs"; -import logger from "../../utils/logger.js"; import Monitor from "./Monitor.js"; import Team from "./Team.js"; import Notification from "./Notification.js"; diff --git a/server/src/db/mongo/modules/checkModule.js b/server/src/db/v1/modules/checkModule.js similarity index 100% rename from server/src/db/mongo/modules/checkModule.js rename to server/src/db/v1/modules/checkModule.js diff --git a/server/src/db/mongo/modules/checkModuleQueries.js b/server/src/db/v1/modules/checkModuleQueries.js similarity index 100% rename from server/src/db/mongo/modules/checkModuleQueries.js rename to server/src/db/v1/modules/checkModuleQueries.js diff --git a/server/src/db/mongo/modules/inviteModule.js b/server/src/db/v1/modules/inviteModule.js similarity index 100% rename from server/src/db/mongo/modules/inviteModule.js rename to server/src/db/v1/modules/inviteModule.js diff --git a/server/src/db/mongo/modules/maintenanceWindowModule.js b/server/src/db/v1/modules/maintenanceWindowModule.js similarity index 100% rename from server/src/db/mongo/modules/maintenanceWindowModule.js rename to server/src/db/v1/modules/maintenanceWindowModule.js diff --git a/server/src/db/mongo/modules/monitorModule.js b/server/src/db/v1/modules/monitorModule.js similarity index 100% rename from server/src/db/mongo/modules/monitorModule.js rename to server/src/db/v1/modules/monitorModule.js diff --git a/server/src/db/mongo/modules/monitorModuleQueries.js b/server/src/db/v1/modules/monitorModuleQueries.js similarity index 100% rename from server/src/db/mongo/modules/monitorModuleQueries.js rename to server/src/db/v1/modules/monitorModuleQueries.js diff --git a/server/src/db/mongo/modules/notificationModule.js b/server/src/db/v1/modules/notificationModule.js similarity index 100% rename from server/src/db/mongo/modules/notificationModule.js rename to server/src/db/v1/modules/notificationModule.js diff --git a/server/src/db/mongo/modules/recoveryModule.js b/server/src/db/v1/modules/recoveryModule.js similarity index 100% rename from server/src/db/mongo/modules/recoveryModule.js rename to server/src/db/v1/modules/recoveryModule.js diff --git a/server/src/db/mongo/modules/settingsModule.js b/server/src/db/v1/modules/settingsModule.js similarity index 100% rename from server/src/db/mongo/modules/settingsModule.js rename to server/src/db/v1/modules/settingsModule.js diff --git a/server/src/db/mongo/modules/statusPageModule.js b/server/src/db/v1/modules/statusPageModule.js similarity index 100% rename from server/src/db/mongo/modules/statusPageModule.js rename to server/src/db/v1/modules/statusPageModule.js diff --git a/server/src/db/mongo/modules/userModule.js b/server/src/db/v1/modules/userModule.js similarity index 100% rename from server/src/db/mongo/modules/userModule.js rename to server/src/db/v1/modules/userModule.js diff --git a/server/src/db/mongo/utils/seedDb.js b/server/src/db/v1/utils/seedDb.js similarity index 95% rename from server/src/db/mongo/utils/seedDb.js rename to server/src/db/v1/utils/seedDb.js index 224744a51..cdfe3675c 100755 --- a/server/src/db/mongo/utils/seedDb.js +++ b/server/src/db/v1/utils/seedDb.js @@ -1,5 +1,5 @@ -import Monitor from "../../models/Monitor.js"; -import Check from "../../models/Check.js"; +import Monitor from "../models/Monitor.js"; +import Check from "../models/Check.js"; import { logger } from "../../../utils/logger.js"; const generateRandomUrl = () => { diff --git a/server/src/index.js b/server/src/index.js index d1ff649eb..e8b756d0e 100755 --- a/server/src/index.js +++ b/server/src/index.js @@ -7,8 +7,8 @@ import path from "path"; import fs from "fs"; import Logger from "./utils/logger.js"; -import SettingsService from "./service/system/settingsService.js"; -import AppSettings from "./db/models/AppSettings.js"; +import SettingsService from "./service/v1/system/settingsService.js"; +import AppSettings from "./db/v1/models/AppSettings.js"; const SERVICE_NAME = "Server"; let logger; diff --git a/server/src/middleware/handleErrors.js b/server/src/middleware/v1/handleErrors.js similarity index 71% rename from server/src/middleware/handleErrors.js rename to server/src/middleware/v1/handleErrors.js index b0ded5ab7..43671fbe2 100755 --- a/server/src/middleware/handleErrors.js +++ b/server/src/middleware/v1/handleErrors.js @@ -1,6 +1,6 @@ -import { logger } from "../utils/logger.js"; -import ServiceRegistry from "../service/system/serviceRegistry.js"; -import StringService from "../service/system/stringService.js"; +import { logger } from "../../utils/logger.js"; +import ServiceRegistry from "../../service/v1/system/serviceRegistry.js"; +import StringService from "../../service/v1/system/stringService.js"; const handleErrors = (error, req, res, next) => { const status = error.status || 500; diff --git a/server/src/middleware/isAllowed.js b/server/src/middleware/v1/isAllowed.js similarity index 87% rename from server/src/middleware/isAllowed.js rename to server/src/middleware/v1/isAllowed.js index 5d61974be..cc240f531 100755 --- a/server/src/middleware/isAllowed.js +++ b/server/src/middleware/v1/isAllowed.js @@ -1,9 +1,9 @@ import jwt from "jsonwebtoken"; const TOKEN_PREFIX = "Bearer "; const SERVICE_NAME = "allowedRoles"; -import ServiceRegistry from "../service/system/serviceRegistry.js"; -import StringService from "../service/system/stringService.js"; -import SettingsService from "../service/system/settingsService.js"; +import ServiceRegistry from "../../service/v1/system/serviceRegistry.js"; +import StringService from "../../service/v1/system/stringService.js"; +import SettingsService from "../../service/v1/system/settingsService.js"; const isAllowed = (allowedRoles) => { return (req, res, next) => { diff --git a/server/src/middleware/languageMiddleware.js b/server/src/middleware/v1/languageMiddleware.js similarity index 93% rename from server/src/middleware/languageMiddleware.js rename to server/src/middleware/v1/languageMiddleware.js index 6293dc837..819858a21 100755 --- a/server/src/middleware/languageMiddleware.js +++ b/server/src/middleware/v1/languageMiddleware.js @@ -1,4 +1,4 @@ -import { logger } from "../utils/logger.js"; +import { logger } from "../../utils/logger.js"; const languageMiddleware = (stringService, translationService) => async (req, res, next) => { try { diff --git a/server/src/middleware/rateLimiter.js b/server/src/middleware/v1/rateLimiter.js similarity index 100% rename from server/src/middleware/rateLimiter.js rename to server/src/middleware/v1/rateLimiter.js diff --git a/server/src/middleware/responseHandler.js b/server/src/middleware/v1/responseHandler.js similarity index 100% rename from server/src/middleware/responseHandler.js rename to server/src/middleware/v1/responseHandler.js diff --git a/server/src/middleware/sanitization.js b/server/src/middleware/v1/sanitization.js similarity index 100% rename from server/src/middleware/sanitization.js rename to server/src/middleware/v1/sanitization.js diff --git a/server/src/middleware/verifyJWT.js b/server/src/middleware/v1/verifyJWT.js similarity index 87% rename from server/src/middleware/verifyJWT.js rename to server/src/middleware/v1/verifyJWT.js index d01db0a97..b57391dfd 100755 --- a/server/src/middleware/verifyJWT.js +++ b/server/src/middleware/v1/verifyJWT.js @@ -1,7 +1,7 @@ import jwt from "jsonwebtoken"; -import ServiceRegistry from "../service/system/serviceRegistry.js"; -import SettingsService from "../service/system/settingsService.js"; -import StringService from "../service/system/stringService.js"; +import ServiceRegistry from "../../service/v1/system/serviceRegistry.js"; +import SettingsService from "../../service/v1/system/settingsService.js"; +import StringService from "../../service/v1/system/stringService.js"; const SERVICE_NAME = "verifyJWT"; const TOKEN_PREFIX = "Bearer "; diff --git a/server/src/middleware/verifyOwnership.js b/server/src/middleware/v1/verifyOwnership.js similarity index 88% rename from server/src/middleware/verifyOwnership.js rename to server/src/middleware/v1/verifyOwnership.js index 4431b0c26..741fb6436 100755 --- a/server/src/middleware/verifyOwnership.js +++ b/server/src/middleware/v1/verifyOwnership.js @@ -1,6 +1,6 @@ -import { logger } from "../utils/logger.js"; -import ServiceRegistry from "../service/system/serviceRegistry.js"; -import StringService from "../service/system/stringService.js"; +import { logger } from "../../utils/logger.js"; +import ServiceRegistry from "../../service/v1/system/serviceRegistry.js"; +import StringService from "../../service/v1/system/stringService.js"; import { ObjectId } from "mongodb"; const SERVICE_NAME = "verifyOwnership"; diff --git a/server/src/middleware/verifySuperAdmin.js b/server/src/middleware/v1/verifySuperAdmin.js similarity index 97% rename from server/src/middleware/verifySuperAdmin.js rename to server/src/middleware/v1/verifySuperAdmin.js index a5a93eede..41ac0a3e0 100755 --- a/server/src/middleware/verifySuperAdmin.js +++ b/server/src/middleware/v1/verifySuperAdmin.js @@ -1,5 +1,5 @@ const jwt = require("jsonwebtoken"); -const logger = require("../utils/logger"); +const logger = require("../../utils/logger.js"); const SERVICE_NAME = "verifyAdmin"; const TOKEN_PREFIX = "Bearer "; import ServiceRegistry from "../service/serviceRegistry.js"; diff --git a/server/src/middleware/verifyTeamAccess.js b/server/src/middleware/v1/verifyTeamAccess.js similarity index 100% rename from server/src/middleware/verifyTeamAccess.js rename to server/src/middleware/v1/verifyTeamAccess.js diff --git a/server/src/routes/v1/authRoute.js b/server/src/routes/v1/authRoute.js index f69f94f43..d9b036b05 100755 --- a/server/src/routes/v1/authRoute.js +++ b/server/src/routes/v1/authRoute.js @@ -1,6 +1,6 @@ import { Router } from "express"; -import { verifyJWT } from "../../middleware/verifyJWT.js"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { verifyJWT } from "../../middleware/v1/verifyJWT.js"; +import { isAllowed } from "../../middleware/v1/isAllowed.js"; import multer from "multer"; const upload = multer(); diff --git a/server/src/routes/v1/checkRoute.js b/server/src/routes/v1/checkRoute.js index 673fe51e0..78f6b8a42 100755 --- a/server/src/routes/v1/checkRoute.js +++ b/server/src/routes/v1/checkRoute.js @@ -1,6 +1,6 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "../../middleware/v1/isAllowed.js"; class CheckRoutes { constructor(checkController) { diff --git a/server/src/routes/v1/diagnosticRoute.js b/server/src/routes/v1/diagnosticRoute.js index 1b7cc06be..2cf481e08 100755 --- a/server/src/routes/v1/diagnosticRoute.js +++ b/server/src/routes/v1/diagnosticRoute.js @@ -1,6 +1,6 @@ import { Router } from "express"; -import { verifyJWT } from "../../middleware/verifyJWT.js"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { verifyJWT } from "../../middleware/v1/verifyJWT.js"; +import { isAllowed } from "../../middleware/v1/isAllowed.js"; class DiagnosticRoutes { constructor(diagnosticController) { diff --git a/server/src/routes/v1/inviteRoute.js b/server/src/routes/v1/inviteRoute.js index 74fcfeb91..a47550536 100755 --- a/server/src/routes/v1/inviteRoute.js +++ b/server/src/routes/v1/inviteRoute.js @@ -1,6 +1,6 @@ import { Router } from "express"; -import { verifyJWT } from "../../middleware/verifyJWT.js"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { verifyJWT } from "../../middleware/v1/verifyJWT.js"; +import { isAllowed } from "../../middleware/v1/isAllowed.js"; class InviteRoutes { constructor(inviteController) { diff --git a/server/src/routes/v1/logRoutes.js b/server/src/routes/v1/logRoutes.js index feabe669a..43da8b75a 100755 --- a/server/src/routes/v1/logRoutes.js +++ b/server/src/routes/v1/logRoutes.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "../../middleware/v1/isAllowed.js"; class LogRoutes { constructor(logController) { this.router = Router(); diff --git a/server/src/routes/v1/maintenanceWindowRoute.js b/server/src/routes/v1/maintenanceWindowRoute.js index 622e98f88..3df040994 100755 --- a/server/src/routes/v1/maintenanceWindowRoute.js +++ b/server/src/routes/v1/maintenanceWindowRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import MaintenanceWindow from "../../db/models/MaintenanceWindow.js"; +import MaintenanceWindow from "../../db/v1/models/MaintenanceWindow.js"; class MaintenanceWindowRoutes { constructor(maintenanceWindowController) { this.router = Router(); diff --git a/server/src/routes/v1/monitorRoute.js b/server/src/routes/v1/monitorRoute.js index dde72e256..107964f6b 100755 --- a/server/src/routes/v1/monitorRoute.js +++ b/server/src/routes/v1/monitorRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "../../middleware/v1/isAllowed.js"; import multer from "multer"; import { fetchMonitorCertificate } from "../../controllers/v1/controllerUtils.js"; const upload = multer({ diff --git a/server/src/routes/v1/queueRoute.js b/server/src/routes/v1/queueRoute.js index 59e1cb744..c6cfa05da 100755 --- a/server/src/routes/v1/queueRoute.js +++ b/server/src/routes/v1/queueRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "../../middleware/v1/isAllowed.js"; class QueueRoutes { constructor(queueController) { this.router = Router(); diff --git a/server/src/routes/v1/settingsRoute.js b/server/src/routes/v1/settingsRoute.js index 76556e923..e88186af2 100755 --- a/server/src/routes/v1/settingsRoute.js +++ b/server/src/routes/v1/settingsRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "../../middleware/v1/isAllowed.js"; class SettingsRoutes { constructor(settingsController) { diff --git a/server/src/routes/v1/statusPageRoute.js b/server/src/routes/v1/statusPageRoute.js index 8300066a8..2c3b0b265 100755 --- a/server/src/routes/v1/statusPageRoute.js +++ b/server/src/routes/v1/statusPageRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { verifyJWT } from "../../middleware/verifyJWT.js"; +import { verifyJWT } from "../../middleware/v1/verifyJWT.js"; import multer from "multer"; const upload = multer(); diff --git a/server/src/service/business/checkService.js b/server/src/service/v1/business/checkService.js similarity index 100% rename from server/src/service/business/checkService.js rename to server/src/service/v1/business/checkService.js diff --git a/server/src/service/business/diagnosticService.js b/server/src/service/v1/business/diagnosticService.js similarity index 100% rename from server/src/service/business/diagnosticService.js rename to server/src/service/v1/business/diagnosticService.js diff --git a/server/src/service/business/inviteService.js b/server/src/service/v1/business/inviteService.js similarity index 100% rename from server/src/service/business/inviteService.js rename to server/src/service/v1/business/inviteService.js diff --git a/server/src/service/business/maintenanceWindowService.js b/server/src/service/v1/business/maintenanceWindowService.js similarity index 100% rename from server/src/service/business/maintenanceWindowService.js rename to server/src/service/v1/business/maintenanceWindowService.js diff --git a/server/src/service/business/monitorService.js b/server/src/service/v1/business/monitorService.js similarity index 99% rename from server/src/service/business/monitorService.js rename to server/src/service/v1/business/monitorService.js index 637870b71..061234102 100644 --- a/server/src/service/business/monitorService.js +++ b/server/src/service/v1/business/monitorService.js @@ -1,4 +1,4 @@ -import { createMonitorsBodyValidation } from "../../validation/joi.js"; +import { createMonitorsBodyValidation } from "../../../validation/joi.js"; const SERVICE_NAME = "MonitorService"; class MonitorService { diff --git a/server/src/service/business/userService.js b/server/src/service/v1/business/userService.js similarity index 100% rename from server/src/service/business/userService.js rename to server/src/service/v1/business/userService.js diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js b/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js similarity index 100% rename from server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js rename to server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js b/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js similarity index 90% rename from server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js rename to server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js index b348e2534..2833a59f5 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js +++ b/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js @@ -5,11 +5,11 @@ class SuperSimpleQueueHelper { /** * @param {{ - * db: import("../database").Database, - * logger: import("../logger").Logger, - * networkService: import("../networkService").NetworkService, - * statusService: import("../statusService").StatusService, - * notificationService: import("../notificationService").NotificationService + * db: import("../database.js").Database, + * logger: import("../logger.js").Logger, + * networkService: import("../networkService.js").NetworkService, + * statusService: import("../statusService.js").StatusService, + * notificationService: import("../notificationService.js").NotificationService * }} */ constructor({ db, logger, networkService, statusService, notificationService }) { diff --git a/server/src/service/infrastructure/bufferService.js b/server/src/service/v1/infrastructure/bufferService.js similarity index 100% rename from server/src/service/infrastructure/bufferService.js rename to server/src/service/v1/infrastructure/bufferService.js diff --git a/server/src/service/infrastructure/emailService.js b/server/src/service/v1/infrastructure/emailService.js similarity index 100% rename from server/src/service/infrastructure/emailService.js rename to server/src/service/v1/infrastructure/emailService.js diff --git a/server/src/service/infrastructure/errorService.js b/server/src/service/v1/infrastructure/errorService.js similarity index 100% rename from server/src/service/infrastructure/errorService.js rename to server/src/service/v1/infrastructure/errorService.js diff --git a/server/src/service/infrastructure/networkService.js b/server/src/service/v1/infrastructure/networkService.js similarity index 100% rename from server/src/service/infrastructure/networkService.js rename to server/src/service/v1/infrastructure/networkService.js diff --git a/server/src/service/infrastructure/notificationService.js b/server/src/service/v1/infrastructure/notificationService.js similarity index 100% rename from server/src/service/infrastructure/notificationService.js rename to server/src/service/v1/infrastructure/notificationService.js diff --git a/server/src/service/infrastructure/notificationUtils.js b/server/src/service/v1/infrastructure/notificationUtils.js similarity index 100% rename from server/src/service/infrastructure/notificationUtils.js rename to server/src/service/v1/infrastructure/notificationUtils.js diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/v1/infrastructure/statusService.js similarity index 99% rename from server/src/service/infrastructure/statusService.js rename to server/src/service/v1/infrastructure/statusService.js index 72fa359d7..248d1c910 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/v1/infrastructure/statusService.js @@ -1,4 +1,4 @@ -import MonitorStats from "../../db/models/MonitorStats.js"; +import MonitorStats from "../../../db/v1/models/MonitorStats.js"; const SERVICE_NAME = "StatusService"; class StatusService { diff --git a/server/src/service/system/serviceRegistry.js b/server/src/service/v1/system/serviceRegistry.js similarity index 100% rename from server/src/service/system/serviceRegistry.js rename to server/src/service/v1/system/serviceRegistry.js diff --git a/server/src/service/system/settingsService.js b/server/src/service/v1/system/settingsService.js similarity index 100% rename from server/src/service/system/settingsService.js rename to server/src/service/v1/system/settingsService.js diff --git a/server/src/service/system/stringService.js b/server/src/service/v1/system/stringService.js similarity index 100% rename from server/src/service/system/stringService.js rename to server/src/service/v1/system/stringService.js diff --git a/server/src/service/system/translationService.js b/server/src/service/v1/system/translationService.js similarity index 100% rename from server/src/service/system/translationService.js rename to server/src/service/v1/system/translationService.js diff --git a/server/src/service/v2/business/CheckService.ts b/server/src/service/v2/business/CheckService.ts new file mode 100644 index 000000000..181fe048d --- /dev/null +++ b/server/src/service/v2/business/CheckService.ts @@ -0,0 +1,132 @@ +import { json } from "stream/consumers"; +import { ICheck, Check, Monitor, ISystemInfo, ICaptureInfo } from "../../../db/v1/models/index.js"; +import { MonitorType } from "../../../db/v1/models/Monitor.js"; +import { StatusResponse } from "../infrastructure/NetworkService.js"; +import type { ICapturePayload, ILighthousePayload } from "../infrastructure/NetworkService.js"; +import mongoose from "mongoose"; + +export interface ICheckService { + buildCheck: (statusResponse: StatusResponse, type: MonitorType) => Promise; + cleanupOrphanedChecks: () => Promise; +} + +class CheckService implements ICheckService { + private isCapturePayload = (payload: any): payload is ICapturePayload => { + if (!payload || typeof payload !== "object") return false; + + if (!("data" in payload) || typeof payload.data !== "object") { + return false; + } + + const data = payload.data as Partial; + if (!data.cpu || typeof data.cpu !== "object" || typeof data.cpu.usage_percent !== "number") { + return false; + } + s; + + if (!data.memory || typeof data.memory !== "object" || typeof data.memory.usage_percent !== "number") { + return false; + } + + if (data.disk && !Array.isArray(data.disk)) { + return false; + } + if (data.net && !Array.isArray(data.net)) { + return false; + } + + if (!("capture" in payload) || typeof payload.capture !== "object") return false; + const capture = payload.capture as Record; + if (typeof capture.version !== "string" || typeof capture.mode !== "string") return false; + + return true; + }; + + private isPagespeedPayload = (payload: any): payload is ILighthousePayload => { + if (!payload || typeof payload !== "object") return false; + + if (!("lighthouseResult" in payload) || typeof payload.lighthouseResult !== "object") { + return false; + } + return true; + }; + + private buildBaseCheck = (statusResponse: StatusResponse) => { + const monitorId = new mongoose.Types.ObjectId(statusResponse.monitorId); + const check = new Check({ + monitorId: monitorId, + type: statusResponse?.type, + status: statusResponse?.status, + message: statusResponse?.message, + responseTime: statusResponse?.responseTime, + timings: statusResponse?.timings, + }); + return check; + }; + + private buildInfrastructureCheck = (statusResponse: StatusResponse) => { + if (!this.isCapturePayload(statusResponse.payload)) { + throw new Error("Invalid payload for infrastructure monitor"); + } + const check = this.buildBaseCheck(statusResponse); + check.system = statusResponse.payload.data; + check.capture = statusResponse.payload.capture; + return check; + }; + + private buildPagespeedCheck = (statusResponse: StatusResponse) => { + if (!this.isPagespeedPayload(statusResponse.payload)) { + throw new Error("Invalid payload for pagespeed monitor"); + } + const check = this.buildBaseCheck(statusResponse); + const lighthouseResult = statusResponse?.payload?.lighthouseResult; + check.lighthouse = { + accessibility: lighthouseResult?.categories?.accessibility?.score || 0, + bestPractices: lighthouseResult?.categories?.["best-practices"]?.score || 0, + seo: lighthouseResult?.categories?.seo?.score || 0, + performance: lighthouseResult?.categories?.performance?.score || 0, + audits: { + cls: lighthouseResult?.audits?.["cumulative-layout-shift"] || {}, + si: lighthouseResult?.audits?.["speed-index"] || {}, + fcp: lighthouseResult?.audits?.["first-contentful-paint"] || {}, + lcp: lighthouseResult?.audits?.["largest-contentful-paint"] || {}, + tbt: lighthouseResult?.audits?.["total-blocking-time"] || {}, + }, + }; + return check; + }; + + buildCheck = async (statusResponse: StatusResponse, type: MonitorType): Promise => { + switch (type) { + case "infrastructure": + return this.buildInfrastructureCheck(statusResponse as StatusResponse); + + case "pagespeed": + return this.buildPagespeedCheck(statusResponse as StatusResponse); + case "http": + case "https": + return this.buildBaseCheck(statusResponse); + + case "ping": + return this.buildBaseCheck(statusResponse); + default: + throw new Error(`Unsupported monitor type: ${type}`); + } + }; + + cleanupOrphanedChecks = async () => { + try { + const monitorIds = await Monitor.find().distinct("_id"); + const result = await Check.deleteMany({ + monitorId: { $nin: monitorIds }, + }); + console.log(`Deleted ${result.deletedCount} orphaned Checks.`); + return true; + } catch (error) { + console.error("Error cleaning up orphaned Checks:", error); + return false; + } + }; +} + +export default CheckService; diff --git a/server/src/service/v2/business/MonitorService.ts b/server/src/service/v2/business/MonitorService.ts new file mode 100644 index 000000000..6e48e79e5 --- /dev/null +++ b/server/src/service/v2/business/MonitorService.ts @@ -0,0 +1,464 @@ +import mongoose from "mongoose"; + +import { IMonitor, Monitor, ITokenizedUser, MonitorStats, Check } from "../../../db/v1/models/index.js"; +import ApiError from "../../../utils/ApiError.js"; +import { IJobQueue } from "../infrastructure/JobQueue.js"; +import { MonitorWithChecksResponse } from "../../../types/monitor-response-with-checks.js"; +import { MonitorStatus, MonitorType } from "../../../db/v1/models/monitors/Monitor.js"; +export interface IMonitorService { + create: (tokenizedUser: ITokenizedUser, monitorData: IMonitor) => Promise; + getAll: () => Promise; + getAllEmbedChecks: (page: number, limit: number, type: MonitorType[]) => Promise; + get: (monitorId: string) => Promise; + getEmbedChecks: (monitorId: string, range: string, status?: string) => Promise; + toggleActive: (monitorId: string, tokenizedUser: ITokenizedUser) => Promise; + update: (tokenizedUser: ITokenizedUser, monitorId: string, updateData: Partial) => Promise; + delete: (monitorId: string) => Promise; +} + +class MonitorService implements IMonitorService { + private jobQueue: IJobQueue; + constructor(jobQueue: IJobQueue) { + this.jobQueue = jobQueue; + } + + create = async (tokenizedUser: ITokenizedUser, monitorData: IMonitor) => { + const monitor = await Monitor.create({ + ...monitorData, + createdBy: tokenizedUser.sub, + updatedBy: tokenizedUser.sub, + }); + await MonitorStats.create({ + monitorId: monitor._id, + currentStreakStartedAt: Date.now(), + }); + await this.jobQueue.addJob(monitor); + return monitor; + }; + + getAll = async () => { + return Monitor.find(); + }; + + getAllEmbedChecks = async (page: number, limit: number, type: MonitorType[] = []) => { + const skip = (page - 1) * limit; + let find = {}; + if (type.length > 0) find = { type: { $in: type } }; + const monitors = await Monitor.find(find).skip(skip).limit(limit); + return monitors; + }; + + get = async (monitorId: string) => { + const monitor = await Monitor.findById(monitorId); + if (!monitor) { + throw new ApiError("Monitor not found", 404); + } + return monitor; + }; + + private getStartDate(range: string): Date { + const now = new Date(); + switch (range) { + case "30m": + return new Date(now.getTime() - 30 * 60 * 1000); + case "24h": + return new Date(now.getTime() - 24 * 60 * 60 * 1000); + case "7d": + return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + case "30d": + return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + default: + throw new ApiError("Invalid range parameter", 400); + } + } + + private getDateFormat(range: string): string { + switch (range) { + case "30m": + return "%Y-%m-%dT%H:%M:00Z"; + case "24h": + case "7d": + return "%Y-%m-%dT%H:00:00Z"; + case "30d": + return "%Y-%m-%d"; + default: + throw new ApiError("Invalid range parameter", 400); + } + } + + private getBaseGroup = (dateFormat: string): Record => { + return { + _id: { $dateToString: { format: dateFormat, date: "$createdAt" } }, + count: { $sum: 1 }, + avgResponseTime: { $avg: "$responseTime" }, + }; + }; + + private getBaseProjection = (): object => { + return { status: 1, responseTime: 1, createdAt: 1 }; + }; + + private getPageSpeedGroup = (dateFormat: string): Record => { + return { + _id: { $dateToString: { format: dateFormat, date: "$createdAt" } }, + count: { $sum: 1 }, + avgResponseTime: { $avg: "$responseTime" }, + accessibility: { $avg: "$lighthouse.accessibility" }, + bestPractices: { $avg: "$lighthouse.bestPractices" }, + seo: { $avg: "$lighthouse.seo" }, + performance: { $avg: "$lighthouse.performance" }, + cls: { $avg: "$lighthouse.audits.cls.score" }, + si: { $avg: "$lighthouse.audits.si.score" }, + fcp: { $avg: "$lighthouse.audits.fcp.score" }, + lcp: { $avg: "$lighthouse.audits.lcp.score" }, + tbt: { $avg: "$lighthouse.audits.tbt.score" }, + }; + }; + + private getPageSpeedProjection = (): object => { + const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 }; + projectStage["lighthouse.accessibility"] = 1; + projectStage["lighthouse.seo"] = 1; + projectStage["lighthouse.bestPractices"] = 1; + projectStage["lighthouse.performance"] = 1; + projectStage["lighthouse.audits.cls.score"] = 1; + projectStage["lighthouse.audits.si.score"] = 1; + projectStage["lighthouse.audits.fcp.score"] = 1; + projectStage["lighthouse.audits.lcp.score"] = 1; + projectStage["lighthouse.audits.tbt.score"] = 1; + return projectStage; + }; + + private getInfraGroup = (dateFormat: string): Record => { + return { + _id: { $dateToString: { format: dateFormat, date: "$createdAt" } }, + count: { $sum: 1 }, + avgResponseTime: { $avg: "$responseTime" }, + physicalCores: { $last: "$system.cpu.physical_core" }, + logicalCores: { $last: "$system.cpu.logical_core" }, + frequency: { $avg: "$system.cpu.frequency" }, + currentFrequency: { $last: "$system.cpu.current_frequency" }, + tempsArrays: { $push: "$system.cpu.temperature" }, + freePercent: { $avg: "$system.cpu.free_percent" }, + usedPercent: { $avg: "$system.cpu.usage_percent" }, + total_bytes: { $last: "$system.memory.total_bytes" }, + available_bytes: { $last: "$system.memory.available_bytes" }, + used_bytes: { $last: "$system.memory.used_bytes" }, + memory_usage_percent: { $avg: "$system.memory.usage_percent" }, + disksArray: { $push: "$system.disk" }, + os: { $last: "$system.host.os" }, + platform: { $last: "$system.host.platform" }, + kernel_version: { $last: "$system.host.kernel_version" }, + pretty_name: { $last: "$system.host.pretty_name" }, + netsArray: { $push: "$system.net" }, + }; + }; + + private getInfraProjection = (): object => { + const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 }; + projectStage["system.cpu.physical_core"] = 1; + projectStage["system.cpu.logical_core"] = 1; + projectStage["system.cpu.frequency"] = 1; + projectStage["system.cpu.current_frequency"] = 1; + projectStage["system.cpu.temperature"] = 1; + projectStage["system.cpu.free_percent"] = 1; + projectStage["system.cpu.usage_percent"] = 1; + projectStage["system.memory.total_bytes"] = 1; + projectStage["system.memory.available_bytes"] = 1; + projectStage["system.memory.used_bytes"] = 1; + projectStage["system.memory.usage_percent"] = 1; + projectStage["system.disk"] = 1; + projectStage["system.host.os"] = 1; + projectStage["system.host.platform"] = 1; + projectStage["system.host.kernel_version"] = 1; + projectStage["system.host.pretty_name"] = 1; + projectStage["system.net"] = 1; + return projectStage; + }; + + private getFinalProjection = (type: string): object => { + if (type === "pagespeed") { + return { + _id: 1, + count: 1, + avgResponseTime: 1, + accessibility: "$accessibility", + seo: "$seo", + bestPractices: "$bestPractices", + performance: "$performance", + cls: "$cls", + si: "$si", + fcp: "$fcp", + lcp: "$lcp", + tbt: "$tbt", + }; + } + + if (type === "infrastructure") { + return { + _id: 1, + count: 1, + avgResponseTime: 1, + cpu: { + physicalCores: "$physicalCores", + logicalCores: "$logicalCores", + frequency: "$frequency", + currentFrequency: "$currentFrequency", + temperatures: { + $map: { + input: { + $range: [0, { $size: { $arrayElemAt: ["$tempsArrays", 0] } }], + }, + as: "idx", + in: { + $avg: { + $map: { + input: "$tempsArrays", + as: "arr", + in: { $arrayElemAt: ["$$arr", "$$idx"] }, + }, + }, + }, + }, + }, + freePercent: "$freePercent", + usedPercent: "$usedPercent", + }, + memory: { + total_bytes: "$total_bytes", + available_bytes: "$available_bytes", + used_bytes: "$used_bytes", + usage_percent: "$memory_usage_percent", + }, + disks: { + $map: { + input: { + $range: [0, { $size: { $arrayElemAt: ["$disksArray", 0] } }], + }, + as: "idx", + in: { + $let: { + vars: { + diskGroup: { + $map: { + input: "$disksArray", + as: "diskArr", + in: { $arrayElemAt: ["$$diskArr", "$$idx"] }, + }, + }, + }, + in: { + device: { $arrayElemAt: ["$$diskGroup.device", 0] }, + total_bytes: { $avg: "$$diskGroup.total_bytes" }, + free_bytes: { $avg: "$$diskGroup.free_bytes" }, + used_bytes: { $avg: "$$diskGroup.used_bytes" }, + usage_percent: { $avg: "$$diskGroup.usage_percent" }, + total_inodes: { $avg: "$$diskGroup.total_inodes" }, + free_inodes: { $avg: "$$diskGroup.free_inodes" }, + used_inodes: { $avg: "$$diskGroup.used_inodes" }, + inodes_usage_percent: { + $avg: "$$diskGroup.inodes_usage_percent", + }, + read_bytes: { $avg: "$$diskGroup.read_bytes" }, + write_bytes: { $avg: "$$diskGroup.write_bytes" }, + read_time: { $avg: "$$diskGroup.read_time" }, + write_time: { $avg: "$$diskGroup.write_time" }, + }, + }, + }, + }, + }, + host: { + os: "$os", + platform: "$platform", + kernel_version: "$kernel_version", + pretty_name: "$pretty_name", + }, + net: { + $map: { + input: { + $range: [0, { $size: { $arrayElemAt: ["$netsArray", 0] } }], + }, + as: "idx", + in: { + $let: { + vars: { + netGroup: { + $map: { + input: "$netsArray", + as: "netArr", + in: { $arrayElemAt: ["$$netArr", "$$idx"] }, + }, + }, + }, + in: { + name: { $arrayElemAt: ["$$netGroup.name", 0] }, + bytes_sent: { $avg: "$$netGroup.bytes_sent" }, + bytes_recv: { $avg: "$$netGroup.bytes_recv" }, + packets_sent: { $avg: "$$netGroup.packets_sent" }, + packets_recv: { $avg: "$$netGroup.packets_recv" }, + err_in: { $avg: "$$netGroup.err_in" }, + err_out: { $avg: "$$netGroup.err_out" }, + drop_in: { $avg: "$$netGroup.drop_in" }, + drop_out: { $avg: "$$netGroup.drop_out" }, + fifo_in: { $avg: "$$netGroup.fifo_in" }, + fifo_out: { $avg: "$$netGroup.fifo_out" }, + }, + }, + }, + }, + }, + }; + } + return {}; + }; + + getEmbedChecks = async (monitorId: string, range: string, status: string | undefined): Promise => { + const monitor = await Monitor.findById(monitorId); + if (!monitor) { + throw new ApiError("Monitor not found", 404); + } + const startDate = this.getStartDate(range); + const dateFormat = this.getDateFormat(range); + + // Build match stage + const matchStage: { + monitorId: mongoose.Types.ObjectId; + createdAt: { $gte: Date }; + status?: string; + } = { + monitorId: monitor._id, + createdAt: { $gte: startDate }, + }; + + if (status) { + matchStage.status = status; + } + + let groupClause; + + if (monitor.type === "pagespeed") { + groupClause = this.getPageSpeedGroup(dateFormat); + } else if (monitor.type === "infrastructure") { + groupClause = this.getInfraGroup(dateFormat); + } else { + groupClause = this.getBaseGroup(dateFormat); + } + + let projectStage; + if (monitor.type === "pagespeed") { + projectStage = this.getPageSpeedProjection(); + } else if (monitor.type === "infrastructure") { + projectStage = this.getInfraProjection(); + } else { + projectStage = this.getBaseProjection(); + } + + let finalProjection = {}; + if (monitor.type === "pagespeed" || monitor.type === "infrastructure") { + finalProjection = this.getFinalProjection(monitor.type); + } else { + finalProjection = { _id: 1, count: 1, avgResponseTime: 1 }; + } + + const checks = await Check.aggregate([ + { + $match: matchStage, + }, + { $sort: { createdAt: 1 } }, + { $project: projectStage }, + { $group: groupClause }, + { $sort: { _id: -1 } }, + { + $project: finalProjection, + }, + ]); + + // Get monitor stats + const monitorStats = await MonitorStats.findOne({ + monitorId: monitor._id, + }).lean(); + + if (!monitorStats) { + throw new ApiError("Monitor stats not found", 404); + } + + return { + monitor: monitor.toObject(), + checks, + stats: monitorStats, + }; + }; + + async toggleActive(id: string, tokenizedUser: ITokenizedUser) { + const pendingStatus: MonitorStatus = "initializing"; + const updatedMonitor = await Monitor.findOneAndUpdate( + { _id: id }, + [ + { + $set: { + isActive: { $not: "$isActive" }, + status: pendingStatus, + updatedBy: tokenizedUser.sub, + updatedAt: new Date(), + }, + }, + ], + { new: true } + ); + + if (!updatedMonitor) { + throw new ApiError("Monitor not found", 404); + } + + await this.jobQueue.updateJob(updatedMonitor); + + if (updatedMonitor?.isActive) { + await this.jobQueue.resumeJob(updatedMonitor); + } else { + await this.jobQueue.pauseJob(updatedMonitor); + } + return updatedMonitor; + } + + async update(tokenizedUser: ITokenizedUser, monitorId: string, updateData: Partial) { + const allowedFields: (keyof IMonitor)[] = ["name", "interval", "isActive", "n", "m", "notificationChannels"]; + const safeUpdate: Partial = {}; + + for (const field of allowedFields) { + if (updateData[field] !== undefined) { + (safeUpdate as any)[field] = updateData[field]; + } + } + + const updatedMonitor = await Monitor.findByIdAndUpdate( + monitorId, + { + $set: { + ...safeUpdate, + updatedAt: new Date(), + updatedBy: tokenizedUser.sub, + }, + }, + { new: true, runValidators: true } + ); + + if (!updatedMonitor) { + throw new ApiError("Monitor not found", 404); + } + await this.jobQueue.updateJob(updatedMonitor); + return updatedMonitor; + } + + async delete(monitorId: string) { + const monitor = await Monitor.findById(monitorId); + if (!monitor) { + throw new ApiError("Monitor not found", 404); + } + await monitor.deleteOne(); + await this.jobQueue.deleteJob(monitor); + return true; + } +} + +export default MonitorService; From 059408f098a9307884cc34c7bc4d9d29296b7a7b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 23 Sep 2025 14:40:57 -0700 Subject: [PATCH 012/400] add v2 models --- server/src/db/v2/models/auth/Role.ts | 50 +++ server/src/db/v2/models/auth/User.ts | 108 +++++++ server/src/db/v2/models/checks/Check.ts | 297 ++++++++++++++++++ server/src/db/v2/models/index.ts | 33 ++ server/src/db/v2/models/invite/Invite.ts | 43 +++ .../db/v2/models/maintenance/Maintenance.ts | 39 +++ server/src/db/v2/models/monitors/Monitor.ts | 100 ++++++ .../src/db/v2/models/monitors/MonitorStats.ts | 77 +++++ .../NotificationChannel.ts | 50 +++ 9 files changed, 797 insertions(+) create mode 100644 server/src/db/v2/models/auth/Role.ts create mode 100644 server/src/db/v2/models/auth/User.ts create mode 100644 server/src/db/v2/models/checks/Check.ts create mode 100644 server/src/db/v2/models/index.ts create mode 100644 server/src/db/v2/models/invite/Invite.ts create mode 100644 server/src/db/v2/models/maintenance/Maintenance.ts create mode 100644 server/src/db/v2/models/monitors/Monitor.ts create mode 100644 server/src/db/v2/models/monitors/MonitorStats.ts create mode 100644 server/src/db/v2/models/notification-channel/NotificationChannel.ts diff --git a/server/src/db/v2/models/auth/Role.ts b/server/src/db/v2/models/auth/Role.ts new file mode 100644 index 000000000..1031de8b2 --- /dev/null +++ b/server/src/db/v2/models/auth/Role.ts @@ -0,0 +1,50 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; + +export interface IRole extends Document { + _id: Types.ObjectId; + name: string; + description?: string; + permissions: string[]; + isSystem: boolean; + isActive: boolean; + createdAt: Date; + updatedAt: Date; +} + +const roleSchema = new Schema( + { + name: { + type: String, + required: true, + trim: true, + maxlength: 50, + }, + description: { + type: String, + trim: true, + maxlength: 200, + }, + + permissions: [ + { + type: String, + required: true, + }, + ], + isSystem: { + type: Boolean, + default: false, + }, + isActive: { + type: Boolean, + default: true, + }, + }, + { + timestamps: true, + } +); + +roleSchema.index({ name: 1 }, { unique: true }); + +export const Role = mongoose.model("Role_v2", roleSchema); diff --git a/server/src/db/v2/models/auth/User.ts b/server/src/db/v2/models/auth/User.ts new file mode 100644 index 000000000..b72a29e70 --- /dev/null +++ b/server/src/db/v2/models/auth/User.ts @@ -0,0 +1,108 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; + +export interface ITokenizedUser { + sub: string; + roles: string[]; +} +export interface IUser extends Document { + _id: Types.ObjectId; + email: string; + firstName: string; + lastName: string; + passwordHash: string; + roles: Types.ObjectId[]; + profile: { + avatar?: string; + bio?: string; + timezone: string; + locale: string; + }; + preferences: { + theme: "light" | "dark" | "system"; + }; + lastLoginAt?: Date; + isActive: boolean; + isVerified: boolean; + createdAt: Date; + updatedAt: Date; +} + +const userSchema = new Schema( + { + email: { + type: String, + required: true, + unique: true, + trim: true, + lowercase: true, + }, + firstName: { + type: String, + required: true, + trim: true, + maxlength: 50, + }, + lastName: { + type: String, + required: true, + trim: true, + maxlength: 50, + }, + passwordHash: { + type: String, + required: true, + }, + roles: [ + { + type: Schema.Types.ObjectId, + ref: "Role_v2", + }, + ], + profile: { + avatar: { + type: String, + }, + bio: { + type: String, + maxlength: 200, + }, + timezone: { + type: String, + default: "UTC", + }, + locale: { + type: String, + default: "en", + }, + }, + preferences: { + theme: { + type: String, + enum: ["light", "dark", "system"], + default: "system", + }, + }, + lastLoginAt: { + type: Date, + }, + isActive: { + type: Boolean, + default: true, + }, + isVerified: { + type: Boolean, + default: false, + }, + }, + { + timestamps: true, + toJSON: { + transform: function (_doc, ret) { + delete ret.passwordHash; + return ret; + }, + }, + } +); + +export const User = mongoose.model("User_v2", userSchema); diff --git a/server/src/db/v2/models/checks/Check.ts b/server/src/db/v2/models/checks/Check.ts new file mode 100644 index 000000000..83bbc9f91 --- /dev/null +++ b/server/src/db/v2/models/checks/Check.ts @@ -0,0 +1,297 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; +import { MonitorType, MonitorTypes, MonitorStatus, MonitorStatuses } from "../monitors/Monitor.js"; +import type { Response } from "got"; +export type GotTimings = Response["timings"]; +export interface ITimingPhases { + wait: number; + dns: number; + tcp: number; + tls: number; + request: number; + firstByte: number; + download: number; + total: number; +} + +export interface ICpuInfo { + physical_core: number; + logical_core: number; + frequency: number; + current_frequency: number; + temperature: number[]; // per-core temps + free_percent: number; + usage_percent: number; +} + +export interface IMemoryInfo { + total_bytes: number; + available_bytes: number; + used_bytes: number; + usage_percent: number; +} + +export interface IHostInfo { + os?: string; + platform?: string; + kernel_version?: string; + pretty_name?: string; +} + +export interface IDiskInfo { + device: string; + total_bytes: number; + free_bytes: number; + used_bytes: number; + usage_percent: number; + total_inodes?: number; + free_inodes?: number; + used_inodes?: number; + inodes_usage_percent?: number; + read_bytes?: number; + write_bytes?: number; + read_time?: number; + write_time?: number; +} + +export interface INetInfo { + name: string; + bytes_sent: number; + bytes_recv: number; + packets_sent: number; + packets_recv: number; + err_in: number; + err_out: number; + drop_in: number; + drop_out: number; + fifo_in: number; + fifo_out: number; +} + +export interface ICaptureInfo { + version?: string; + mode?: string; +} + +export interface ISystemInfo { + cpu: ICpuInfo; + memory: IMemoryInfo; + disk: IDiskInfo[]; + host: IHostInfo; + net: INetInfo[]; +} + +export interface ILighthouseAudit { + id?: string; + title?: string; + score?: number | null; + displayValue?: string; + numericValue?: number; + numericUnit?: string; +} +export interface ILighthouseCategories { + accessibility?: { score?: number | null }; + "best-practices"?: { score?: number | null }; + seo?: { score?: number | null }; + performance?: { score?: number | null }; +} + +export interface ILighthouseResult { + categories?: ILighthouseCategories; + audits?: Record; +} + +export interface ICheckLighthouseFields { + accessibility: number; + bestPractices: number; + seo: number; + performance: number; + audits: { + cls: ILighthouseAudit; + si: ILighthouseAudit; + fcp: ILighthouseAudit; + lcp: ILighthouseAudit; + tbt: ILighthouseAudit; + }; +} + +export interface ICheck extends Document { + _id: Types.ObjectId; + monitorId: Types.ObjectId; + type: MonitorType; + status: MonitorStatus; + message: string; + responseTime?: number; // in ms + timings?: GotTimings; + httpStatusCode?: number; + errorMessage?: string; + ack: boolean; + ackAt?: Date; + ackBy?: Types.ObjectId; + expiry: Date; + createdAt: Date; + updatedAt: Date; + system?: ISystemInfo; + capture?: ICaptureInfo; + lighthouse?: ICheckLighthouseFields; +} + +const CheckSchema = new Schema( + { + monitorId: { type: Schema.Types.ObjectId, ref: "Monitor_v2", required: true }, + + type: { + type: String, + required: true, + enum: MonitorTypes, + }, + status: { + type: String, + required: true, + enum: MonitorStatuses, + }, + message: { type: String, trim: true }, + responseTime: { type: Number }, + timings: { + start: { type: Date }, + socket: { type: Date }, + lookup: { type: Date }, + connect: { type: Date }, + secureConnect: { type: Date }, + response: { type: Date }, + end: { type: Date }, + phases: { + wait: { type: Number }, + dns: { type: Number }, + tcp: { type: Number }, + tls: { type: Number }, + request: { type: Number }, + firstByte: { type: Number }, + download: { type: Number }, + total: { type: Number }, + }, + }, + system: { + type: { + cpu: { + physical_core: { type: Number }, + logical_core: { type: Number }, + frequency: { type: Number }, + current_frequency: { type: Number }, + temperature: [{ type: Number }], + free_percent: { type: Number }, + usage_percent: { type: Number }, + }, + memory: { + total_bytes: { type: Number }, + available_bytes: { type: Number }, + used_bytes: { type: Number }, + usage_percent: { type: Number }, + }, + disk: [ + { + device: { type: String }, + total_bytes: { type: Number }, + free_bytes: { type: Number }, + used_bytes: { type: Number }, + usage_percent: { type: Number }, + total_inodes: { type: Number }, + free_inodes: { type: Number }, + used_inodes: { type: Number }, + inodes_usage_percent: { type: Number }, + read_bytes: { type: Number }, + write_bytes: { type: Number }, + read_time: { type: Number }, + write_time: { type: Number }, + }, + ], + host: { + os: { type: String }, + platform: { type: String }, + kernel_version: { type: String }, + pretty_name: { type: String }, + }, + net: [ + { + name: { type: String }, + bytes_sent: { type: Number }, + bytes_recv: { type: Number }, + packets_sent: { type: Number }, + packets_recv: { type: Number }, + err_in: { type: Number }, + err_out: { type: Number }, + drop_in: { type: Number }, + drop_out: { type: Number }, + fifo_in: { type: Number }, + fifo_out: { type: Number }, + }, + ], + }, + required: false, + }, + + capture: { + type: { + version: { type: String }, + mode: { type: String }, + }, + required: false, + }, + lighthouse: { + accessibility: { type: Number, required: false }, + bestPractices: { type: Number, required: false }, + seo: { type: Number, required: false }, + performance: { type: Number, required: false }, + audits: { + cls: { + type: Object, + }, + si: { + type: Object, + }, + fcp: { + type: Object, + }, + lcp: { + type: Object, + }, + tbt: { + type: Object, + }, + }, + type: { + accessibility: { type: Number }, + bestPractices: { type: Number }, + seo: { type: Number }, + performance: { type: Number }, + audits: { + cls: { type: Object }, + si: { type: Object }, + fcp: { type: Object }, + lcp: { type: Object }, + tbt: { type: Object }, + }, + }, + required: false, + }, + + httpStatusCode: { type: Number }, + errorMessage: { type: String, trim: true }, + ack: { type: Boolean, required: true, default: false }, + ackAt: { type: Date }, + ackBy: { type: Schema.Types.ObjectId, ref: "User_v2" }, + expiry: { + type: Date, + default: Date.now, + expires: 60 * 60 * 24 * 30, + }, + }, + { timestamps: true } +); + +CheckSchema.index({ monitorId: 1, createdAt: -1 }); +CheckSchema.index({ status: 1 }); +CheckSchema.index({ status: 1, ack: 1 }); +CheckSchema.index({ ack: 1, ackAt: 1 }); +CheckSchema.index({ createdAt: -1 }); + +export const Check = mongoose.model("Check_v2", CheckSchema); diff --git a/server/src/db/v2/models/index.ts b/server/src/db/v2/models/index.ts new file mode 100644 index 000000000..fbc05c10f --- /dev/null +++ b/server/src/db/v2/models/index.ts @@ -0,0 +1,33 @@ +export { User } from "./auth/User.js"; +export type { IUser } from "./auth/User.js"; +export type { ITokenizedUser } from "./auth/User.js"; +export { Role } from "./auth/Role.js"; +export type { IRole } from "./auth/Role.js"; +export { connectDatabase, disconnectDatabase } from "../index.js"; +export { Monitor } from "./monitors/Monitor.js"; +export { MonitorStatuses } from "./monitors/Monitor.js"; +export type { IMonitor } from "./monitors/Monitor.js"; +export { Check } from "./checks/Check.js"; +export type { + ICheck, + ISystemInfo, + ICaptureInfo, + INetInfo, + IDiskInfo, + IHostInfo, + IMemoryInfo, + ICpuInfo, + ILighthouseAudit, + ITimingPhases, + ILighthouseCategories, + ILighthouseResult, + ICheckLighthouseFields, +} from "./checks/Check.js"; +export type { IMonitorStats } from "./monitors/MonitorStats.js"; +export { MonitorStats } from "./monitors/MonitorStats.js"; +export type { INotificationChannel } from "./notification-channel/NotificationChannel.js"; +export { NotificationChannel } from "./notification-channel/NotificationChannel.js"; +export type { IMaintenance } from "./maintenance/Maintenance.js"; +export { Maintenance } from "./maintenance/Maintenance.js"; +export type { IInvite } from "./invite/Invite.js"; +export { Invite } from "./invite/Invite.js"; diff --git a/server/src/db/v2/models/invite/Invite.ts b/server/src/db/v2/models/invite/Invite.ts new file mode 100644 index 000000000..2bc569900 --- /dev/null +++ b/server/src/db/v2/models/invite/Invite.ts @@ -0,0 +1,43 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; + +export interface IInvite extends Document { + _id: Types.ObjectId; + email: string; + tokenHash: string; + roles: Types.ObjectId[]; + createdBy: Types.ObjectId; + updatedBy: Types.ObjectId; + expiry: Date; + createdAt: Date; + updatedAt: Date; +} + +const InviteSchema = new Schema( + { + email: { + type: String, + required: true, + unique: true, + trim: true, + lowercase: true, + }, + roles: [ + { + type: Schema.Types.ObjectId, + ref: "Role_v2", + required: true, + }, + ], + tokenHash: { type: String, required: true, unique: true }, + expiry: { + type: Date, + default: Date.now, + expires: 60 * 60 * 24, + }, + createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, + updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, + }, + { timestamps: true } +); + +export const Invite = mongoose.model("Invite_v2", InviteSchema); diff --git a/server/src/db/v2/models/maintenance/Maintenance.ts b/server/src/db/v2/models/maintenance/Maintenance.ts new file mode 100644 index 000000000..c2ce33a2b --- /dev/null +++ b/server/src/db/v2/models/maintenance/Maintenance.ts @@ -0,0 +1,39 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; + +export interface IMaintenance extends Document { + _id: Types.ObjectId; + name: string; + isActive: boolean; + monitors: Types.ObjectId[]; + startTime: Date; + endTime: Date; + createdBy: Types.ObjectId; + updatedBy: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const MaintenanceSchema = new Schema( + { + name: { type: String, required: true, trim: true }, + isActive: { type: Boolean, required: true, default: true }, + monitors: [ + { + type: Schema.Types.ObjectId, + ref: "Monitor_v2", + required: true, + }, + ], + startTime: { type: Date, required: true }, + endTime: { type: Date, required: true }, + createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, + updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, + }, + { timestamps: true } +); + +MaintenanceSchema.index({ isActive: 1 }); +MaintenanceSchema.index({ startTime: 1 }); +MaintenanceSchema.index({ endTime: 1 }); + +export const Maintenance = mongoose.model("Maintenance_v2", MaintenanceSchema); diff --git a/server/src/db/v2/models/monitors/Monitor.ts b/server/src/db/v2/models/monitors/Monitor.ts new file mode 100644 index 000000000..055d36a1b --- /dev/null +++ b/server/src/db/v2/models/monitors/Monitor.ts @@ -0,0 +1,100 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; +import { Check, MonitorStats } from "../index.js"; + +export const MonitorTypes = ["http", "https", "ping", "infrastructure", "pagespeed"] as const; +export type MonitorType = (typeof MonitorTypes)[number]; + +export const MonitorStatuses = ["up", "down", "paused", "initializing"] as const; +export type MonitorStatus = (typeof MonitorStatuses)[number]; +export interface IMonitor extends Document { + _id: Types.ObjectId; + name: string; + url: string; + secret?: string; + type: MonitorType; + interval: number; // in ms + isActive: boolean; + status: MonitorStatus; + n: number; // Number of consecutive successes required to change status + lastCheckedAt?: Date; + latestChecks: { + status: MonitorStatus; + responseTime: number; + checkedAt: Date; + }[]; + notificationChannels?: Types.ObjectId[]; + createdBy: Types.ObjectId; + updatedBy: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const MonitorSchema = new Schema( + { + name: { type: String, required: true, trim: true, maxlength: 100 }, + url: { type: String, required: true, trim: true }, + secret: { type: String, required: false }, + type: { + type: String, + required: true, + enum: MonitorTypes, + }, + interval: { type: Number, required: true, default: 60000 }, + isActive: { type: Boolean, required: true, default: true }, + status: { + type: String, + required: true, + default: "initializing", + enum: MonitorStatuses, + }, + n: { type: Number, required: true, default: 1 }, + lastCheckedAt: { type: Date }, + latestChecks: { + type: [ + { + status: { + type: String, + required: true, + enum: MonitorStatuses, + }, + responseTime: { type: Number, required: true }, + checkedAt: { type: Date, required: true }, + }, + ], + default: [], + }, + notificationChannels: [ + { + type: Schema.Types.ObjectId, + ref: "NotificationChannel_v2", + required: false, + default: [], + }, + ], + + createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, + updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, + }, + { timestamps: true } +); + +MonitorSchema.pre("deleteOne", { document: true, query: false }, async function (next) { + try { + const monitorId = this._id; + await Check.deleteMany({ monitorId }); + await MonitorStats.deleteMany({ monitorId }); + next(); + } catch (error: any) { + next(error); + } +}); + +MonitorSchema.index({ isActive: 1 }); +MonitorSchema.index({ status: 1 }); +MonitorSchema.index({ type: 1 }); +MonitorSchema.index({ lastCheckedAt: 1 }); +MonitorSchema.index({ isActive: 1, status: 1 }); +MonitorSchema.index({ createdBy: 1 }); +MonitorSchema.index({ updatedBy: 1 }); + +export const Monitor = mongoose.model("Monitor_v2", MonitorSchema); diff --git a/server/src/db/v2/models/monitors/MonitorStats.ts b/server/src/db/v2/models/monitors/MonitorStats.ts new file mode 100644 index 000000000..56e76f9ab --- /dev/null +++ b/server/src/db/v2/models/monitors/MonitorStats.ts @@ -0,0 +1,77 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; +import { MonitorStatus, MonitorStatuses } from "./Monitor.js"; + +export interface IMonitorStats extends mongoose.Document { + monitorId: mongoose.Types.ObjectId; + avgResponseTime: number; + maxResponseTime: number; + totalChecks: number; + totalUpChecks: number; + totalDownChecks: number; + uptimePercentage: number; + lastCheckTimestamp: number; + lastResponseTime: number; + timeOfLastFailure: number; + currentStreak: number; + currentStreakStatus: MonitorStatus; + currentStreakStartedAt: number; + createdAt: Date; + updatedAt: Date; +} + +const MonitorStatsSchema = new Schema( + { + monitorId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Monitor_v2", + immutable: true, + index: true, + }, + avgResponseTime: { + type: Number, + default: 0, + }, + maxResponseTime: { + type: Number, + default: 0, + }, + lastResponseTime: { + type: Number, + default: 0, + }, + totalChecks: { + type: Number, + default: 0, + }, + totalUpChecks: { + type: Number, + default: 0, + }, + totalDownChecks: { + type: Number, + default: 0, + }, + uptimePercentage: { + type: Number, + default: 0, + }, + lastCheckTimestamp: { + type: Number, + default: 0, + }, + timeOfLastFailure: { + type: Number, + default: 0, + }, + currentStreak: { type: Number, required: false, default: 0 }, + currentStreakStatus: { + type: String, + required: false, + enum: MonitorStatuses, + }, + currentStreakStartedAt: { type: Number, required: false }, + }, + { timestamps: true } +); + +export const MonitorStats = mongoose.model("MonitorStats_v2", MonitorStatsSchema); diff --git a/server/src/db/v2/models/notification-channel/NotificationChannel.ts b/server/src/db/v2/models/notification-channel/NotificationChannel.ts new file mode 100644 index 000000000..e77639608 --- /dev/null +++ b/server/src/db/v2/models/notification-channel/NotificationChannel.ts @@ -0,0 +1,50 @@ +import mongoose, { Schema, Document, Types } from "mongoose"; +export const ChannelTypes = ["email", "slack", "discord", "webhook"] as const; +export type ChannelType = (typeof ChannelTypes)[number]; + +export interface INotificationChannelConfig { + url?: string; // For webhook, slack, discord + emailAddress?: string; // For email +} + +export interface INotificationChannel { + _id: Types.ObjectId; + name: string; + type: ChannelType; + config: INotificationChannelConfig; + isActive: boolean; + createdBy: Types.ObjectId; + updatedBy: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const NotificationChannelConfigSchema = new Schema( + { + url: { type: String, required: false }, + emailAddress: { type: String, required: false }, + }, + { _id: false, strict: "throw" } +); + +const NotificationChannelSchema = new Schema( + { + name: { type: String, required: true, trim: true }, + type: { + type: String, + required: true, + enum: ChannelTypes, + }, + config: { type: NotificationChannelConfigSchema, required: true }, + isActive: { type: Boolean, required: true, default: true }, + createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, + updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, + }, + { timestamps: true } +); + +NotificationChannelSchema.index({ isActive: 1 }); +NotificationChannelSchema.index({ type: 1 }); +NotificationChannelSchema.index({ type: 1, isActive: 1 }); + +export const NotificationChannel = mongoose.model("NotificationChannel_v2", NotificationChannelSchema); From c0ff162ed925de8f70280ce40e043a4dfea50915 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 23 Sep 2025 14:46:32 -0700 Subject: [PATCH 013/400] remove v2 files --- .../src/controllers/v2/MonitorController.ts | 157 ------ server/src/db/v2/index.ts | 25 + server/src/db/v2/models/auth/User.ts | 6 - .../src/service/v2/business/CheckService.ts | 132 ----- .../src/service/v2/business/MonitorService.ts | 464 ------------------ 5 files changed, 25 insertions(+), 759 deletions(-) delete mode 100644 server/src/controllers/v2/MonitorController.ts create mode 100644 server/src/db/v2/index.ts delete mode 100644 server/src/service/v2/business/CheckService.ts delete mode 100644 server/src/service/v2/business/MonitorService.ts diff --git a/server/src/controllers/v2/MonitorController.ts b/server/src/controllers/v2/MonitorController.ts deleted file mode 100644 index 6493b7f44..000000000 --- a/server/src/controllers/v2/MonitorController.ts +++ /dev/null @@ -1,157 +0,0 @@ -import { Request, Response, NextFunction } from "express"; -import ApiError from "../../utils/ApiError.js"; -import MonitorService from "../../service/v2/business/MonitorService.js"; -import { MonitorType } from "../../db/v1/models/Monitor.js"; -class MonitorController { - private monitorService: MonitorService; - constructor(monitorService: MonitorService) { - this.monitorService = monitorService; - } - - create = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - - const monitor = await this.monitorService.create(tokenizedUser, req.body); - res.status(201).json({ - message: "Monitor created successfully", - data: monitor, - }); - } catch (error) { - next(error); - } - }; - - get = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - - const id = req.params.id; - if (!id) { - throw new ApiError("Monitor ID is required", 400); - } - - const range = req.query.range; - if (!range || typeof range !== "string") throw new ApiError("Range query parameter is required", 400); - - let monitor; - - const status = req.query.status; - if (status && typeof status !== "string") { - throw new ApiError("Status query parameter must be a string", 400); - } - - if (req.query.embedChecks === "true") { - monitor = await this.monitorService.getEmbedChecks(id, range, status); - } else { - monitor = await this.monitorService.get(id); - } - - res.status(200).json({ - message: "Monitor retrieved successfully", - data: monitor, - }); - } catch (error) { - next(error); - } - }; - - getAll = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - - let monitors; - if (req.query.embedChecks === "true") { - const page = Math.max(1, Number(req.query.page) || 1); - const limit = Math.max(1, Number(req.query.limit) || 10); - const type: MonitorType[] = req.query.type as MonitorType[]; - - monitors = await this.monitorService.getAllEmbedChecks(page, limit, type); - } else { - monitors = await this.monitorService.getAll(); - } - - res.status(200).json({ - message: "Monitors retrieved successfully", - data: monitors, - }); - } catch (error) { - next(error); - } - }; - - toggleActive = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - - const id = req.params.id; - if (!id) { - throw new ApiError("Monitor ID is required", 400); - } - - const monitor = await this.monitorService.toggleActive(id, tokenizedUser); - res.status(200).json({ - message: "Monitor paused/unpaused successfully", - data: monitor, - }); - } catch (error) { - next(error); - } - }; - - update = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - - const id = req.params.id; - if (!id) { - throw new ApiError("Monitor ID is required", 400); - } - - const monitor = await this.monitorService.update(tokenizedUser, id, req.body); - res.status(200).json({ - message: "Monitor updated successfully", - data: monitor, - }); - } catch (error) { - next(error); - } - }; - - delete = async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenizedUser = req.user; - if (!tokenizedUser) { - return res.status(401).json({ message: "Unauthorized" }); - } - const id = req.params.id; - if (!id) { - throw new ApiError("Monitor ID is required", 400); - } - await this.monitorService.delete(id); - - res.status(200).json({ - message: "Monitor deleted successfully", - }); - } catch (error) { - next(error); - } - }; -} - -export default MonitorController; diff --git a/server/src/db/v2/index.ts b/server/src/db/v2/index.ts new file mode 100644 index 000000000..2cd194283 --- /dev/null +++ b/server/src/db/v2/index.ts @@ -0,0 +1,25 @@ +import mongoose from "mongoose"; + +const MONGODB_URI = process.env.MONGODB_URI || "mongodb://localhost:27017/checkmate"; + +export const connectDatabase = async (): Promise => { + try { + await mongoose.connect(MONGODB_URI); + console.log("Connected to MongoDB"); + return true; + } catch (error) { + console.error("MongoDB connection error:", error); + process.exit(1); + } +}; + +export const disconnectDatabase = async (): Promise => { + try { + await mongoose.disconnect(); + console.log("Disconnected from MongoDB"); + return true; + } catch (error) { + console.error("MongoDB disconnection error:", error); + return false; + } +}; diff --git a/server/src/db/v2/models/auth/User.ts b/server/src/db/v2/models/auth/User.ts index b72a29e70..25b9794c7 100644 --- a/server/src/db/v2/models/auth/User.ts +++ b/server/src/db/v2/models/auth/User.ts @@ -96,12 +96,6 @@ const userSchema = new Schema( }, { timestamps: true, - toJSON: { - transform: function (_doc, ret) { - delete ret.passwordHash; - return ret; - }, - }, } ); diff --git a/server/src/service/v2/business/CheckService.ts b/server/src/service/v2/business/CheckService.ts deleted file mode 100644 index 181fe048d..000000000 --- a/server/src/service/v2/business/CheckService.ts +++ /dev/null @@ -1,132 +0,0 @@ -import { json } from "stream/consumers"; -import { ICheck, Check, Monitor, ISystemInfo, ICaptureInfo } from "../../../db/v1/models/index.js"; -import { MonitorType } from "../../../db/v1/models/Monitor.js"; -import { StatusResponse } from "../infrastructure/NetworkService.js"; -import type { ICapturePayload, ILighthousePayload } from "../infrastructure/NetworkService.js"; -import mongoose from "mongoose"; - -export interface ICheckService { - buildCheck: (statusResponse: StatusResponse, type: MonitorType) => Promise; - cleanupOrphanedChecks: () => Promise; -} - -class CheckService implements ICheckService { - private isCapturePayload = (payload: any): payload is ICapturePayload => { - if (!payload || typeof payload !== "object") return false; - - if (!("data" in payload) || typeof payload.data !== "object") { - return false; - } - - const data = payload.data as Partial; - if (!data.cpu || typeof data.cpu !== "object" || typeof data.cpu.usage_percent !== "number") { - return false; - } - s; - - if (!data.memory || typeof data.memory !== "object" || typeof data.memory.usage_percent !== "number") { - return false; - } - - if (data.disk && !Array.isArray(data.disk)) { - return false; - } - if (data.net && !Array.isArray(data.net)) { - return false; - } - - if (!("capture" in payload) || typeof payload.capture !== "object") return false; - const capture = payload.capture as Record; - if (typeof capture.version !== "string" || typeof capture.mode !== "string") return false; - - return true; - }; - - private isPagespeedPayload = (payload: any): payload is ILighthousePayload => { - if (!payload || typeof payload !== "object") return false; - - if (!("lighthouseResult" in payload) || typeof payload.lighthouseResult !== "object") { - return false; - } - return true; - }; - - private buildBaseCheck = (statusResponse: StatusResponse) => { - const monitorId = new mongoose.Types.ObjectId(statusResponse.monitorId); - const check = new Check({ - monitorId: monitorId, - type: statusResponse?.type, - status: statusResponse?.status, - message: statusResponse?.message, - responseTime: statusResponse?.responseTime, - timings: statusResponse?.timings, - }); - return check; - }; - - private buildInfrastructureCheck = (statusResponse: StatusResponse) => { - if (!this.isCapturePayload(statusResponse.payload)) { - throw new Error("Invalid payload for infrastructure monitor"); - } - const check = this.buildBaseCheck(statusResponse); - check.system = statusResponse.payload.data; - check.capture = statusResponse.payload.capture; - return check; - }; - - private buildPagespeedCheck = (statusResponse: StatusResponse) => { - if (!this.isPagespeedPayload(statusResponse.payload)) { - throw new Error("Invalid payload for pagespeed monitor"); - } - const check = this.buildBaseCheck(statusResponse); - const lighthouseResult = statusResponse?.payload?.lighthouseResult; - check.lighthouse = { - accessibility: lighthouseResult?.categories?.accessibility?.score || 0, - bestPractices: lighthouseResult?.categories?.["best-practices"]?.score || 0, - seo: lighthouseResult?.categories?.seo?.score || 0, - performance: lighthouseResult?.categories?.performance?.score || 0, - audits: { - cls: lighthouseResult?.audits?.["cumulative-layout-shift"] || {}, - si: lighthouseResult?.audits?.["speed-index"] || {}, - fcp: lighthouseResult?.audits?.["first-contentful-paint"] || {}, - lcp: lighthouseResult?.audits?.["largest-contentful-paint"] || {}, - tbt: lighthouseResult?.audits?.["total-blocking-time"] || {}, - }, - }; - return check; - }; - - buildCheck = async (statusResponse: StatusResponse, type: MonitorType): Promise => { - switch (type) { - case "infrastructure": - return this.buildInfrastructureCheck(statusResponse as StatusResponse); - - case "pagespeed": - return this.buildPagespeedCheck(statusResponse as StatusResponse); - case "http": - case "https": - return this.buildBaseCheck(statusResponse); - - case "ping": - return this.buildBaseCheck(statusResponse); - default: - throw new Error(`Unsupported monitor type: ${type}`); - } - }; - - cleanupOrphanedChecks = async () => { - try { - const monitorIds = await Monitor.find().distinct("_id"); - const result = await Check.deleteMany({ - monitorId: { $nin: monitorIds }, - }); - console.log(`Deleted ${result.deletedCount} orphaned Checks.`); - return true; - } catch (error) { - console.error("Error cleaning up orphaned Checks:", error); - return false; - } - }; -} - -export default CheckService; diff --git a/server/src/service/v2/business/MonitorService.ts b/server/src/service/v2/business/MonitorService.ts deleted file mode 100644 index 6e48e79e5..000000000 --- a/server/src/service/v2/business/MonitorService.ts +++ /dev/null @@ -1,464 +0,0 @@ -import mongoose from "mongoose"; - -import { IMonitor, Monitor, ITokenizedUser, MonitorStats, Check } from "../../../db/v1/models/index.js"; -import ApiError from "../../../utils/ApiError.js"; -import { IJobQueue } from "../infrastructure/JobQueue.js"; -import { MonitorWithChecksResponse } from "../../../types/monitor-response-with-checks.js"; -import { MonitorStatus, MonitorType } from "../../../db/v1/models/monitors/Monitor.js"; -export interface IMonitorService { - create: (tokenizedUser: ITokenizedUser, monitorData: IMonitor) => Promise; - getAll: () => Promise; - getAllEmbedChecks: (page: number, limit: number, type: MonitorType[]) => Promise; - get: (monitorId: string) => Promise; - getEmbedChecks: (monitorId: string, range: string, status?: string) => Promise; - toggleActive: (monitorId: string, tokenizedUser: ITokenizedUser) => Promise; - update: (tokenizedUser: ITokenizedUser, monitorId: string, updateData: Partial) => Promise; - delete: (monitorId: string) => Promise; -} - -class MonitorService implements IMonitorService { - private jobQueue: IJobQueue; - constructor(jobQueue: IJobQueue) { - this.jobQueue = jobQueue; - } - - create = async (tokenizedUser: ITokenizedUser, monitorData: IMonitor) => { - const monitor = await Monitor.create({ - ...monitorData, - createdBy: tokenizedUser.sub, - updatedBy: tokenizedUser.sub, - }); - await MonitorStats.create({ - monitorId: monitor._id, - currentStreakStartedAt: Date.now(), - }); - await this.jobQueue.addJob(monitor); - return monitor; - }; - - getAll = async () => { - return Monitor.find(); - }; - - getAllEmbedChecks = async (page: number, limit: number, type: MonitorType[] = []) => { - const skip = (page - 1) * limit; - let find = {}; - if (type.length > 0) find = { type: { $in: type } }; - const monitors = await Monitor.find(find).skip(skip).limit(limit); - return monitors; - }; - - get = async (monitorId: string) => { - const monitor = await Monitor.findById(monitorId); - if (!monitor) { - throw new ApiError("Monitor not found", 404); - } - return monitor; - }; - - private getStartDate(range: string): Date { - const now = new Date(); - switch (range) { - case "30m": - return new Date(now.getTime() - 30 * 60 * 1000); - case "24h": - return new Date(now.getTime() - 24 * 60 * 60 * 1000); - case "7d": - return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); - case "30d": - return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); - default: - throw new ApiError("Invalid range parameter", 400); - } - } - - private getDateFormat(range: string): string { - switch (range) { - case "30m": - return "%Y-%m-%dT%H:%M:00Z"; - case "24h": - case "7d": - return "%Y-%m-%dT%H:00:00Z"; - case "30d": - return "%Y-%m-%d"; - default: - throw new ApiError("Invalid range parameter", 400); - } - } - - private getBaseGroup = (dateFormat: string): Record => { - return { - _id: { $dateToString: { format: dateFormat, date: "$createdAt" } }, - count: { $sum: 1 }, - avgResponseTime: { $avg: "$responseTime" }, - }; - }; - - private getBaseProjection = (): object => { - return { status: 1, responseTime: 1, createdAt: 1 }; - }; - - private getPageSpeedGroup = (dateFormat: string): Record => { - return { - _id: { $dateToString: { format: dateFormat, date: "$createdAt" } }, - count: { $sum: 1 }, - avgResponseTime: { $avg: "$responseTime" }, - accessibility: { $avg: "$lighthouse.accessibility" }, - bestPractices: { $avg: "$lighthouse.bestPractices" }, - seo: { $avg: "$lighthouse.seo" }, - performance: { $avg: "$lighthouse.performance" }, - cls: { $avg: "$lighthouse.audits.cls.score" }, - si: { $avg: "$lighthouse.audits.si.score" }, - fcp: { $avg: "$lighthouse.audits.fcp.score" }, - lcp: { $avg: "$lighthouse.audits.lcp.score" }, - tbt: { $avg: "$lighthouse.audits.tbt.score" }, - }; - }; - - private getPageSpeedProjection = (): object => { - const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 }; - projectStage["lighthouse.accessibility"] = 1; - projectStage["lighthouse.seo"] = 1; - projectStage["lighthouse.bestPractices"] = 1; - projectStage["lighthouse.performance"] = 1; - projectStage["lighthouse.audits.cls.score"] = 1; - projectStage["lighthouse.audits.si.score"] = 1; - projectStage["lighthouse.audits.fcp.score"] = 1; - projectStage["lighthouse.audits.lcp.score"] = 1; - projectStage["lighthouse.audits.tbt.score"] = 1; - return projectStage; - }; - - private getInfraGroup = (dateFormat: string): Record => { - return { - _id: { $dateToString: { format: dateFormat, date: "$createdAt" } }, - count: { $sum: 1 }, - avgResponseTime: { $avg: "$responseTime" }, - physicalCores: { $last: "$system.cpu.physical_core" }, - logicalCores: { $last: "$system.cpu.logical_core" }, - frequency: { $avg: "$system.cpu.frequency" }, - currentFrequency: { $last: "$system.cpu.current_frequency" }, - tempsArrays: { $push: "$system.cpu.temperature" }, - freePercent: { $avg: "$system.cpu.free_percent" }, - usedPercent: { $avg: "$system.cpu.usage_percent" }, - total_bytes: { $last: "$system.memory.total_bytes" }, - available_bytes: { $last: "$system.memory.available_bytes" }, - used_bytes: { $last: "$system.memory.used_bytes" }, - memory_usage_percent: { $avg: "$system.memory.usage_percent" }, - disksArray: { $push: "$system.disk" }, - os: { $last: "$system.host.os" }, - platform: { $last: "$system.host.platform" }, - kernel_version: { $last: "$system.host.kernel_version" }, - pretty_name: { $last: "$system.host.pretty_name" }, - netsArray: { $push: "$system.net" }, - }; - }; - - private getInfraProjection = (): object => { - const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 }; - projectStage["system.cpu.physical_core"] = 1; - projectStage["system.cpu.logical_core"] = 1; - projectStage["system.cpu.frequency"] = 1; - projectStage["system.cpu.current_frequency"] = 1; - projectStage["system.cpu.temperature"] = 1; - projectStage["system.cpu.free_percent"] = 1; - projectStage["system.cpu.usage_percent"] = 1; - projectStage["system.memory.total_bytes"] = 1; - projectStage["system.memory.available_bytes"] = 1; - projectStage["system.memory.used_bytes"] = 1; - projectStage["system.memory.usage_percent"] = 1; - projectStage["system.disk"] = 1; - projectStage["system.host.os"] = 1; - projectStage["system.host.platform"] = 1; - projectStage["system.host.kernel_version"] = 1; - projectStage["system.host.pretty_name"] = 1; - projectStage["system.net"] = 1; - return projectStage; - }; - - private getFinalProjection = (type: string): object => { - if (type === "pagespeed") { - return { - _id: 1, - count: 1, - avgResponseTime: 1, - accessibility: "$accessibility", - seo: "$seo", - bestPractices: "$bestPractices", - performance: "$performance", - cls: "$cls", - si: "$si", - fcp: "$fcp", - lcp: "$lcp", - tbt: "$tbt", - }; - } - - if (type === "infrastructure") { - return { - _id: 1, - count: 1, - avgResponseTime: 1, - cpu: { - physicalCores: "$physicalCores", - logicalCores: "$logicalCores", - frequency: "$frequency", - currentFrequency: "$currentFrequency", - temperatures: { - $map: { - input: { - $range: [0, { $size: { $arrayElemAt: ["$tempsArrays", 0] } }], - }, - as: "idx", - in: { - $avg: { - $map: { - input: "$tempsArrays", - as: "arr", - in: { $arrayElemAt: ["$$arr", "$$idx"] }, - }, - }, - }, - }, - }, - freePercent: "$freePercent", - usedPercent: "$usedPercent", - }, - memory: { - total_bytes: "$total_bytes", - available_bytes: "$available_bytes", - used_bytes: "$used_bytes", - usage_percent: "$memory_usage_percent", - }, - disks: { - $map: { - input: { - $range: [0, { $size: { $arrayElemAt: ["$disksArray", 0] } }], - }, - as: "idx", - in: { - $let: { - vars: { - diskGroup: { - $map: { - input: "$disksArray", - as: "diskArr", - in: { $arrayElemAt: ["$$diskArr", "$$idx"] }, - }, - }, - }, - in: { - device: { $arrayElemAt: ["$$diskGroup.device", 0] }, - total_bytes: { $avg: "$$diskGroup.total_bytes" }, - free_bytes: { $avg: "$$diskGroup.free_bytes" }, - used_bytes: { $avg: "$$diskGroup.used_bytes" }, - usage_percent: { $avg: "$$diskGroup.usage_percent" }, - total_inodes: { $avg: "$$diskGroup.total_inodes" }, - free_inodes: { $avg: "$$diskGroup.free_inodes" }, - used_inodes: { $avg: "$$diskGroup.used_inodes" }, - inodes_usage_percent: { - $avg: "$$diskGroup.inodes_usage_percent", - }, - read_bytes: { $avg: "$$diskGroup.read_bytes" }, - write_bytes: { $avg: "$$diskGroup.write_bytes" }, - read_time: { $avg: "$$diskGroup.read_time" }, - write_time: { $avg: "$$diskGroup.write_time" }, - }, - }, - }, - }, - }, - host: { - os: "$os", - platform: "$platform", - kernel_version: "$kernel_version", - pretty_name: "$pretty_name", - }, - net: { - $map: { - input: { - $range: [0, { $size: { $arrayElemAt: ["$netsArray", 0] } }], - }, - as: "idx", - in: { - $let: { - vars: { - netGroup: { - $map: { - input: "$netsArray", - as: "netArr", - in: { $arrayElemAt: ["$$netArr", "$$idx"] }, - }, - }, - }, - in: { - name: { $arrayElemAt: ["$$netGroup.name", 0] }, - bytes_sent: { $avg: "$$netGroup.bytes_sent" }, - bytes_recv: { $avg: "$$netGroup.bytes_recv" }, - packets_sent: { $avg: "$$netGroup.packets_sent" }, - packets_recv: { $avg: "$$netGroup.packets_recv" }, - err_in: { $avg: "$$netGroup.err_in" }, - err_out: { $avg: "$$netGroup.err_out" }, - drop_in: { $avg: "$$netGroup.drop_in" }, - drop_out: { $avg: "$$netGroup.drop_out" }, - fifo_in: { $avg: "$$netGroup.fifo_in" }, - fifo_out: { $avg: "$$netGroup.fifo_out" }, - }, - }, - }, - }, - }, - }; - } - return {}; - }; - - getEmbedChecks = async (monitorId: string, range: string, status: string | undefined): Promise => { - const monitor = await Monitor.findById(monitorId); - if (!monitor) { - throw new ApiError("Monitor not found", 404); - } - const startDate = this.getStartDate(range); - const dateFormat = this.getDateFormat(range); - - // Build match stage - const matchStage: { - monitorId: mongoose.Types.ObjectId; - createdAt: { $gte: Date }; - status?: string; - } = { - monitorId: monitor._id, - createdAt: { $gte: startDate }, - }; - - if (status) { - matchStage.status = status; - } - - let groupClause; - - if (monitor.type === "pagespeed") { - groupClause = this.getPageSpeedGroup(dateFormat); - } else if (monitor.type === "infrastructure") { - groupClause = this.getInfraGroup(dateFormat); - } else { - groupClause = this.getBaseGroup(dateFormat); - } - - let projectStage; - if (monitor.type === "pagespeed") { - projectStage = this.getPageSpeedProjection(); - } else if (monitor.type === "infrastructure") { - projectStage = this.getInfraProjection(); - } else { - projectStage = this.getBaseProjection(); - } - - let finalProjection = {}; - if (monitor.type === "pagespeed" || monitor.type === "infrastructure") { - finalProjection = this.getFinalProjection(monitor.type); - } else { - finalProjection = { _id: 1, count: 1, avgResponseTime: 1 }; - } - - const checks = await Check.aggregate([ - { - $match: matchStage, - }, - { $sort: { createdAt: 1 } }, - { $project: projectStage }, - { $group: groupClause }, - { $sort: { _id: -1 } }, - { - $project: finalProjection, - }, - ]); - - // Get monitor stats - const monitorStats = await MonitorStats.findOne({ - monitorId: monitor._id, - }).lean(); - - if (!monitorStats) { - throw new ApiError("Monitor stats not found", 404); - } - - return { - monitor: monitor.toObject(), - checks, - stats: monitorStats, - }; - }; - - async toggleActive(id: string, tokenizedUser: ITokenizedUser) { - const pendingStatus: MonitorStatus = "initializing"; - const updatedMonitor = await Monitor.findOneAndUpdate( - { _id: id }, - [ - { - $set: { - isActive: { $not: "$isActive" }, - status: pendingStatus, - updatedBy: tokenizedUser.sub, - updatedAt: new Date(), - }, - }, - ], - { new: true } - ); - - if (!updatedMonitor) { - throw new ApiError("Monitor not found", 404); - } - - await this.jobQueue.updateJob(updatedMonitor); - - if (updatedMonitor?.isActive) { - await this.jobQueue.resumeJob(updatedMonitor); - } else { - await this.jobQueue.pauseJob(updatedMonitor); - } - return updatedMonitor; - } - - async update(tokenizedUser: ITokenizedUser, monitorId: string, updateData: Partial) { - const allowedFields: (keyof IMonitor)[] = ["name", "interval", "isActive", "n", "m", "notificationChannels"]; - const safeUpdate: Partial = {}; - - for (const field of allowedFields) { - if (updateData[field] !== undefined) { - (safeUpdate as any)[field] = updateData[field]; - } - } - - const updatedMonitor = await Monitor.findByIdAndUpdate( - monitorId, - { - $set: { - ...safeUpdate, - updatedAt: new Date(), - updatedBy: tokenizedUser.sub, - }, - }, - { new: true, runValidators: true } - ); - - if (!updatedMonitor) { - throw new ApiError("Monitor not found", 404); - } - await this.jobQueue.updateJob(updatedMonitor); - return updatedMonitor; - } - - async delete(monitorId: string) { - const monitor = await Monitor.findById(monitorId); - if (!monitor) { - throw new ApiError("Monitor not found", 404); - } - await monitor.deleteOne(); - await this.jobQueue.deleteJob(monitor); - return true; - } -} - -export default MonitorService; From 2d12fbdff11c2c3fae0600258e4544d0da044487 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 23 Sep 2025 14:56:02 -0700 Subject: [PATCH 014/400] fix notification schema --- server/src/db/v2/models/monitors/Monitor.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/server/src/db/v2/models/monitors/Monitor.ts b/server/src/db/v2/models/monitors/Monitor.ts index 055d36a1b..52cbae9d8 100644 --- a/server/src/db/v2/models/monitors/Monitor.ts +++ b/server/src/db/v2/models/monitors/Monitor.ts @@ -63,14 +63,10 @@ const MonitorSchema = new Schema( ], default: [], }, - notificationChannels: [ - { - type: Schema.Types.ObjectId, - ref: "NotificationChannel_v2", - required: false, - default: [], - }, - ], + notificationChannels: { + type: [{ type: Schema.Types.ObjectId, ref: "NotificationChannel_v2" }], + default: [], + }, createdBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, updatedBy: { type: Schema.Types.ObjectId, ref: "User_v2", required: true }, From 68f4adfbfef319683fb675d6d2df1203ad50448d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 23 Sep 2025 15:22:08 -0700 Subject: [PATCH 015/400] add v2 services --- server/package-lock.json | 1392 +++++++++++++++++ server/package.json | 2 + server/src/config/index.ts | 25 + server/src/service/v2/business/AuthService.ts | 206 +++ .../src/service/v2/business/CheckService.ts | 132 ++ .../src/service/v2/business/InviteService.ts | 61 + .../service/v2/business/MaintenanceService.ts | 141 ++ .../v2/business/MonitorStatsService.ts | 25 + .../v2/business/NotificationChannelService.ts | 100 ++ .../src/service/v2/business/QueueService.ts | 23 + server/src/service/v2/business/UserService.ts | 13 + .../service/v2/infrastructure/JobGenerator.ts | 80 + .../src/service/v2/infrastructure/JobQueue.ts | 227 +++ .../v2/infrastructure/NetworkService.ts | 193 +++ .../v2/infrastructure/NotificationService.ts | 58 + .../NotificationServices/Discord.ts | 73 + .../NotificationServices/Email.ts | 65 + .../NotificationServices/IMessageService.ts | 15 + .../NotificationServices/Slack.ts | 116 ++ .../NotificationServices/Webhook.ts | 43 + .../NotificationServices/index.ts | 5 + .../v2/infrastructure/StatusService.ts | 100 ++ server/src/utils/ApiError.ts | 15 + server/src/utils/JWTUtils.ts | 22 + 24 files changed, 3132 insertions(+) create mode 100644 server/src/config/index.ts create mode 100644 server/src/service/v2/business/AuthService.ts create mode 100644 server/src/service/v2/business/CheckService.ts create mode 100644 server/src/service/v2/business/InviteService.ts create mode 100644 server/src/service/v2/business/MaintenanceService.ts create mode 100644 server/src/service/v2/business/MonitorStatsService.ts create mode 100644 server/src/service/v2/business/NotificationChannelService.ts create mode 100644 server/src/service/v2/business/QueueService.ts create mode 100644 server/src/service/v2/business/UserService.ts create mode 100644 server/src/service/v2/infrastructure/JobGenerator.ts create mode 100644 server/src/service/v2/infrastructure/JobQueue.ts create mode 100644 server/src/service/v2/infrastructure/NetworkService.ts create mode 100644 server/src/service/v2/infrastructure/NotificationService.ts create mode 100644 server/src/service/v2/infrastructure/NotificationServices/Discord.ts create mode 100644 server/src/service/v2/infrastructure/NotificationServices/Email.ts create mode 100644 server/src/service/v2/infrastructure/NotificationServices/IMessageService.ts create mode 100644 server/src/service/v2/infrastructure/NotificationServices/Slack.ts create mode 100644 server/src/service/v2/infrastructure/NotificationServices/Webhook.ts create mode 100644 server/src/service/v2/infrastructure/NotificationServices/index.ts create mode 100644 server/src/service/v2/infrastructure/StatusService.ts create mode 100644 server/src/utils/ApiError.ts create mode 100644 server/src/utils/JWTUtils.ts diff --git a/server/package-lock.json b/server/package-lock.json index 06c1c30b3..92d258433 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -45,6 +45,8 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/nodemailer": "7.0.1", + "@types/ping": "0.4.4", "c8": "10.1.3", "chai": "5.2.0", "eslint": "^9.17.0", @@ -72,6 +74,751 @@ "lru-cache": "^10.4.3" } }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-sesv2": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.895.0.tgz", + "integrity": "sha512-tYfgwoG4n8LEc0NHVriNNS+YqeDMJHHcUzJxJJ7Nols5Y3EJzv76QRvu2rdLa9XoxvTSFGXJSuqrd8s0ElbCQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.894.0", + "@aws-sdk/credential-provider-node": "3.895.0", + "@aws-sdk/middleware-host-header": "3.893.0", + "@aws-sdk/middleware-logger": "3.893.0", + "@aws-sdk/middleware-recursion-detection": "3.893.0", + "@aws-sdk/middleware-user-agent": "3.895.0", + "@aws-sdk/region-config-resolver": "3.893.0", + "@aws-sdk/signature-v4-multi-region": "3.894.0", + "@aws-sdk/types": "3.893.0", + "@aws-sdk/util-endpoints": "3.895.0", + "@aws-sdk/util-user-agent-browser": "3.893.0", + "@aws-sdk/util-user-agent-node": "3.895.0", + "@smithy/config-resolver": "^4.2.2", + "@smithy/core": "^3.11.1", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/hash-node": "^4.1.1", + "@smithy/invalid-dependency": "^4.1.1", + "@smithy/middleware-content-length": "^4.1.1", + "@smithy/middleware-endpoint": "^4.2.3", + "@smithy/middleware-retry": "^4.2.4", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.3", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-body-length-node": "^4.1.0", + "@smithy/util-defaults-mode-browser": "^4.1.3", + "@smithy/util-defaults-mode-node": "^4.1.3", + "@smithy/util-endpoints": "^3.1.2", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.2", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.895.0.tgz", + "integrity": "sha512-AQHk6iJrwce/NwZa5/Njy0ZGoHdxWCajkgufhXk53L0kRiC3vUPPWEV1m1F3etQWhaUsatcO2xtRuKvLpe4zgA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.894.0", + "@aws-sdk/middleware-host-header": "3.893.0", + "@aws-sdk/middleware-logger": "3.893.0", + "@aws-sdk/middleware-recursion-detection": "3.893.0", + "@aws-sdk/middleware-user-agent": "3.895.0", + "@aws-sdk/region-config-resolver": "3.893.0", + "@aws-sdk/types": "3.893.0", + "@aws-sdk/util-endpoints": "3.895.0", + "@aws-sdk/util-user-agent-browser": "3.893.0", + "@aws-sdk/util-user-agent-node": "3.895.0", + "@smithy/config-resolver": "^4.2.2", + "@smithy/core": "^3.11.1", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/hash-node": "^4.1.1", + "@smithy/invalid-dependency": "^4.1.1", + "@smithy/middleware-content-length": "^4.1.1", + "@smithy/middleware-endpoint": "^4.2.3", + "@smithy/middleware-retry": "^4.2.4", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.3", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-body-length-node": "^4.1.0", + "@smithy/util-defaults-mode-browser": "^4.1.3", + "@smithy/util-defaults-mode-node": "^4.1.3", + "@smithy/util-endpoints": "^3.1.2", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.2", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.894.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.894.0.tgz", + "integrity": "sha512-7zbO31NV2FaocmMtWOg/fuTk3PC2Ji2AC0Fi2KqrppEDIcwLlTTuT9w/rdu/93Pz+wyUhCxWnDc0tPbwtCLs+A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.893.0", + "@aws-sdk/xml-builder": "3.894.0", + "@smithy/core": "^3.11.1", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/signature-v4": "^5.2.1", + "@smithy/smithy-client": "^4.6.3", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.894.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.894.0.tgz", + "integrity": "sha512-2aiQJIRWOuROPPISKgzQnH/HqSfucdk5z5VMemVH3Mm2EYOrzBwmmiiFpmSMN3ST+sE8c7gusqycUchP+KfALQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.894.0", + "@aws-sdk/types": "3.893.0", + "@smithy/property-provider": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.894.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.894.0.tgz", + "integrity": "sha512-Z5QQpqFRflszrT+lUq6+ORuu4jRDcpgCUSoTtlhczidMqfdOSckKmK3chZEfmUUJPSwoFQZ7EiVTsX3c886fBg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.894.0", + "@aws-sdk/types": "3.893.0", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/property-provider": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.3", + "@smithy/types": "^4.5.0", + "@smithy/util-stream": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.895.0.tgz", + "integrity": "sha512-uIh7N4IN/yIk+qYMAkVpVkjhB90SGKSfaXEVcnmxzBDG6e5304HKT0esqoCVZvtFfLKasjm2TOpalM5l3fi/dA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.894.0", + "@aws-sdk/credential-provider-env": "3.894.0", + "@aws-sdk/credential-provider-http": "3.894.0", + "@aws-sdk/credential-provider-process": "3.894.0", + "@aws-sdk/credential-provider-sso": "3.895.0", + "@aws-sdk/credential-provider-web-identity": "3.895.0", + "@aws-sdk/nested-clients": "3.895.0", + "@aws-sdk/types": "3.893.0", + "@smithy/credential-provider-imds": "^4.1.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.895.0.tgz", + "integrity": "sha512-7xsBCmkBUz+2sNqNsDJ1uyQsBvwhNFzwFt8wX39WrFJTpTQh3uNQ5g8QH21BbkKqIFKCLdvgHgwt3Ub5RGVuPA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.894.0", + "@aws-sdk/credential-provider-http": "3.894.0", + "@aws-sdk/credential-provider-ini": "3.895.0", + "@aws-sdk/credential-provider-process": "3.894.0", + "@aws-sdk/credential-provider-sso": "3.895.0", + "@aws-sdk/credential-provider-web-identity": "3.895.0", + "@aws-sdk/types": "3.893.0", + "@smithy/credential-provider-imds": "^4.1.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.894.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.894.0.tgz", + "integrity": "sha512-VU74GNsj+SsO+pl4d+JimlQ7+AcderZaC6bFndQssQdFZ5NRad8yFNz5Xbec8CPJr+z/VAwHib6431F5nYF46g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.894.0", + "@aws-sdk/types": "3.893.0", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.895.0.tgz", + "integrity": "sha512-bZCcHUZGz+XlCaK0KEOHGHkMtlwIvnpxJvlZtSCVaBdX/IgouxaB42fxChflxSMRWF45ygdezfky4i17f6vC4w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.895.0", + "@aws-sdk/core": "3.894.0", + "@aws-sdk/token-providers": "3.895.0", + "@aws-sdk/types": "3.893.0", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.895.0.tgz", + "integrity": "sha512-tKbXbOp2xrL02fxKvB7ko1E4Uvyy5TF9qi5pT2MVWNnfSsBlUM80aJ6tyUPKWXdUTdAlPrU3XcwgQl/DnnRa9A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.894.0", + "@aws-sdk/nested-clients": "3.895.0", + "@aws-sdk/types": "3.893.0", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.893.0.tgz", + "integrity": "sha512-qL5xYRt80ahDfj9nDYLhpCNkDinEXvjLe/Qen/Y/u12+djrR2MB4DRa6mzBCkLkdXDtf0WAoW2EZsNCfGrmOEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.893.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.893.0.tgz", + "integrity": "sha512-ZqzMecjju5zkBquSIfVfCORI/3Mge21nUY4nWaGQy+NUXehqCGG4W7AiVpiHGOcY2cGJa7xeEkYcr2E2U9U0AA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.893.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.893.0.tgz", + "integrity": "sha512-H7Zotd9zUHQAr/wr3bcWHULYhEeoQrF54artgsoUGIf/9emv6LzY89QUccKIxYd6oHKNTrTyXm9F0ZZrzXNxlg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.893.0", + "@aws/lambda-invoke-store": "^0.0.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.894.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.894.0.tgz", + "integrity": "sha512-0C3lTVdTuv5CkJ4LulpA7FmGFSKrGUKxnFZ6+qGjYjNzbdiHXfq0TyEBiDmVqDkoV2k4AT2H/m0Xw//rTkcNEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.894.0", + "@aws-sdk/types": "3.893.0", + "@aws-sdk/util-arn-parser": "3.893.0", + "@smithy/core": "^3.11.1", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/protocol-http": "^5.2.1", + "@smithy/signature-v4": "^5.2.1", + "@smithy/smithy-client": "^4.6.3", + "@smithy/types": "^4.5.0", + "@smithy/util-config-provider": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-stream": "^4.3.2", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.895.0.tgz", + "integrity": "sha512-JUqQW2RPp4I95wZ/Im9fTiaX3DF55oJgeoiNlLdHkQZPSNNS/pT1WMWMReSvJdcfSNU3xSUaLtI+h4mQjQUDbQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.894.0", + "@aws-sdk/types": "3.893.0", + "@aws-sdk/util-endpoints": "3.895.0", + "@smithy/core": "^3.11.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.895.0.tgz", + "integrity": "sha512-8w1ihfYgvds6kfal/qJXQQrHRsKYh2nujSyzWMo2TMKMze9WPZA93G4mRbRtKtbSuQ66mVWePH8Cksq35ABu2Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.894.0", + "@aws-sdk/middleware-host-header": "3.893.0", + "@aws-sdk/middleware-logger": "3.893.0", + "@aws-sdk/middleware-recursion-detection": "3.893.0", + "@aws-sdk/middleware-user-agent": "3.895.0", + "@aws-sdk/region-config-resolver": "3.893.0", + "@aws-sdk/types": "3.893.0", + "@aws-sdk/util-endpoints": "3.895.0", + "@aws-sdk/util-user-agent-browser": "3.893.0", + "@aws-sdk/util-user-agent-node": "3.895.0", + "@smithy/config-resolver": "^4.2.2", + "@smithy/core": "^3.11.1", + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/hash-node": "^4.1.1", + "@smithy/invalid-dependency": "^4.1.1", + "@smithy/middleware-content-length": "^4.1.1", + "@smithy/middleware-endpoint": "^4.2.3", + "@smithy/middleware-retry": "^4.2.4", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/smithy-client": "^4.6.3", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-body-length-node": "^4.1.0", + "@smithy/util-defaults-mode-browser": "^4.1.3", + "@smithy/util-defaults-mode-node": "^4.1.3", + "@smithy/util-endpoints": "^3.1.2", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.2", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.893.0.tgz", + "integrity": "sha512-/cJvh3Zsa+Of0Zbg7vl9wp/kZtdb40yk/2+XcroAMVPO9hPvmS9r/UOm6tO7FeX4TtkRFwWaQJiTZTgSdsPY+Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.893.0", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/types": "^4.5.0", + "@smithy/util-config-provider": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.894.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.894.0.tgz", + "integrity": "sha512-Te5b3fSbatkZrh3eYNmpOadZFKsCLNSwiolQKQeEeKHxdnqORwYXa+0ypcTHle6ukic+tFRRd9n3NuMVo9uiVg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-sdk-s3": "3.894.0", + "@aws-sdk/types": "3.893.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/signature-v4": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.895.0.tgz", + "integrity": "sha512-vJqrEHFFGRZ3ok5T+jII00sa2DQ3HdVkTBIfM0DcrcPssqDV18VKdA767qiBdIEN/cygjdBg8Ri/cuq6ER9BeQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.894.0", + "@aws-sdk/nested-clients": "3.895.0", + "@aws-sdk/types": "3.893.0", + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.893.0.tgz", + "integrity": "sha512-Aht1nn5SnA0N+Tjv0dzhAY7CQbxVtmq1bBR6xI0MhG7p2XYVh1wXuKTzrldEvQWwA3odOYunAfT9aBiKZx9qIg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.893.0.tgz", + "integrity": "sha512-u8H4f2Zsi19DGnwj5FSZzDMhytYF/bCh37vAtBsn3cNDL3YG578X5oc+wSX54pM3tOxS+NY7tvOAo52SW7koUA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.895.0.tgz", + "integrity": "sha512-MhxBvWbwxmKknuggO2NeMwOVkHOYL98pZ+1ZRI5YwckoCL3AvISMnPJgfN60ww6AIXHGpkp+HhpFdKOe8RHSEg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.893.0", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-endpoints": "^3.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.893.0.tgz", + "integrity": "sha512-T89pFfgat6c8nMmpI8eKjBcDcgJq36+m9oiXbcUzeU55MP9ZuGgBomGjGnHaEyF36jenW9gmg3NfZDm0AO2XPg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.893.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.893.0.tgz", + "integrity": "sha512-PE9NtbDBW6Kgl1bG6A5fF3EPo168tnkj8TgMcT0sg4xYBWsBpq0bpJZRh+Jm5Bkwiw9IgTCLjEU7mR6xWaMB9w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.893.0", + "@smithy/types": "^4.5.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.895.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.895.0.tgz", + "integrity": "sha512-lLRC7BAFOPtJk4cZC0Q0MZBMCGF109QpGnug3L3n/2TJW02Sinz9lzA0ykBpYXe9j60LjIYSENCg+F4DZE5vxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.895.0", + "@aws-sdk/types": "3.893.0", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.894.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.894.0.tgz", + "integrity": "sha512-E6EAMc9dT1a2DOdo4zyOf3fp5+NJ2wI+mcm7RaW1baFIWDwcb99PpvWoV7YEiK7oaBDshuOEGWKUSYXdW+JYgA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder/node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.0.1.tgz", + "integrity": "sha512-ORHRQ2tmvnBXc8t/X9Z8IcSbBA4xTLKuN873FopzklHMeqBst7YG0d+AX97inkvDX+NChYtSr+qGfcqGFaI8Zw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -1655,6 +2402,626 @@ "dev": true, "license": "(Unlicense OR Apache-2.0)" }, + "node_modules/@smithy/abort-controller": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.1.1.tgz", + "integrity": "sha512-vkzula+IwRvPR6oKQhMYioM3A/oX/lFCZiwuxkQbRhqJS2S4YRY2k7k/SyR2jMf3607HLtbEwlRxi0ndXHMjRg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.2.2.tgz", + "integrity": "sha512-IT6MatgBWagLybZl1xQcURXRICvqz1z3APSCAI9IqdvfCkrA7RaQIEfgC6G/KvfxnDfQUDqFV+ZlixcuFznGBQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.2", + "@smithy/types": "^4.5.0", + "@smithy/util-config-provider": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.12.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.12.0.tgz", + "integrity": "sha512-zJeAgogZfbwlPGL93y4Z/XNeIN37YCreRUd6YMIRvaq+6RnBK8PPYYIQ85Is/GglPh3kNImD5riDCXbVSDpCiQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-body-length-browser": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-stream": "^4.3.2", + "@smithy/util-utf8": "^4.1.0", + "@smithy/uuid": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.1.2.tgz", + "integrity": "sha512-JlYNq8TShnqCLg0h+afqe2wLAwZpuoSgOyzhYvTgbiKBWRov+uUve+vrZEQO6lkdLOWPh7gK5dtb9dS+KGendg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.2.1.tgz", + "integrity": "sha512-5/3wxKNtV3wO/hk1is+CZUhL8a1yy/U+9u9LKQ9kZTkMsHaQjJhc3stFfiujtMnkITjzWfndGA2f7g9Uh9vKng==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.2.1", + "@smithy/querystring-builder": "^4.1.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.1.1.tgz", + "integrity": "sha512-H9DIU9WBLhYrvPs9v4sYvnZ1PiAI0oc8CgNQUJ1rpN3pP7QADbTOUjchI2FB764Ub0DstH5xbTqcMJu1pnVqxA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "@smithy/util-buffer-from": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.1.1.tgz", + "integrity": "sha512-1AqLyFlfrrDkyES8uhINRlJXmHA2FkG+3DY8X+rmLSqmFwk3DJnvhyGzyByPyewh2jbmV+TYQBEfngQax8IFGg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.1.0.tgz", + "integrity": "sha512-ePTYUOV54wMogio+he4pBybe8fwg4sDvEVDBU8ZlHOZXbXK3/C0XfJgUCu6qAZcawv05ZhZzODGUerFBPsPUDQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.1.1.tgz", + "integrity": "sha512-9wlfBBgTsRvC2JxLJxv4xDGNBrZuio3AgSl0lSFX7fneW2cGskXTYpFxCdRYD2+5yzmsiTuaAJD1Wp7gWt9y9w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.2.4.tgz", + "integrity": "sha512-FZ4hzupOmthm8Q8ujYrd0I+/MHwVMuSTdkDtIQE0xVuvJt9pLT6Q+b0p4/t+slDyrpcf+Wj7SN+ZqT5OryaaZg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.12.0", + "@smithy/middleware-serde": "^4.1.1", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "@smithy/url-parser": "^4.1.1", + "@smithy/util-middleware": "^4.1.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.3.0.tgz", + "integrity": "sha512-qhEX9745fAxZvtLM4bQJAVC98elWjiMO2OiHl1s6p7hUzS4QfZO1gXUYNwEK8m0J6NoCD5W52ggWxbIDHI0XSg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.2", + "@smithy/protocol-http": "^5.2.1", + "@smithy/service-error-classification": "^4.1.2", + "@smithy/smithy-client": "^4.6.4", + "@smithy/types": "^4.5.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-retry": "^4.1.2", + "@smithy/uuid": "^1.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.1.1.tgz", + "integrity": "sha512-lh48uQdbCoj619kRouev5XbWhCwRKLmphAif16c4J6JgJ4uXjub1PI6RL38d3BLliUvSso6klyB/LTNpWSNIyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.1.1.tgz", + "integrity": "sha512-ygRnniqNcDhHzs6QAPIdia26M7e7z9gpkIMUe/pK0RsrQ7i5MblwxY8078/QCnGq6AmlUUWgljK2HlelsKIb/A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.2.2.tgz", + "integrity": "sha512-SYGTKyPvyCfEzIN5rD8q/bYaOPZprYUPD2f5g9M7OjaYupWOoQFYJ5ho+0wvxIRf471i2SR4GoiZ2r94Jq9h6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.1.1", + "@smithy/shared-ini-file-loader": "^4.2.0", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.2.1.tgz", + "integrity": "sha512-REyybygHlxo3TJICPF89N2pMQSf+p+tBJqpVe1+77Cfi9HBPReNjTgtZ1Vg73exq24vkqJskKDpfF74reXjxfw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/querystring-builder": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.1.1.tgz", + "integrity": "sha512-gm3ZS7DHxUbzC2wr8MUCsAabyiXY0gaj3ROWnhSx/9sPMc6eYLMM4rX81w1zsMaObj2Lq3PZtNCC1J6lpEY7zg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.2.1.tgz", + "integrity": "sha512-T8SlkLYCwfT/6m33SIU/JOVGNwoelkrvGjFKDSDtVvAXj/9gOT78JVJEas5a+ETjOu4SVvpCstKgd0PxSu/aHw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.1.1.tgz", + "integrity": "sha512-J9b55bfimP4z/Jg1gNo+AT84hr90p716/nvxDkPGCD4W70MPms0h8KF50RDRgBGZeL83/u59DWNqJv6tEP/DHA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "@smithy/util-uri-escape": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.1.1.tgz", + "integrity": "sha512-63TEp92YFz0oQ7Pj9IuI3IgnprP92LrZtRAkE3c6wLWJxfy/yOPRt39IOKerVr0JS770olzl0kGafXlAXZ1vng==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.1.2.tgz", + "integrity": "sha512-Kqd8wyfmBWHZNppZSMfrQFpc3M9Y/kjyN8n8P4DqJJtuwgK1H914R471HTw7+RL+T7+kI1f1gOnL7Vb5z9+NgQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.2.0.tgz", + "integrity": "sha512-OQTfmIEp2LLuWdxa8nEEPhZmiOREO6bcB6pjs0AySf4yiZhl6kMOfqmcwcY8BaBPX+0Tb+tG7/Ia/6mwpoZ7Pw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.2.1.tgz", + "integrity": "sha512-M9rZhWQLjlQVCCR37cSjHfhriGRN+FQ8UfgrYNufv66TJgk+acaggShl3KS5U/ssxivvZLlnj7QH2CUOKlxPyA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.1.0", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-hex-encoding": "^4.1.0", + "@smithy/util-middleware": "^4.1.1", + "@smithy/util-uri-escape": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.6.4", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.6.4.tgz", + "integrity": "sha512-qL7O3VDyfzCSN9r+sdbQXGhaHtrfSJL30En6Jboj0I3bobf2g1/T0eP2L4qxqrEW26gWhJ4THI4ElVVLjYyBHg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.12.0", + "@smithy/middleware-endpoint": "^4.2.4", + "@smithy/middleware-stack": "^4.1.1", + "@smithy/protocol-http": "^5.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-stream": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.5.0.tgz", + "integrity": "sha512-RkUpIOsVlAwUIZXO1dsz8Zm+N72LClFfsNqf173catVlvRZiwPy0x2u0JLEA4byreOPKDZPGjmPDylMoP8ZJRg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.1.1.tgz", + "integrity": "sha512-bx32FUpkhcaKlEoOMbScvc93isaSiRM75pQ5IgIBaMkT7qMlIibpPRONyx/0CvrXHzJLpOn/u6YiDX2hcvs7Dg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.1.1", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.1.0.tgz", + "integrity": "sha512-RUGd4wNb8GeW7xk+AY5ghGnIwM96V0l2uzvs/uVHf+tIuVX2WSvynk5CxNoBCsM2rQRSZElAo9rt3G5mJ/gktQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.1.0.tgz", + "integrity": "sha512-V2E2Iez+bo6bUMOTENPr6eEmepdY8Hbs+Uc1vkDKgKNA/brTJqOW/ai3JO1BGj9GbCeLqw90pbbH7HFQyFotGQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.1.0.tgz", + "integrity": "sha512-BOI5dYjheZdgR9XiEM3HJcEMCXSoqbzu7CzIgYrx0UtmvtC3tC2iDGpJLsSRFffUpy8ymsg2ARMP5fR8mtuUQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.1.0.tgz", + "integrity": "sha512-N6yXcjfe/E+xKEccWEKzK6M+crMrlwaCepKja0pNnlSkm6SjAeLKKA++er5Ba0I17gvKfN/ThV+ZOx/CntKTVw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.1.0.tgz", + "integrity": "sha512-swXz2vMjrP1ZusZWVTB/ai5gK+J8U0BWvP10v9fpcFvg+Xi/87LHvHfst2IgCs1i0v4qFZfGwCmeD/KNCdJZbQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.1.4.tgz", + "integrity": "sha512-mLDJ1s4eA3vwOGaQOEPlg5LB4LdZUUMpB5UMOMofeGhWqiS7WR7dTpLiNi9zVn+YziKUd3Af5NLfxDs7NJqmIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.1.1", + "@smithy/smithy-client": "^4.6.4", + "@smithy/types": "^4.5.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.1.4.tgz", + "integrity": "sha512-pjX2iMTcOASaSanAd7bu6i3fcMMezr3NTr8Rh64etB0uHRZi+Aw86DoCxPESjY4UTIuA06hhqtTtw95o//imYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.2.2", + "@smithy/credential-provider-imds": "^4.1.2", + "@smithy/node-config-provider": "^4.2.2", + "@smithy/property-provider": "^4.1.1", + "@smithy/smithy-client": "^4.6.4", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.1.2.tgz", + "integrity": "sha512-+AJsaaEGb5ySvf1SKMRrPZdYHRYSzMkCoK16jWnIMpREAnflVspMIDeCVSZJuj+5muZfgGpNpijE3mUNtjv01Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.2.2", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.1.0.tgz", + "integrity": "sha512-1LcueNN5GYC4tr8mo14yVYbh/Ur8jHhWOxniZXii+1+ePiIbsLZ5fEI0QQGtbRRP5mOhmooos+rLmVASGGoq5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.1.1.tgz", + "integrity": "sha512-CGmZ72mL29VMfESz7S6dekqzCh8ZISj3B+w0g1hZFXaOjGTVaSqfAEFAq8EGp8fUL+Q2l8aqNmt8U1tglTikeg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.1.2.tgz", + "integrity": "sha512-NCgr1d0/EdeP6U5PSZ9Uv5SMR5XRRYoVr1kRVtKZxWL3tixEL3UatrPIMFZSKwHlCcp2zPLDvMubVDULRqeunA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.1.2", + "@smithy/types": "^4.5.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.3.2.tgz", + "integrity": "sha512-Ka+FA2UCC/Q1dEqUanCdpqwxOFdf5Dg2VXtPtB1qxLcSGh5C1HdzklIt18xL504Wiy9nNUKwDMRTVCbKGoK69g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.2.1", + "@smithy/node-http-handler": "^4.2.1", + "@smithy/types": "^4.5.0", + "@smithy/util-base64": "^4.1.0", + "@smithy/util-buffer-from": "^4.1.0", + "@smithy/util-hex-encoding": "^4.1.0", + "@smithy/util-utf8": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.1.0.tgz", + "integrity": "sha512-b0EFQkq35K5NHUYxU72JuoheM6+pytEVUGlTwiFxWFpmddA+Bpz3LgsPRIpBk8lnPE47yT7AF2Egc3jVnKLuPg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.1.0.tgz", + "integrity": "sha512-mEu1/UIXAdNYuBcyEPbjScKi/+MQVXNIuY/7Cm5XLIWe319kDrT5SizBE95jqtmEXoDbGoZxKLCMttdZdqTZKQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.1.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.0.0.tgz", + "integrity": "sha512-OlA/yZHh0ekYFnbUkmYBDQPE6fGfdrvgz39ktp8Xf+FA6BfxLejPTMDOG0Nfk5/rDySAz1dRbFf24zaAFYVXlQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@szmarczak/http-timer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", @@ -1714,6 +3081,24 @@ "undici-types": "~7.10.0" } }, + "node_modules/@types/nodemailer": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-7.0.1.tgz", + "integrity": "sha512-UfHAghPmGZVzaL8x9y+mKZMWyHC399+iq0MOmya5tIyenWX3lcdSb60vOmp0DocR6gCDTYTozv/ULQnREyyjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@aws-sdk/client-sesv2": "^3.839.0", + "@types/node": "*" + } + }, + "node_modules/@types/ping": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@types/ping/-/ping-0.4.4.tgz", + "integrity": "sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/relateurl": { "version": "0.2.33", "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.33.tgz", @@ -2073,6 +3458,13 @@ "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", "license": "ISC" }, + "node_modules/bowser": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", + "dev": true, + "license": "MIT" + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", diff --git a/server/package.json b/server/package.json index 637c785cc..2bee352e0 100755 --- a/server/package.json +++ b/server/package.json @@ -54,6 +54,8 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/nodemailer": "7.0.1", + "@types/ping": "0.4.4", "c8": "10.1.3", "chai": "5.2.0", "eslint": "^9.17.0", diff --git a/server/src/config/index.ts b/server/src/config/index.ts new file mode 100644 index 000000000..7ace6f99c --- /dev/null +++ b/server/src/config/index.ts @@ -0,0 +1,25 @@ +import dotenv from "dotenv"; + +export interface IEevConfig { + NODE_ENV: string; + JWT_SECRET: string; + PORT: number; + PAGESPEED_API_KEY: string; + SMTP_HOST: string; + SMTP_PORT: number; + SMTP_USER: string; + SMTP_PASS: string; +} + +dotenv.config(); + +export const config: IEevConfig = { + NODE_ENV: process.env.NODE_ENV || "development", + JWT_SECRET: process.env.JWT_SECRET || "your_jwt_secret", + PORT: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000, + PAGESPEED_API_KEY: process.env.PAGESPEED_API_KEY || "", + SMTP_HOST: process.env.SMTP_HOST || "smtp.example.com", + SMTP_PORT: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587, + SMTP_USER: process.env.SMTP_USER || "user@example.com", + SMTP_PASS: process.env.SMTP_PASS || "your_smtp_password", +}; diff --git a/server/src/service/v2/business/AuthService.ts b/server/src/service/v2/business/AuthService.ts new file mode 100644 index 000000000..cf49a71c6 --- /dev/null +++ b/server/src/service/v2/business/AuthService.ts @@ -0,0 +1,206 @@ +import bcrypt from "bcryptjs"; +import { User, Role, ITokenizedUser, Monitor, Check, NotificationChannel } from "../../../db/v2/models/index.js"; +import ApiError from "../../../utils/ApiError.js"; +import { Types } from "mongoose"; +import { IJobQueue } from "../infrastructure/JobQueue.js"; + +export const PERMISSIONS = { + users: { + all: "users.*", + create: "users.create", + view: "users.view", + update: "users.update", + delete: "users.delete", + }, + + monitors: { + all: "monitors.*", + create: "monitors.create", + view: "monitors.view", + update: "monitors.update", + delete: "monitors.delete", + }, + notifications: { + all: "notifications.*", + create: "notifications.create", + view: "notifications.view", + update: "notifications.update", + delete: "notifications.delete", + }, + checks: { + all: "checks.*", + create: "checks.create", + view: "checks.view", + update: "checks.update", + delete: "checks.delete", + }, + statusPages: { + all: "statusPages.*", + create: "statusPages.create", + view: "statusPages.view", + update: "statusPages.update", + delete: "statusPages.delete", + }, +}; + +const DEFAULT_ROLES = [ + { + name: "SuperAdmin", + description: "Super admin with all permissions", + permissions: ["*"], + isSystem: true, + }, + { + name: "Admin", + description: "Admin with full permissions", + permissions: [PERMISSIONS.monitors.all, PERMISSIONS.users.all], + isSystem: true, + }, + { + name: "Manager", + description: "Can manage users", + permissions: [PERMISSIONS.users.create, PERMISSIONS.users.update, PERMISSIONS.monitors.all], + isSystem: true, + }, + { + name: "Member", + description: "Basic team member", + permissions: [PERMISSIONS.users.update, PERMISSIONS.monitors.create, PERMISSIONS.monitors.view, PERMISSIONS.monitors.update], + isSystem: true, + }, +]; + +export type RegisterData = { + email: string; + firstName: string; + lastName: string; + password: string; + roles?: Types.ObjectId[]; // Optional roles for invite-based registration +}; + +export type LoginData = { + email: string; + password: string; +}; + +export type AuthResult = ITokenizedUser; + +export interface IAuthService { + register(signupData: RegisterData): Promise; + registerWithInvite(signupData: RegisterData): Promise; + login(loginData: LoginData): Promise; + cleanup(): Promise; + cleanMonitors(): Promise; +} + +class AuthService implements IAuthService { + private jobQueue: IJobQueue; + constructor(jobQueue: IJobQueue) { + this.jobQueue = jobQueue; + } + + async register(signupData: RegisterData): Promise { + const userCount = await User.countDocuments(); + + if (userCount > 0) { + throw new Error("Registration is closed. Please request an invite."); + } + + const { email, firstName, lastName, password } = signupData; + + // Create all default roles + const rolePromises = DEFAULT_ROLES.map((roleData) => + new Role({ + ...roleData, + }).save() + ); + const roles = await Promise.all(rolePromises); + + // Hash password and create user + const saltRounds = 12; + const passwordHash = await bcrypt.hash(password, saltRounds); + + // Find admin role and assign to first user + const superAdminRole = roles.find((role) => role.name === "SuperAdmin"); + + const user = new User({ + email, + firstName, + lastName, + passwordHash, + roles: [superAdminRole!._id], + }); + + const savedUser = await user.save(); + return { + sub: savedUser._id.toString(), + roles: savedUser.roles.map((role) => role.toString()), + }; + } + + async registerWithInvite(signupData: RegisterData): Promise { + const { email, firstName, lastName, password, roles } = signupData; + + const saltRounds = 12; + const passwordHash = await bcrypt.hash(password, saltRounds); + const user = new User({ + email, + firstName, + lastName, + passwordHash, + roles: roles || [], + }); + try { + const savedUser = await user.save(); + return { + sub: savedUser._id.toString(), + roles: savedUser.roles.map((role) => role.toString()), + }; + } catch (error: any) { + if (error?.code === 11000) { + const dupError = new ApiError("Email already in use", 409); + dupError.stack = error?.stack; + throw dupError; + } + throw error; + } + } + + async login(loginData: LoginData): Promise { + const { email, password } = loginData; + + // Find user by email + const user = await User.findOne({ email }); + + if (!user) { + throw new Error("Invalid email or password"); + } + + // Check password + const isPasswordValid = await bcrypt.compare(password, user.passwordHash); + if (!isPasswordValid) { + throw new Error("Invalid email or password"); + } + + return { + sub: user._id.toString(), + roles: user.roles.map((role) => role.toString()), + }; + } + + async cleanup() { + await User.deleteMany({}); + await Role.deleteMany({}); + await Monitor.deleteMany({}); + await Check.deleteMany({}); + await NotificationChannel.deleteMany({}); + await this.jobQueue.flush(); + } + + async cleanMonitors() { + await Monitor.deleteMany({}); + await Check.deleteMany({}); + } +} + +export default AuthService; diff --git a/server/src/service/v2/business/CheckService.ts b/server/src/service/v2/business/CheckService.ts new file mode 100644 index 000000000..5c68891a9 --- /dev/null +++ b/server/src/service/v2/business/CheckService.ts @@ -0,0 +1,132 @@ +import { json } from "stream/consumers"; +import { ICheck, Check, Monitor } from "../../../db/v2/models/index.js"; +import type { ISystemInfo, ICaptureInfo } from "../../../db/v2/models/index.js"; +import { MonitorType } from "../../../db/v2/models/monitors/Monitor.js"; +import { StatusResponse } from "../infrastructure/NetworkService.js"; +import type { ICapturePayload, ILighthousePayload } from "../infrastructure/NetworkService.js"; +import mongoose from "mongoose"; + +export interface ICheckService { + buildCheck: (statusResponse: StatusResponse, type: MonitorType) => Promise; + cleanupOrphanedChecks: () => Promise; +} + +class CheckService implements ICheckService { + private isCapturePayload = (payload: any): payload is ICapturePayload => { + if (!payload || typeof payload !== "object") return false; + + if (!("data" in payload) || typeof payload.data !== "object") { + return false; + } + + const data = payload.data as Partial; + if (!data.cpu || typeof data.cpu !== "object" || typeof data.cpu.usage_percent !== "number") { + return false; + } + + if (!data.memory || typeof data.memory !== "object" || typeof data.memory.usage_percent !== "number") { + return false; + } + + if (data.disk && !Array.isArray(data.disk)) { + return false; + } + if (data.net && !Array.isArray(data.net)) { + return false; + } + + if (!("capture" in payload) || typeof payload.capture !== "object") return false; + const capture = payload.capture as Record; + if (typeof capture.version !== "string" || typeof capture.mode !== "string") return false; + + return true; + }; + + private isPagespeedPayload = (payload: any): payload is ILighthousePayload => { + if (!payload || typeof payload !== "object") return false; + + if (!("lighthouseResult" in payload) || typeof payload.lighthouseResult !== "object") { + return false; + } + return true; + }; + + private buildBaseCheck = (statusResponse: StatusResponse) => { + const monitorId = new mongoose.Types.ObjectId(statusResponse.monitorId); + const check = new Check({ + monitorId: monitorId, + type: statusResponse?.type, + status: statusResponse?.status, + message: statusResponse?.message, + responseTime: statusResponse?.responseTime, + timings: statusResponse?.timings, + }); + return check; + }; + + private buildInfrastructureCheck = (statusResponse: StatusResponse) => { + if (!this.isCapturePayload(statusResponse.payload)) { + throw new Error("Invalid payload for infrastructure monitor"); + } + const check = this.buildBaseCheck(statusResponse); + check.system = statusResponse.payload.data; + check.capture = statusResponse.payload.capture; + return check; + }; + + private buildPagespeedCheck = (statusResponse: StatusResponse) => { + if (!this.isPagespeedPayload(statusResponse.payload)) { + throw new Error("Invalid payload for pagespeed monitor"); + } + const check = this.buildBaseCheck(statusResponse); + const lighthouseResult = statusResponse?.payload?.lighthouseResult; + check.lighthouse = { + accessibility: lighthouseResult?.categories?.accessibility?.score || 0, + bestPractices: lighthouseResult?.categories?.["best-practices"]?.score || 0, + seo: lighthouseResult?.categories?.seo?.score || 0, + performance: lighthouseResult?.categories?.performance?.score || 0, + audits: { + cls: lighthouseResult?.audits?.["cumulative-layout-shift"] || {}, + si: lighthouseResult?.audits?.["speed-index"] || {}, + fcp: lighthouseResult?.audits?.["first-contentful-paint"] || {}, + lcp: lighthouseResult?.audits?.["largest-contentful-paint"] || {}, + tbt: lighthouseResult?.audits?.["total-blocking-time"] || {}, + }, + }; + return check; + }; + + buildCheck = async (statusResponse: StatusResponse, type: MonitorType): Promise => { + switch (type) { + case "infrastructure": + return this.buildInfrastructureCheck(statusResponse as StatusResponse); + + case "pagespeed": + return this.buildPagespeedCheck(statusResponse as StatusResponse); + case "http": + case "https": + return this.buildBaseCheck(statusResponse); + + case "ping": + return this.buildBaseCheck(statusResponse); + default: + throw new Error(`Unsupported monitor type: ${type}`); + } + }; + + cleanupOrphanedChecks = async () => { + try { + const monitorIds = await Monitor.find().distinct("_id"); + const result = await Check.deleteMany({ + monitorId: { $nin: monitorIds }, + }); + console.log(`Deleted ${result.deletedCount} orphaned Checks.`); + return true; + } catch (error) { + console.error("Error cleaning up orphaned Checks:", error); + return false; + } + }; +} + +export default CheckService; diff --git a/server/src/service/v2/business/InviteService.ts b/server/src/service/v2/business/InviteService.ts new file mode 100644 index 000000000..1c954d75f --- /dev/null +++ b/server/src/service/v2/business/InviteService.ts @@ -0,0 +1,61 @@ +import crypto from "node:crypto"; +import { ITokenizedUser, IInvite, Invite } from "../../../db/v2/models/index.js"; +import ApiError from "../../../utils/ApiError.js"; + +export interface IInviteService { + create: (tokenizedUser: ITokenizedUser, invite: IInvite) => Promise<{ token: string }>; + getAll: () => Promise; + get: (tokenHash: string) => Promise; + delete: (id: string) => Promise; +} + +class InviteService implements IInviteService { + constructor() {} + + create = async (tokenizedUser: ITokenizedUser, inviteData: IInvite) => { + const token = crypto.randomBytes(32).toString("hex"); + const tokenHash = crypto.createHash("sha256").update(token).digest("hex"); + try { + const invite = await Invite.create({ + ...inviteData, + tokenHash, + createdBy: tokenizedUser.sub, + updatedBy: tokenizedUser.sub, + }); + if (!invite) { + throw new ApiError("Failed to create invite", 500); + } + return { token }; + } catch (error: any) { + if (error?.code === 11000) { + const dupError = new ApiError("Invite with this email already exists", 409); + dupError.stack = error?.stack; + throw dupError; + } + throw error; + } + }; + + get = async (token: string) => { + const tokenHash = crypto.createHash("sha256").update(token).digest("hex"); + const invite = await Invite.findOne({ tokenHash }); + if (!invite) { + throw new ApiError("Invite not found", 404); + } + return invite; + }; + + getAll = async () => { + return Invite.find(); + }; + + delete = async (id: string) => { + const result = await Invite.deleteOne({ _id: id }); + if (!result.deletedCount) { + throw new ApiError("Invite not found", 404); + } + return result.deletedCount === 1; + }; +} + +export default InviteService; diff --git a/server/src/service/v2/business/MaintenanceService.ts b/server/src/service/v2/business/MaintenanceService.ts new file mode 100644 index 000000000..ef8c8152f --- /dev/null +++ b/server/src/service/v2/business/MaintenanceService.ts @@ -0,0 +1,141 @@ +import { ITokenizedUser, IMaintenance, Maintenance } from "../../../db/v2/models/index.js"; +import ApiError from "../../../utils/ApiError.js"; + +export interface IMaintenanceService { + create: ( + tokenizedUser: ITokenizedUser, + + maintenance: IMaintenance + ) => Promise; + getAll: () => Promise; + get: (id: string) => Promise; + toggleActive: (tokenizedUser: ITokenizedUser, id: string) => Promise; + update: (tokenizedUser: ITokenizedUser, id: string, updateData: Partial) => Promise; + delete: (id: string) => Promise; + isInMaintenance: (monitorId: string) => Promise; +} + +type MaintenanceCache = Map; + +class MaintenanceService implements IMaintenanceService { + private maintenanceCache: MaintenanceCache; + private lastRefresh: number; + private CACHE_TTL_MS = 60 * 1000; + + constructor() { + this.maintenanceCache = new Map(); + this.lastRefresh = 0; + } + + create = async (tokenizedUser: ITokenizedUser, maintenanceData: IMaintenance) => { + const maintenance = await Maintenance.create({ + ...maintenanceData, + createdBy: tokenizedUser.sub, + updatedBy: tokenizedUser.sub, + }); + return maintenance; + }; + + get = async (id: string) => { + const maintenance = await Maintenance.findById(id); + if (!maintenance) { + throw new ApiError("Maintenance not found", 404); + } + return maintenance; + }; + + getAll = async () => { + return Maintenance.find(); + }; + + toggleActive = async (tokenizedUser: ITokenizedUser, id: string) => { + const updatedMaintenance = await Maintenance.findOneAndUpdate( + { _id: id }, + [ + { + $set: { + isActive: { $not: "$isActive" }, + updatedBy: tokenizedUser.sub, + updatedAt: new Date(), + }, + }, + ], + { new: true } + ); + if (!updatedMaintenance) { + throw new ApiError("Maintenance not found", 404); + } + return updatedMaintenance; + }; + + update = async (tokenizedUser: ITokenizedUser, id: string, updateData: Partial) => { + const allowedFields: (keyof IMaintenance)[] = ["name", "monitors", "startTime", "endTime", "isActive"]; + const safeUpdate: Partial = {}; + for (const field of allowedFields) { + if (updateData[field] !== undefined) { + (safeUpdate as any)[field] = updateData[field]; + } + } + + const updatedMaintenance = await Maintenance.findByIdAndUpdate( + id, + { + $set: { + ...safeUpdate, + updatedAt: new Date(), + updatedBy: tokenizedUser.sub, + }, + }, + { new: true, runValidators: true } + ); + + if (!updatedMaintenance) { + throw new ApiError("Failed to update maintenance", 500); + } + + return updatedMaintenance; + }; + + delete = async (id: string) => { + const result = await Maintenance.deleteOne({ _id: id }); + if (!result.deletedCount) { + throw new ApiError("Maintenance not found", 404); + } + return result.deletedCount === 1; + }; + + private refreshCache = async () => { + const now = new Date(); + + const activeMaintenances = await Maintenance.find({ + isActive: true, + startTime: { $lte: now }, + endTime: { $gte: now }, + }).lean(); + + // Reset cache + const newCache = new Map(); + + for (const m of activeMaintenances) { + for (const monitorId of m.monitors) { + const key = monitorId.toString(); + if (!newCache.has(key)) newCache.set(key, []); + newCache.get(key)!.push(m); + } + } + + this.maintenanceCache = newCache; + this.lastRefresh = Date.now(); + }; + + isInMaintenance = async (monitorId: string) => { + const now = Date.now(); + if (now - this.lastRefresh > this.CACHE_TTL_MS) { + await this.refreshCache(); + } + const maintenances = this.maintenanceCache.get(monitorId) || []; + return maintenances.length > 0; + }; +} + +export default MaintenanceService; diff --git a/server/src/service/v2/business/MonitorStatsService.ts b/server/src/service/v2/business/MonitorStatsService.ts new file mode 100644 index 000000000..dc7d4acb3 --- /dev/null +++ b/server/src/service/v2/business/MonitorStatsService.ts @@ -0,0 +1,25 @@ +import { MonitorStats } from "../../../db/v2/models/index.js"; +import { Monitor } from "../../../db/v2/models/index.js"; +export interface IMonitorStatsService { + cleanupOrphanedMonitorStats: () => Promise; +} + +class MonitorStatsService implements IMonitorStatsService { + constructor() {} + + async cleanupOrphanedMonitorStats() { + try { + const monitorIds = await Monitor.find().distinct("_id"); + const result = await MonitorStats.deleteMany({ + monitorId: { $nin: monitorIds }, + }); + console.log(`Deleted ${result.deletedCount} orphaned MonitorStats.`); + return true; + } catch (error) { + console.error("Error cleaning up orphaned MonitorStats:", error); + return false; + } + } +} + +export default MonitorStatsService; diff --git a/server/src/service/v2/business/NotificationChannelService.ts b/server/src/service/v2/business/NotificationChannelService.ts new file mode 100644 index 000000000..35eae5b12 --- /dev/null +++ b/server/src/service/v2/business/NotificationChannelService.ts @@ -0,0 +1,100 @@ +import { ITokenizedUser, INotificationChannel, NotificationChannel, Monitor } from "../../../db/v2/models/index.js"; +import ApiError from "../../../utils/ApiError.js"; + +export interface INotificationChannelService { + create: ( + tokenizedUser: ITokenizedUser, + + notificationChannel: INotificationChannel + ) => Promise; + getAll: () => Promise; + get: (id: string) => Promise; + toggleActive: (tokenizedUser: ITokenizedUser, id: string) => Promise; + update: (tokenizedUser: ITokenizedUser, id: string, updateData: Partial) => Promise; + delete: (id: string) => Promise; +} + +class NotificationChannelService implements INotificationChannelService { + constructor() {} + + create = async (tokenizedUser: ITokenizedUser, notificationChannelData: INotificationChannel) => { + const notificationChannel = await NotificationChannel.create({ + ...notificationChannelData, + createdBy: tokenizedUser.sub, + updatedBy: tokenizedUser.sub, + }); + return notificationChannel; + }; + + get = async (id: string) => { + const channel = await NotificationChannel.findById(id); + if (!channel) { + throw new ApiError("Notification channel not found", 404); + } + return channel; + }; + + getAll = async () => { + return NotificationChannel.find(); + }; + + toggleActive = async (tokenizedUser: ITokenizedUser, id: string) => { + const updatedChannel = await NotificationChannel.findOneAndUpdate( + { _id: id }, + [ + { + $set: { + isActive: { $not: "$isActive" }, + updatedBy: tokenizedUser.sub, + updatedAt: new Date(), + }, + }, + ], + { new: true } + ); + if (!updatedChannel) { + throw new ApiError("Notification channel not found", 404); + } + return updatedChannel; + }; + + update = async (tokenizedUser: ITokenizedUser, id: string, updateData: Partial) => { + const allowedFields: (keyof INotificationChannel)[] = ["name", "config", "isActive"]; + const safeUpdate: Partial = {}; + for (const field of allowedFields) { + if (updateData[field] !== undefined) { + (safeUpdate as any)[field] = updateData[field]; + } + } + + const updatedChannel = await NotificationChannel.findByIdAndUpdate( + id, + { + $set: { + ...safeUpdate, + updatedAt: new Date(), + updatedBy: tokenizedUser.sub, + }, + }, + { new: true, runValidators: true } + ); + + if (!updatedChannel) { + throw new ApiError("Failed to update notification channel", 500); + } + + return updatedChannel; + }; + + delete = async (id: string) => { + const result = await NotificationChannel.deleteOne({ _id: id }); + if (!result.deletedCount) { + throw new ApiError("Notification channel not found", 404); + } + + await Monitor.updateMany({ notificationChannels: id }, { $pull: { notificationChannels: id } }); + return result.deletedCount === 1; + }; +} + +export default NotificationChannelService; diff --git a/server/src/service/v2/business/QueueService.ts b/server/src/service/v2/business/QueueService.ts new file mode 100644 index 000000000..20069072e --- /dev/null +++ b/server/src/service/v2/business/QueueService.ts @@ -0,0 +1,23 @@ +import { IJobQueue } from "../infrastructure/JobQueue.js"; + +class QueueService { + private jobQueue: IJobQueue; + + constructor(jobQueue: IJobQueue) { + this.jobQueue = jobQueue; + } + + async getMetrics() { + return await this.jobQueue.getMetrics(); + } + + async getJobs() { + return await this.jobQueue.getJobs(); + } + + async flush() { + return await this.jobQueue.flush(); + } +} + +export default QueueService; diff --git a/server/src/service/v2/business/UserService.ts b/server/src/service/v2/business/UserService.ts new file mode 100644 index 000000000..46d221759 --- /dev/null +++ b/server/src/service/v2/business/UserService.ts @@ -0,0 +1,13 @@ +import { IUser, User } from "../../../db/v2/models/index.js"; + +export interface IUserService { + getAllUsers(): Promise; +} + +class UserService implements IUserService { + async getAllUsers(): Promise { + return await User.find(); + } +} + +export default UserService; diff --git a/server/src/service/v2/infrastructure/JobGenerator.ts b/server/src/service/v2/infrastructure/JobGenerator.ts new file mode 100644 index 000000000..4f2beaf71 --- /dev/null +++ b/server/src/service/v2/infrastructure/JobGenerator.ts @@ -0,0 +1,80 @@ +import { IMonitor } from "../../../db/v2/models/index.js"; +import { INetworkService } from "./NetworkService.js"; +import { ICheckService } from "../business/CheckService.js"; +import { IMonitorStatsService } from "../business/MonitorStatsService.js"; +import { IStatusService } from "./StatusService.js"; +import { INotificationService } from "./NotificationService.js"; +import { IMaintenanceService } from "../business/MaintenanceService.js"; +import ApiError from "../../../utils/ApiError.js"; + +export interface IJobGenerator { + generateJob: () => (Monitor: IMonitor) => Promise; + generateCleanupJob: () => () => Promise; +} + +class JobGenerator implements IJobGenerator { + private networkService: INetworkService; + private checkService: ICheckService; + private monitorStatsService: IMonitorStatsService; + private statusService: IStatusService; + private notificationService: INotificationService; + private maintenanceService: IMaintenanceService; + + constructor( + networkService: INetworkService, + checkService: ICheckService, + monitorStatsService: IMonitorStatsService, + statusService: IStatusService, + notificationService: INotificationService, + maintenanceService: IMaintenanceService + ) { + this.networkService = networkService; + this.checkService = checkService; + this.monitorStatsService = monitorStatsService; + this.statusService = statusService; + this.notificationService = notificationService; + this.maintenanceService = maintenanceService; + } + + generateJob = () => { + return async (monitor: IMonitor) => { + try { + const monitorId = monitor._id.toString(); + if (!monitorId) { + throw new ApiError("No monitorID for creating job", 400); + } + + // Check for active maintenance window, if found, skip the check + const isInMaintenance = await this.maintenanceService.isInMaintenance(monitorId); + if (isInMaintenance) { + return; + } + + const status = await this.networkService.requestStatus(monitor); + const check = await this.checkService.buildCheck(status, monitor.type); + await check.save(); + const [updatedMonitor, statusChanged] = await this.statusService.updateMonitorStatus(monitor, status); + + if (statusChanged) { + await this.notificationService.handleNotifications(updatedMonitor); + } + await this.statusService.updateMonitorStats(updatedMonitor, status, statusChanged); + } catch (error) { + throw error; + } + }; + }; + + generateCleanupJob = () => { + return async () => { + try { + await this.checkService.cleanupOrphanedChecks(); + await this.monitorStatsService.cleanupOrphanedMonitorStats(); + } catch (error) { + throw error; + } + }; + }; +} + +export default JobGenerator; diff --git a/server/src/service/v2/infrastructure/JobQueue.ts b/server/src/service/v2/infrastructure/JobQueue.ts new file mode 100644 index 000000000..af415d3f7 --- /dev/null +++ b/server/src/service/v2/infrastructure/JobQueue.ts @@ -0,0 +1,227 @@ +import { IJob } from "super-simple-scheduler/dist/job/job.js"; +import { Monitor, IMonitor } from "../../../db/v2/models/index.js"; +import Scheduler from "super-simple-scheduler"; +import { IJobGenerator } from "./JobGenerator.js"; +export interface IJobMetrics { + jobs: number; + activeJobs: number; + failingJobs: number; + jobsWithFailures: Array<{ + monitorId: string | number; + monitorUrl: string | null; + monitorType: string | null; + failedAt: number | null; + failCount: number | null; + failReason: string | null; + }>; + totalRuns: number; + totalFailures: number; +} + +export interface IJobData extends IJob { + lastRunTook: number | null; +} + +export interface IJobQueue { + init: () => Promise; + addJob: (monitor: IMonitor) => Promise; + pauseJob: (monitor: IMonitor) => Promise; + resumeJob: (monitor: IMonitor) => Promise; + updateJob: (monitor: IMonitor) => Promise; + deleteJob: (monitor: IMonitor) => Promise; + getMetrics: () => Promise; + getJobs: () => Promise; + flush: () => Promise; + shutdown: () => Promise; +} + +export default class JobQueue implements IJobQueue { + private scheduler: Scheduler; + private static instance: JobQueue | null = null; + private jobGenerator: any; + constructor() { + this.scheduler = new Scheduler({ + logLevel: "debug", + }); + } + + static async create(jobGenerator: IJobGenerator) { + if (!JobQueue.instance) { + const instance = new JobQueue(); + instance.jobGenerator = jobGenerator; + await instance.init(); + JobQueue.instance = instance; + } + return JobQueue.instance; + } + + static getInstance(): JobQueue | null { + return JobQueue.instance; + } + + init = async () => { + try { + this.scheduler.start(); + // Add template and jobs + this.scheduler.addTemplate("monitor-job", this.jobGenerator.generateJob()); + + // Add a cleanup job + this.scheduler.addTemplate("cleanup-job", this.jobGenerator.generateCleanupJob()); + await this.scheduler.addJob({ + id: "cleanup-orphaned-checks", + template: "cleanup-job", + repeat: 24 * 60 * 60 * 1000, // 24 hours + active: true, + }); + + const monitors = await Monitor.find(); + for (const monitor of monitors) { + this.addJob(monitor); + } + + return true; + } catch (error) { + console.error(error); + return false; + } + }; + + addJob = async (monitor: IMonitor) => { + try { + return await this.scheduler?.addJob({ + id: monitor._id.toString(), + template: "monitor-job", + repeat: monitor.interval, + active: monitor.isActive, + data: monitor, + }); + } catch (error) { + console.error(error); + return false; + } + }; + + pauseJob = async (monitor: IMonitor) => { + try { + return await this.scheduler?.pauseJob(monitor._id.toString()); + } catch (error) { + console.error(error); + return false; + } + }; + + resumeJob = async (monitor: IMonitor) => { + try { + return await this.scheduler.resumeJob(monitor._id.toString()); + } catch (error) { + console.error(error); + return false; + } + }; + + updateJob = async (monitor: IMonitor) => { + try { + return await this.scheduler.updateJob(monitor._id.toString(), { + repeat: monitor.interval, + data: monitor, + }); + } catch (error) { + console.error(error); + return false; + } + }; + + deleteJob = async (monitor: IMonitor) => { + try { + this.scheduler?.removeJob(monitor._id.toString()); + return true; + } catch (error) { + console.error(error); + return false; + } + }; + + getMetrics = async (): Promise => { + try { + const jobs = await this.scheduler.getJobs(); + const metrics: IJobMetrics = jobs.reduce( + (acc, job) => { + if (!job.data) return acc; + + acc.totalRuns += job.runCount || 0; + acc.totalFailures += job.failCount || 0; + acc.jobs++; + + // Check if job is currently failing (has recent failures) + const hasFailures = job.failCount && job.failCount > 0; + const isCurrentlyFailing = hasFailures && job.lastFailedAt && (!job.lastRunAt || job.lastFailedAt > job.lastRunAt); + + if (isCurrentlyFailing) { + acc.failingJobs++; + } + + if (job.lockedAt) { + acc.activeJobs++; + } + + if (hasFailures) { + acc.jobsWithFailures.push({ + monitorId: job.id, + monitorUrl: job.data?.url || null, + monitorType: job.data?.type || null, + failedAt: job.lastFailedAt || null, + failCount: job.failCount || null, + failReason: job.lastFailReason || null, + }); + } + return acc; + }, + { + jobs: 0, + activeJobs: 0, + failingJobs: 0, + jobsWithFailures: [], + totalRuns: 0, + totalFailures: 0, + } + ); + return metrics; + } catch (error) { + console.error(error); + return null; + } + }; + + getJobs = async (): Promise => { + try { + const jobs = await this.scheduler.getJobs(); + return jobs.map((job) => { + return { + ...job, + lastRunTook: job.lockedAt || !job.lastFinishedAt || !job.lastRunAt ? null : job.lastFinishedAt - job.lastRunAt, + }; + }); + } catch (error) { + console.error(error); + return null; + } + }; + + flush = async () => { + try { + return await this.scheduler.flushJobs(); + } catch (error) { + console.error(error); + return false; + } + }; + + shutdown = async () => { + try { + return await this.scheduler.stop(); + } catch (error) { + console.error(error); + return false; + } + }; +} diff --git a/server/src/service/v2/infrastructure/NetworkService.ts b/server/src/service/v2/infrastructure/NetworkService.ts new file mode 100644 index 000000000..90657bd2d --- /dev/null +++ b/server/src/service/v2/infrastructure/NetworkService.ts @@ -0,0 +1,193 @@ +import { Got, HTTPError } from "got"; +import ping from "ping"; +import { IMonitor } from "../../../db/v2/models/index.js"; +import { GotTimings } from "../../../db/v2/models/checks/Check.js"; +import type { Response } from "got"; +import type { ISystemInfo, ICaptureInfo, ILighthouseResult } from "../../../db/v2/models/index.js"; +import { MonitorType, MonitorStatus } from "../../../db/v2/models/monitors/Monitor.js"; +import ApiError from "../../../utils/ApiError.js"; +import { config } from "../../../config/index.js"; +export interface INetworkService { + requestHttp: (monitor: IMonitor) => Promise; + requestInfrastructure: (monitor: IMonitor) => Promise; + requestStatus: (monitor: IMonitor) => Promise; + requestPagespeed: (monitor: IMonitor) => Promise; + requestPing: (monitor: IMonitor) => Promise; +} + +export interface ICapturePayload { + data: ISystemInfo; + capture: ICaptureInfo; +} + +export interface ILighthousePayload { + lighthouseResult: ILighthouseResult; +} + +export interface StatusResponse { + monitorId: string; + type: MonitorType; + code?: number; + status: MonitorStatus; + message: string; + responseTime: number; + timings?: GotTimings; + payload?: TPayload; +} + +class NetworkService implements INetworkService { + private got: Got; + private NETWORK_ERROR: number; + constructor(got: Got) { + this.got = got; + this.NETWORK_ERROR = 5000; + } + + private buildStatusResponse = (monitor: IMonitor, response: Response | null, error: any | null): StatusResponse => { + if (error) { + const statusResponse: StatusResponse = { + monitorId: monitor._id.toString(), + type: monitor.type, + status: "down" as MonitorStatus, + code: this.NETWORK_ERROR, + message: error.message || "Network error", + responseTime: 0, + timings: { phases: {} } as GotTimings, + }; + if (error instanceof HTTPError) { + statusResponse.code = error?.response?.statusCode || this.NETWORK_ERROR; + statusResponse.message = error.message || "HTTP error"; + statusResponse.responseTime = error.timings?.phases?.total || 0; + statusResponse.timings = error.timings; + } + return statusResponse; + } + + const statusResponse: StatusResponse = { + monitorId: monitor._id.toString(), + type: monitor.type, + code: response?.statusCode || this.NETWORK_ERROR, + status: response?.ok === true ? "up" : "down", + message: response?.statusMessage || "", + responseTime: response?.timings?.phases?.total || 0, + timings: response?.timings || ({ phases: {} } as GotTimings), + }; + + return statusResponse; + }; + + requestHttp = async (monitor: IMonitor) => { + try { + const url = monitor.url; + if (!url) { + throw new Error("No URL provided"); + } + + try { + const response: Response = await this.got(url); + return this.buildStatusResponse(monitor, response, null); + } catch (error) { + return this.buildStatusResponse(monitor, null, error); + } + } catch (error) { + throw error; + } + }; + + requestInfrastructure = async (monitor: IMonitor) => { + const url = monitor.url; + if (!url) { + throw new Error("No URL provided"); + } + const secret = monitor.secret; + if (!secret) { + throw new Error("No secret provided for infrastructure monitor"); + } + + let statusResponse: StatusResponse; + try { + const response: Response | null = await this.got(url, { + headers: { Authorization: `Bearer ${secret}` }, + responseType: "json", + }); + + statusResponse = this.buildStatusResponse(monitor, response, null); + if (!response?.body) { + throw new ApiError("No payload received from infrastructure monitor", 500); + } + statusResponse.payload = response?.body; + return statusResponse; + } catch (error) { + statusResponse = this.buildStatusResponse(monitor, null, error); + } + return statusResponse; + }; + + requestPagespeed = async (monitor: IMonitor) => { + const apiKey = config.PAGESPEED_API_KEY; + if (!apiKey) { + throw new Error("No API key provided for pagespeed monitor"); + } + const url = monitor.url; + if (!url) { + throw new Error("No URL provided"); + } + + let statusResponse: StatusResponse; + + try { + const response: Response = await this.got(url); + statusResponse = this.buildStatusResponse(monitor, response, null) as StatusResponse; + } catch (error) { + statusResponse = this.buildStatusResponse(monitor, null, error); + } + + const pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance&key=${apiKey}`; + const pagespeedResponse = await this.got(pagespeedUrl, { + responseType: "json", + }); + const payload = pagespeedResponse.body; + if (payload) { + statusResponse.payload = payload; + return statusResponse; + } else { + throw new ApiError("No payload received from pagespeed monitor", 500); + } + }; + + requestPing = async (monitor: IMonitor) => { + const response = await ping.promise.probe(monitor.url); + const status = response?.alive === true ? "up" : "down"; + + const rawTime = typeof response?.time === "string" ? parseFloat(response.time) : Number(response?.time); + const responseTime = Number.isFinite(rawTime) ? rawTime : 0; + + return { + monitorId: monitor._id.toString(), + type: monitor.type, + status: status as MonitorStatus, + message: "Ping successful", + responseTime, + timings: { phases: {} } as GotTimings, + }; + }; + + requestStatus = async (monitor: IMonitor) => { + switch (monitor?.type) { + case "http": + return await this.requestHttp(monitor); // uses GOT + case "https": + return await this.requestHttp(monitor); // uses GOT + case "infrastructure": + return await this.requestInfrastructure(monitor); // uses GOT + case "pagespeed": + return await this.requestPagespeed(monitor); // uses GOT + case "ping": + return await this.requestPing(monitor); // uses PING + default: + throw new Error("Not implemented"); + } + }; +} + +export default NetworkService; diff --git a/server/src/service/v2/infrastructure/NotificationService.ts b/server/src/service/v2/infrastructure/NotificationService.ts new file mode 100644 index 000000000..4195958bd --- /dev/null +++ b/server/src/service/v2/infrastructure/NotificationService.ts @@ -0,0 +1,58 @@ +import UserService from "../business/UserService.js"; +import { IMonitor, NotificationChannel } from "../../../db/v2/models/index.js"; +import { EmailService, SlackService, DiscordService, WebhookService } from "./NotificationServices/index.js"; +export interface INotificationService { + handleNotifications: (monitor: IMonitor) => Promise; +} + +class NotificationService implements INotificationService { + private emailService: EmailService; + private slackService: SlackService; + private discordService: DiscordService; + private webhookService: WebhookService; + private userService: UserService; + + constructor(userService: UserService) { + this.userService = userService; + this.emailService = new EmailService(userService); + this.slackService = new SlackService(); + this.discordService = new DiscordService(); + this.webhookService = new WebhookService(); + } + + handleNotifications = async (monitor: IMonitor) => { + const notificationIds = monitor.notificationChannels || []; + + if (notificationIds.length === 0) { + return; + } + + const notificationChannels = await NotificationChannel.find({ + _id: { $in: notificationIds }, + }); + + for (const channel of notificationChannels) { + // Implement sending logic based on channel.type and channel.config + let service; + switch (channel.type) { + case "email": + await this.emailService.sendMessage(this.emailService.buildAlert(monitor), channel); + break; + case "slack": + await this.slackService.sendMessage(this.slackService.buildAlert(monitor), channel); + break; + case "discord": + await this.discordService.sendMessage(this.discordService.buildAlert(monitor), channel); + break; + case "webhook": + await this.webhookService.sendMessage(this.webhookService.buildAlert(monitor), channel); + break; + default: + console.warn(`Unknown notification channel type: ${channel.type}`); + } + } + return; + }; +} + +export default NotificationService; diff --git a/server/src/service/v2/infrastructure/NotificationServices/Discord.ts b/server/src/service/v2/infrastructure/NotificationServices/Discord.ts new file mode 100644 index 000000000..c9ec30700 --- /dev/null +++ b/server/src/service/v2/infrastructure/NotificationServices/Discord.ts @@ -0,0 +1,73 @@ +import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js"; +import { IAlert, IMessageService } from "./IMessageService.js"; +import got from "got"; +import ApiError from "../../../../utils/ApiError.js"; +class DiscordService implements IMessageService { + constructor() {} + + private toDiscordEmbeds = (alert: IAlert) => { + return { + color: alert.status === "up" ? 65280 : 16711680, + title: `Monitor name: ${alert.name}`, + description: `Status: **${alert.status}**`, + fields: [ + { + name: "Url", + value: alert.url, + }, + { + name: "Checked at", + value: alert.checkTime ? alert.checkTime.toISOString() : "N/A", + }, + { name: "Alert time", value: alert.alertTime.toISOString() }, + ...(alert.details + ? Object.entries(alert.details).map(([key, value]) => ({ + name: key, + value, + })) + : []), + ], + }; + }; + + buildAlert = (monitor: IMonitor) => { + const name = monitor?.name || "Unnamed monitor"; + const monitorStatus = monitor?.status || "unknown status"; + const url = monitor?.url || "no URL"; + const checkTime = monitor?.lastCheckedAt || null; + const alertTime = new Date(); + return { + name, + url, + status: monitorStatus, + checkTime, + alertTime, + }; + }; + + sendMessage = async (alert: IAlert, channel: INotificationChannel) => { + const notificationUrl = channel?.config?.url; + if (!notificationUrl) { + throw new ApiError("Webhook URL not configured", 400); + } + + try { + const payload = { + content: "Status Alert", + embeds: [this.toDiscordEmbeds(alert)], + }; + await got.post(notificationUrl, { json: payload }); + } catch (error) { + console.warn("Failed to send Discord message", error); + return false; + } + + return true; + }; + + testMessage = async () => { + return true; + }; +} + +export default DiscordService; diff --git a/server/src/service/v2/infrastructure/NotificationServices/Email.ts b/server/src/service/v2/infrastructure/NotificationServices/Email.ts new file mode 100644 index 000000000..d1443ce08 --- /dev/null +++ b/server/src/service/v2/infrastructure/NotificationServices/Email.ts @@ -0,0 +1,65 @@ +import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js"; +import { IMessageService, IAlert } from "./IMessageService.js"; +import nodemailer, { Transporter } from "nodemailer"; +import { config } from "../../../../config/index.js"; +import UserService from "../../business/UserService.js"; +import ApiError from "../../../../utils/ApiError.js"; +class EmailService implements IMessageService { + private transporter: Transporter; + private userService: UserService; + + constructor(userService: UserService) { + this.userService = userService; + this.transporter = nodemailer.createTransport({ + host: config.SMTP_HOST, + port: config.SMTP_PORT, + secure: config.SMTP_PORT === 465, + auth: { + user: config.SMTP_USER, + pass: config.SMTP_PASS, + }, + }); + } + + buildAlert = (monitor: IMonitor) => { + const name = monitor?.name || "Unnamed monitor"; + const monitorStatus = monitor?.status || "unknown status"; + const url = monitor?.url || "no URL"; + const checkTime = monitor?.lastCheckedAt || null; + const alertTime = new Date(); + return { + name, + url, + status: monitorStatus, + checkTime, + alertTime, + }; + }; + + sendMessage = async (alert: string | IAlert, channel: INotificationChannel) => { + try { + const users = await this.userService.getAllUsers(); + const emails = users.map((u) => u.email).join(","); + + if (!emails || emails.length === 0) { + throw new ApiError("No user emails found", 500); + } + + await this.transporter.sendMail({ + from: `"Checkmate" <${config.SMTP_USER}>`, + to: emails, + subject: "Monitor Alert", + text: JSON.stringify(alert, null, 2), + }); + return true; + } catch (error) { + return false; + } + }; + + testMessage = async () => { + return true; + }; +} + +export default EmailService; diff --git a/server/src/service/v2/infrastructure/NotificationServices/IMessageService.ts b/server/src/service/v2/infrastructure/NotificationServices/IMessageService.ts new file mode 100644 index 000000000..dbafc59fb --- /dev/null +++ b/server/src/service/v2/infrastructure/NotificationServices/IMessageService.ts @@ -0,0 +1,15 @@ +import { IMonitor, INotificationChannel } from "../../../db/v2/models/index.js"; + +export interface IAlert { + name: string; + url: string; + status: string; + details?: Record; + checkTime: Date | null; + alertTime: Date; +} +export interface IMessageService { + buildAlert: (monitor: IMonitor) => IAlert; + sendMessage: (alert: IAlert, channel: INotificationChannel) => Promise; + testMessage: (message: string, channel: INotificationChannel) => Promise; +} diff --git a/server/src/service/v2/infrastructure/NotificationServices/Slack.ts b/server/src/service/v2/infrastructure/NotificationServices/Slack.ts new file mode 100644 index 000000000..50e2021e4 --- /dev/null +++ b/server/src/service/v2/infrastructure/NotificationServices/Slack.ts @@ -0,0 +1,116 @@ +import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js"; +import { IAlert, IMessageService } from "./IMessageService.js"; +import got from "got"; + +class SlackService implements IMessageService { + constructor() {} + + private toSlackBlocks = (alert: IAlert) => { + return [ + { + type: "header", + text: { + type: "plain_text", + text: "Status Alert", + }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `*Monitor name:* ${alert.name}`, + }, + }, + + { + type: "section", + text: { + type: "mrkdwn", + text: `*Status:* ${alert.status}`, + }, + }, + + { + type: "section", + text: { + type: "mrkdwn", + text: `*URL:* ${alert.url}`, + }, + }, + { + type: "section", + text: { + type: "mrkdwn", + text: `*Checked at:* ${alert?.checkTime?.toISOString() || "N/A"}`, + }, + }, + + { + type: "divider", + }, + + ...(alert.details + ? Object.entries(alert.details).map(([key, value]) => ({ + type: "section", + fields: [ + { + type: "mrkdwn", + text: `*${key}:* ${value}`, + }, + ], + })) + : []), + + { + type: "context", + elements: [ + { + type: "mrkdwn", + text: `*Alert generated at:* ${alert?.alertTime?.toISOString() || "N/A"}`, + }, + ], + }, + ]; + }; + + buildAlert = (monitor: IMonitor) => { + const name = monitor?.name || "Unnamed monitor"; + const monitorStatus = monitor?.status || "unknown status"; + const url = monitor?.url || "no URL"; + const checkTime = monitor?.lastCheckedAt || null; + const alertTime = new Date(); + return { + name, + url, + status: monitorStatus, + checkTime, + alertTime, + }; + }; + + sendMessage = async (alert: IAlert, channel: INotificationChannel) => { + const notificationUrl = channel?.config?.url; + if (!notificationUrl) { + throw new Error("Webhook URL not configured"); + } + + try { + const payload = { + text: "Status Alert", + blocks: this.toSlackBlocks(alert), + }; + await got.post(notificationUrl, { json: payload }); + } catch (error) { + console.warn("Error sending Slack message:", error); + return false; + } + + return true; + }; + + testMessage = async () => { + return true; + }; +} + +export default SlackService; diff --git a/server/src/service/v2/infrastructure/NotificationServices/Webhook.ts b/server/src/service/v2/infrastructure/NotificationServices/Webhook.ts new file mode 100644 index 000000000..b84b2b220 --- /dev/null +++ b/server/src/service/v2/infrastructure/NotificationServices/Webhook.ts @@ -0,0 +1,43 @@ +import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js"; +import { IAlert, IMessageService } from "./IMessageService.js"; +import ApiError from "../../../../utils/ApiError.js"; +import got from "got"; +class WebhookService implements IMessageService { + constructor() {} + + buildAlert = (monitor: IMonitor) => { + const name = monitor?.name || "Unnamed monitor"; + const monitorStatus = monitor?.status || "unknown status"; + const url = monitor?.url || "no URL"; + const checkTime = monitor?.lastCheckedAt || null; + const alertTime = new Date(); + return { + name, + url, + status: monitorStatus, + checkTime, + alertTime, + }; + }; + + sendMessage = async (alert: IAlert, channel: INotificationChannel) => { + const notificationUrl = channel?.config?.url; + if (!notificationUrl) { + throw new ApiError("Webhook URL not configured", 400); + } + try { + await got.post(notificationUrl, { json: { ...alert } }); + } catch (error) { + console.warn("Failed to send webhook notification:", error); + return false; + } + + return true; + }; + + testMessage = async () => { + return true; + }; +} + +export default WebhookService; diff --git a/server/src/service/v2/infrastructure/NotificationServices/index.ts b/server/src/service/v2/infrastructure/NotificationServices/index.ts new file mode 100644 index 000000000..2b949f716 --- /dev/null +++ b/server/src/service/v2/infrastructure/NotificationServices/index.ts @@ -0,0 +1,5 @@ +export { default as DiscordService } from "./Discord.js"; +export { default as EmailService } from "./Email.js"; +export { default as SlackService } from "./Slack.js"; +export { default as WebhookService } from "./Webhook.js"; +export * from "./IMessageService.js"; diff --git a/server/src/service/v2/infrastructure/StatusService.ts b/server/src/service/v2/infrastructure/StatusService.ts new file mode 100644 index 000000000..87b0df0af --- /dev/null +++ b/server/src/service/v2/infrastructure/StatusService.ts @@ -0,0 +1,100 @@ +import { IMonitor, IMonitorStats, MonitorStats } from "../../../db/v2/models/index.js"; +import { StatusResponse } from "./NetworkService.js"; +import ApiError from "../../../utils/ApiError.js"; + +const MAX_LATEST_CHECKS = 25; +export interface IStatusService { + updateMonitorStatus: (monitor: IMonitor, status: StatusResponse) => Promise; + + calculateAvgResponseTime: (stats: IMonitorStats, statusResponse: StatusResponse) => number; + + updateMonitorStats: (monitor: IMonitor, status: StatusResponse, statusChanged: boolean) => Promise; +} + +export type StatusChangeResult = [updatedMonitor: IMonitor, statusChanged: boolean]; + +class StatusService implements IStatusService { + updateMonitorStatus = async (monitor: IMonitor, statusResponse: StatusResponse): Promise => { + const newStatus = statusResponse.status; + monitor.lastCheckedAt = new Date(); + + // Store latest checks for display + monitor.latestChecks = monitor.latestChecks || []; + monitor.latestChecks.push({ + status: newStatus, + responseTime: statusResponse.responseTime, + checkedAt: monitor.lastCheckedAt, + }); + while (monitor.latestChecks.length > MAX_LATEST_CHECKS) { + monitor.latestChecks.shift(); + } + + // Update monitor status + if (monitor.status === "initializing") { + monitor.status = newStatus; + return [await monitor.save(), true]; + } else { + const { n } = monitor; + const latestChecks = monitor.latestChecks.slice(-n); + // Return early if not enough statuses to evaluate + if (latestChecks.length < n) { + return [await monitor.save(), false]; + } + + // If all different than current status, update status + const allDifferent = latestChecks.every((check) => check.status !== monitor.status); + if (allDifferent && monitor.status !== newStatus) { + monitor.status = newStatus; + } + return [await monitor.save(), allDifferent]; + } + }; + + calculateAvgResponseTime = (stats: IMonitorStats, statusResponse: StatusResponse): number => { + let avgResponseTime = stats.avgResponseTime; + // Set initial + if (avgResponseTime === 0) { + avgResponseTime = statusResponse.responseTime; + } else { + avgResponseTime = (avgResponseTime * (stats.totalChecks - 1) + statusResponse.responseTime) / stats.totalChecks; + } + return avgResponseTime; + }; + + updateMonitorStats = async (monitor: IMonitor, statusResponse: StatusResponse, statusChanged: boolean) => { + const stats = await MonitorStats.findOne({ monitorId: monitor._id }); + if (!stats) { + throw new ApiError("MonitorStats not found", 500); + } + + // Update check counts + stats.totalChecks += 1; + stats.totalUpChecks += statusResponse.status === "up" ? 1 : 0; + stats.totalDownChecks += statusResponse.status === "down" ? 1 : 0; + + // Update streak + if (!statusChanged) { + stats.currentStreak += 1; + } else { + stats.currentStreak = 1; + stats.currentStreakStatus = statusResponse.status; + stats.currentStreakStartedAt = Date.now(); + } + + // Update time stamps + stats.lastCheckTimestamp = Date.now(); + stats.timeOfLastFailure = statusResponse.status === "down" ? Date.now() : stats.timeOfLastFailure; + + // Update stats that need updated check counts + stats.avgResponseTime = this.calculateAvgResponseTime(stats, statusResponse); + stats.uptimePercentage = stats.totalUpChecks / stats.totalChecks; + + // Other + stats.lastResponseTime = statusResponse.responseTime; + stats.maxResponseTime = Math.max(stats.maxResponseTime, statusResponse.responseTime); + + return await stats.save(); + }; +} + +export default StatusService; diff --git a/server/src/utils/ApiError.ts b/server/src/utils/ApiError.ts new file mode 100644 index 000000000..313d01c2a --- /dev/null +++ b/server/src/utils/ApiError.ts @@ -0,0 +1,15 @@ +class ApiError extends Error { + public status: number; + + constructor(message: string, status: number = 500) { + super(message); + this.status = status; + this.name = this.constructor.name; + + if (Error.captureStackTrace) { + Error.captureStackTrace(this, this.constructor); + } + } +} + +export default ApiError; diff --git a/server/src/utils/JWTUtils.ts b/server/src/utils/JWTUtils.ts new file mode 100644 index 000000000..4eebb78a0 --- /dev/null +++ b/server/src/utils/JWTUtils.ts @@ -0,0 +1,22 @@ +import jwt from "jsonwebtoken"; +import { AuthResult } from "../service/v2/business/AuthService.js"; + +const encode = (data: AuthResult): string => { + const secret = process.env.JWT_SECRET; + if (!secret) { + throw new Error("JWT_SECRET is not defined"); + } + const token = jwt.sign(data, secret, { expiresIn: "99d" }); + return token; +}; + +const decode = (token: string): AuthResult => { + const secret = process.env.JWT_SECRET; + if (!secret) { + throw new Error("JWT_SECRET is not defined"); + } + const decoded = jwt.verify(token, secret) as AuthResult; + return decoded; +}; + +export { encode, decode }; From 66e5441bdf6d8cea0af92d94d074705473cb5a5c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 23 Sep 2025 15:23:20 -0700 Subject: [PATCH 016/400] fix import path --- .../v2/infrastructure/NotificationServices/IMessageService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/service/v2/infrastructure/NotificationServices/IMessageService.ts b/server/src/service/v2/infrastructure/NotificationServices/IMessageService.ts index dbafc59fb..f00a20549 100644 --- a/server/src/service/v2/infrastructure/NotificationServices/IMessageService.ts +++ b/server/src/service/v2/infrastructure/NotificationServices/IMessageService.ts @@ -1,4 +1,4 @@ -import { IMonitor, INotificationChannel } from "../../../db/v2/models/index.js"; +import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.js"; export interface IAlert { name: string; From 76f4e974db348a2e6248f1824719e0157baa37d6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 24 Sep 2025 14:20:30 -0700 Subject: [PATCH 017/400] dev types --- server/package-lock.json | 19 +++++++++++++++++++ server/package.json | 1 + server/src/routes/v2/auth.ts | 0 3 files changed, 20 insertions(+) create mode 100644 server/src/routes/v2/auth.ts diff --git a/server/package-lock.json b/server/package-lock.json index 92d258433..2a522ae64 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -45,6 +45,7 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/jsonwebtoken": "9.0.10", "@types/nodemailer": "7.0.1", "@types/ping": "0.4.4", "c8": "10.1.3", @@ -3072,6 +3073,24 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/jsonwebtoken": { + "version": "9.0.10", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.10.tgz", + "integrity": "sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/ms": "*", + "@types/node": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "24.3.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", diff --git a/server/package.json b/server/package.json index 2bee352e0..2fdcf58cb 100755 --- a/server/package.json +++ b/server/package.json @@ -54,6 +54,7 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/jsonwebtoken": "9.0.10", "@types/nodemailer": "7.0.1", "@types/ping": "0.4.4", "c8": "10.1.3", diff --git a/server/src/routes/v2/auth.ts b/server/src/routes/v2/auth.ts new file mode 100644 index 000000000..e69de29bb From 6d0c56094837a2597cb55f776e7f96d642730e4a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 24 Sep 2025 15:55:09 -0700 Subject: [PATCH 018/400] add v2 routes, controllers --- server/src/config/controllers.js | 18 +- server/src/config/routes.js | 24 + server/src/config/services.js | 73 ++- server/src/controllers/v2/AuthController.ts | 150 ++++++ server/src/controllers/v2/InviteController.ts | 62 +++ .../controllers/v2/MaintenanceController.ts | 96 ++++ .../src/controllers/v2/MonitorController.ts | 157 ++++++ .../v2/NotificationChannelController.ts | 96 ++++ server/src/controllers/v2/QueueController.ts | 29 ++ server/src/middleware/v2/VerifyPermissions.ts | 77 +++ server/src/middleware/v2/VerifyToken.ts | 21 + server/src/routes/v2/auth.ts | 33 ++ server/src/routes/v2/invite.ts | 30 ++ server/src/routes/v2/maintenance.ts | 34 ++ server/src/routes/v2/monitors.ts | 34 ++ server/src/routes/v2/notificationChannels.ts | 34 ++ server/src/routes/v2/queue.ts | 24 + server/src/service/v2/business/AuthService.ts | 4 + .../src/service/v2/business/CheckService.ts | 3 + .../src/service/v2/business/InviteService.ts | 2 + .../service/v2/business/MaintenanceService.ts | 3 + .../src/service/v2/business/MonitorService.ts | 468 ++++++++++++++++++ .../v2/business/MonitorStatsService.ts | 3 + .../v2/business/NotificationChannelService.ts | 4 + .../src/service/v2/business/QueueService.ts | 3 + server/src/service/v2/business/UserService.ts | 2 + .../service/v2/infrastructure/JobGenerator.ts | 2 + .../src/service/v2/infrastructure/JobQueue.ts | 4 + .../v2/infrastructure/NetworkService.ts | 6 +- .../v2/infrastructure/NotificationService.ts | 3 + .../NotificationServices/Discord.ts | 3 + .../NotificationServices/Email.ts | 3 + .../NotificationServices/Slack.ts | 2 + .../NotificationServices/Webhook.ts | 4 + .../v2/infrastructure/StatusService.ts | 2 + server/src/types/express.d.ts | 10 + server/src/types/index.ts | 1 + .../src/types/monitor-response-with-checks.ts | 11 + 38 files changed, 1532 insertions(+), 3 deletions(-) create mode 100644 server/src/controllers/v2/AuthController.ts create mode 100644 server/src/controllers/v2/InviteController.ts create mode 100644 server/src/controllers/v2/MaintenanceController.ts create mode 100644 server/src/controllers/v2/MonitorController.ts create mode 100644 server/src/controllers/v2/NotificationChannelController.ts create mode 100644 server/src/controllers/v2/QueueController.ts create mode 100644 server/src/middleware/v2/VerifyPermissions.ts create mode 100644 server/src/middleware/v2/VerifyToken.ts create mode 100644 server/src/routes/v2/invite.ts create mode 100644 server/src/routes/v2/maintenance.ts create mode 100644 server/src/routes/v2/monitors.ts create mode 100644 server/src/routes/v2/notificationChannels.ts create mode 100644 server/src/routes/v2/queue.ts create mode 100644 server/src/service/v2/business/MonitorService.ts create mode 100644 server/src/types/express.d.ts create mode 100644 server/src/types/index.ts create mode 100644 server/src/types/monitor-response-with-checks.ts diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js index 5c54579eb..78cd7091b 100644 --- a/server/src/config/controllers.js +++ b/server/src/config/controllers.js @@ -2,7 +2,7 @@ import { createCommonDependencies } from "../controllers/v1/baseController.js"; // Services -// Controllers +// V1 Controllers import MonitorController from "../controllers/v1/monitorController.js"; import AuthController from "../controllers/v1/authController.js"; import SettingsController from "../controllers/v1/settingsController.js"; @@ -15,10 +15,18 @@ import StatusPageController from "../controllers/v1/statusPageController.js"; import NotificationController from "../controllers/v1/notificationController.js"; import DiagnosticController from "../controllers/v1/diagnosticController.js"; +// V2 Controllers +import AuthControllerV2 from "../controllers/v2/AuthController.js"; +import InviteControllerV2 from "../controllers/v2/InviteController.js"; +import MaintenanceControllerV2 from "../controllers/v2/MaintenanceController.js"; +import MonitorControllerV2 from "../controllers/v2/MonitorController.js"; +import NotificationChannelControllerV2 from "../controllers/v2/NotificationChannelController.js"; +import QueueControllerV2 from "../controllers/v2/QueueController.js"; export const initializeControllers = (services) => { const controllers = {}; const commonDependencies = createCommonDependencies(services.db, services.errorService, services.logger, services.stringService); + // V1 controllers.authController = new AuthController(commonDependencies, { settingsService: services.settingsService, emailService: services.emailService, @@ -62,5 +70,13 @@ export const initializeControllers = (services) => { diagnosticService: services.diagnosticService, }); + //V2 + controllers.authControllerV2 = new AuthControllerV2(services.authServiceV2, services.inviteServiceV2); + controllers.inviteControllerV2 = new InviteControllerV2(services.inviteServiceV2); + controllers.maintenanceControllerV2 = new MaintenanceControllerV2(services.maintenanceServiceV2); + controllers.monitorControllerV2 = new MonitorControllerV2(services.monitorServiceV2); + controllers.notificationChannelControllerV2 = new NotificationChannelControllerV2(services.notificationChannelServiceV2); + controllers.queueControllerV2 = new QueueControllerV2(services.jobQueueV2); + return controllers; }; diff --git a/server/src/config/routes.js b/server/src/config/routes.js index 40bf8e994..13b369262 100644 --- a/server/src/config/routes.js +++ b/server/src/config/routes.js @@ -13,7 +13,16 @@ import LogRoutes from "../routes/v1/logRoutes.js"; import DiagnosticRoutes from "../routes/v1//diagnosticRoute.js"; import NotificationRoutes from "../routes/v1/notificationRoute.js"; +//V2 +import AuthRoutesV2 from "../routes/v2/auth.js"; +import InviteRoutesV2 from "../routes/v2/invite.js"; +import MaintenanceRoutesV2 from "../routes/v2/maintenance.js"; +import MonitorRoutesV2 from "../routes/v2/monitors.js"; +import NotificationChannelRoutesV2 from "../routes/v2/notificationChannels.js"; +import QueueRoutesV2 from "../routes/v2/queue.js"; + export const setupRoutes = (app, controllers) => { + // V1 const authRoutes = new AuthRoutes(controllers.authController); const monitorRoutes = new MonitorRoutes(controllers.monitorController); const settingsRoutes = new SettingsRoutes(controllers.settingsController); @@ -37,4 +46,19 @@ export const setupRoutes = (app, controllers) => { app.use("/api/v1/status-page", statusPageRoutes.getRouter()); app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter()); app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter()); + + // V2 + const authRoutesV2 = new AuthRoutesV2(controllers.authControllerV2); + const inviteRoutesV2 = new InviteRoutesV2(controllers.inviteControllerV2); + const maintenanceRoutesV2 = new MaintenanceRoutesV2(controllers.maintenanceControllerV2); + const monitorRoutesV2 = new MonitorRoutesV2(controllers.monitorControllerV2); + const notificationChannelRoutesV2 = new NotificationChannelRoutesV2(controllers.notificationChannelControllerV2); + const queueRoutesV2 = new QueueRoutesV2(controllers.queueControllerV2); + + app.use("/api/v2/auth", authApiLimiter, authRoutesV2.getRouter()); + app.use("/api/v2/invite", inviteRoutesV2.getRouter()); + app.use("/api/v2/maintenance", maintenanceRoutesV2.getRouter()); + app.use("/api/v2/monitors", monitorRoutesV2.getRouter()); + app.use("/api/v2/notification-channels", notificationChannelRoutesV2.getRouter()); + app.use("/api/v2/queue", queueRoutesV2.getRouter()); }; diff --git a/server/src/config/services.js b/server/src/config/services.js index 22f978071..ec07c5e15 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -61,13 +61,35 @@ import AppSettings from "../db/v1/models/AppSettings.js"; import InviteModule from "../db/v1/modules/inviteModule.js"; import CheckModule from "../db/v1/modules/checkModule.js"; import StatusPageModule from "../db/v1/modules/statusPageModule.js"; -import UserModule from "../db/v1/modules//userModule.js"; +import UserModule from "../db/v1/modules/userModule.js"; import MaintenanceWindowModule from "../db/v1/modules/maintenanceWindowModule.js"; import MonitorModule from "../db/v1/modules/monitorModule.js"; import NotificationModule from "../db/v1/modules/notificationModule.js"; import RecoveryModule from "../db/v1/modules/recoveryModule.js"; import SettingsModule from "../db/v1/modules/settingsModule.js"; +// V2 Business +import AuthServiceV2 from "../service/v2/business/AuthService.js"; +import CheckServiceV2 from "../service/v2/business/CheckService.js"; +import InviteServiceV2 from "../service/v2/business/InviteService.js"; +import MaintenanceServiceV2 from "../service/v2/business/MaintenanceService.js"; +import MonitorServiceV2 from "../service/v2/business/MonitorService.js"; +import MonitorStatsServiceV2 from "../service/v2/business/MonitorStatsService.js"; +import NotificationChannelServiceV2 from "../service/v2/business/NotificationChannelService.js"; +import QueueServiceV2 from "../service/v2/business/QueueService.js"; +import UserServiceV2 from "../service/v2/business/UserService.js"; + +// V2 Infra +import DiscordServiceV2 from "../service/v2/infrastructure/NotificationServices/Discord.js"; +import EmailServiceV2 from "../service/v2/infrastructure/NotificationServices/Email.js"; +import SlackServiceV2 from "../service/v2/infrastructure/NotificationServices/Slack.js"; +import WebhookServiceV2 from "../service/v2/infrastructure/NotificationServices/Webhook.js"; +import JobGeneratorV2 from "../service/v2/infrastructure/JobGenerator.js"; +import JobQueueV2 from "../service/v2/infrastructure/JobQueue.js"; +import NetworkServiceV2 from "../service/v2/infrastructure/NetworkService.js"; +import NotificationServiceV2 from "../service/v2/infrastructure/NotificationService.js"; +import StatusServiceV2 from "../service/v2/infrastructure/StatusService.js"; + export const initializeServices = async ({ logger, envSettings, settingsService }) => { const serviceRegistry = new ServiceRegistry({ logger }); ServiceRegistry.instance = serviceRegistry; @@ -208,7 +230,37 @@ export const initializeServices = async ({ logger, envSettings, settingsService games, }); + // V2 Services + const jobQueueV2 = new JobQueueV2(); + const authServiceV2 = new AuthServiceV2(jobQueueV2); + const checkServiceV2 = new CheckServiceV2(); + const inviteServiceV2 = new InviteServiceV2(); + const maintenanceServiceV2 = new MaintenanceServiceV2(); + const monitorServiceV2 = new MonitorServiceV2(jobQueueV2); + const monitorStatsServiceV2 = new MonitorStatsServiceV2(); + const notificationChannelServiceV2 = new NotificationChannelServiceV2(); + const queueServiceV2 = new QueueServiceV2(jobQueueV2); + const userServiceV2 = new UserServiceV2(); + + // V2 Infra + const discordServiceV2 = new DiscordServiceV2(); + const emailServiceV2 = new EmailServiceV2(userServiceV2); + const slackServiceV2 = new SlackServiceV2(); + const webhookServiceV2 = new WebhookServiceV2(); + const networkServiceV2 = new NetworkServiceV2(); + const statusServiceV2 = new StatusServiceV2(); + const notificationServiceV2 = new NotificationServiceV2(userServiceV2); + const jobGeneratorV2 = new JobGeneratorV2( + networkServiceV2, + checkServiceV2, + monitorStatsServiceV2, + statusServiceV2, + notificationServiceV2, + maintenanceServiceV2 + ); + const services = { + //v1 settingsService, translationService, stringService, @@ -227,6 +279,25 @@ export const initializeServices = async ({ logger, envSettings, settingsService monitorService, errorService, logger, + //v2 + jobQueueV2, + authServiceV2, + checkServiceV2, + inviteServiceV2, + maintenanceServiceV2, + monitorServiceV2, + monitorStatsServiceV2, + notificationChannelServiceV2, + queueServiceV2, + userServiceV2, + discordServiceV2, + emailServiceV2, + slackServiceV2, + webhookServiceV2, + networkServiceV2, + statusServiceV2, + notificationServiceV2, + jobGeneratorV2, }; Object.values(services).forEach((service) => { diff --git a/server/src/controllers/v2/AuthController.ts b/server/src/controllers/v2/AuthController.ts new file mode 100644 index 000000000..3afed5312 --- /dev/null +++ b/server/src/controllers/v2/AuthController.ts @@ -0,0 +1,150 @@ +import { Request, Response, NextFunction } from "express"; +import { encode, decode } from "../../utils/JWTUtils.js"; +import AuthService from "../../service/v2/business/AuthService.js"; +import ApiError from "../../utils/ApiError.js"; +import InviteService from "../../service/v2/business/InviteService.js"; +import { IInvite } from "../../db/v2/models/index.js"; + +class AuthController { + private authService: AuthService; + private inviteService: InviteService; + constructor(authService: AuthService, inviteService: InviteService) { + this.authService = authService; + this.inviteService = inviteService; + } + + register = async (req: Request, res: Response, next: NextFunction) => { + try { + const { email, firstName, lastName, password } = req.body; + + if (!email || !firstName || !lastName || !password) { + throw new Error("Email, firstName, lastName, and password are required"); + } + + const result = await this.authService.register({ + email, + firstName, + lastName, + password, + }); + + const token = encode(result); + + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week + }); + + res.status(201).json({ + message: "User created successfully", + }); + } catch (error) { + next(error); + } + }; + + registerWithInvite = async (req: Request, res: Response, next: NextFunction) => { + try { + const token = req.params.token; + if (!token) { + throw new ApiError("Invite token is required", 400); + } + + const invite: IInvite = await this.inviteService.get(token); + + const { firstName, lastName, password } = req.body; + const email = invite?.email; + const roles = invite?.roles; + + if (!email || !firstName || !lastName || !password || !roles || roles.length === 0) { + throw new Error("Email, firstName, lastName, password, and roles are required"); + } + + const result = await this.authService.registerWithInvite({ + email, + firstName, + lastName, + password, + roles, + }); + + if (!result) { + throw new Error("Registration failed"); + } + + await this.inviteService.delete(invite._id.toString()); + + const jwt = encode(result); + + res.cookie("token", jwt, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week + }); + + res.status(201).json({ message: "User created successfully" }); + } catch (error) { + next(error); + } + }; + + login = async (req: Request, res: Response, next: NextFunction) => { + try { + const { email, password } = req.body; + // Validation + if (!email || !password) { + return res.status(400).json({ message: "Email and password are required" }); + } + const result = await this.authService.login({ email, password }); + + const token = encode(result); + + res.cookie("token", token, { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + maxAge: 7 * 24 * 60 * 60 * 1000, // 1 week + }); + + res.status(200).json({ + message: "Login successful", + }); + } catch (error) { + next(error); + } + }; + + logout = (req: Request, res: Response) => { + res.clearCookie("token", { + httpOnly: true, + secure: process.env.NODE_ENV === "production", + sameSite: "strict", + }); + res.status(200).json({ message: "Logout successful" }); + }; + + me = (req: Request, res: Response, next: NextFunction) => { + return res.status(200).json({ message: "OK" }); + }; + + cleanup = async (req: Request, res: Response) => { + try { + await this.authService.cleanup(); + res.status(200).json({ message: "Cleanup successful" }); + } catch (error) {} + }; + + cleanMonitors = async (req: Request, res: Response) => { + try { + await this.authService.cleanMonitors(); + res.status(200).json({ message: "Monitors cleanup successful" }); + } catch (error) { + res.status(500).json({ message: "Internal server error" }); + } + }; +} + +export default AuthController; diff --git a/server/src/controllers/v2/InviteController.ts b/server/src/controllers/v2/InviteController.ts new file mode 100644 index 000000000..4d72bf8d4 --- /dev/null +++ b/server/src/controllers/v2/InviteController.ts @@ -0,0 +1,62 @@ +import { Request, Response, NextFunction } from "express"; +import InviteService from "../../service/v2/business/InviteService.js"; + +class InviteController { + private inviteService: InviteService; + constructor(inviteService: InviteService) { + this.inviteService = inviteService; + } + + create = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + const invite = await this.inviteService.create(tokenizedUser, req.body); + res.status(201).json({ message: "OK", data: invite }); + } catch (error: any) { + next(error); + } + }; + + getAll = async (req: Request, res: Response, next: NextFunction) => { + try { + const invites = await this.inviteService.getAll(); + res.status(200).json({ + message: "OK", + data: invites, + }); + } catch (error) { + next(error); + } + }; + + get = async (req: Request, res: Response, next: NextFunction) => { + try { + const token = req.params.token; + if (!token) { + return res.status(400).json({ message: "Token parameter is required" }); + } + const invite = await this.inviteService.get(token); + res.status(200).json({ message: "OK", data: invite }); + } catch (error) { + next(error); + } + }; + + delete = async (req: Request, res: Response, next: NextFunction) => { + try { + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + await this.inviteService.delete(id); + res.status(204).json({ message: "OK" }); + } catch (error: any) { + next(error); + } + }; +} + +export default InviteController; diff --git a/server/src/controllers/v2/MaintenanceController.ts b/server/src/controllers/v2/MaintenanceController.ts new file mode 100644 index 000000000..7c6be11d5 --- /dev/null +++ b/server/src/controllers/v2/MaintenanceController.ts @@ -0,0 +1,96 @@ +import { Request, Response, NextFunction } from "express"; +import MaintenanceService from "../../service/v2/business/MaintenanceService.js"; + +class MaintenanceController { + private maintenanceService: MaintenanceService; + constructor(maintenanceService: MaintenanceService) { + this.maintenanceService = maintenanceService; + } + + create = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + const maintenance = await this.maintenanceService.create(tokenizedUser, req.body); + res.status(201).json({ message: "OK", data: maintenance }); + } catch (error) { + next(error); + } + }; + + getAll = async (req: Request, res: Response, next: NextFunction) => { + try { + const maintenances = await this.maintenanceService.getAll(); + res.status(200).json({ + message: "OK", + data: maintenances, + }); + } catch (error) { + next(error); + } + }; + + toggleActive = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + const maintenance = await this.maintenanceService.toggleActive(tokenizedUser, id); + res.status(200).json({ message: "OK", data: maintenance }); + } catch (error) { + next(error); + } + }; + + update = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + const updatedMaintenance = await this.maintenanceService.update(tokenizedUser, id, req.body); + res.status(200).json({ message: "OK", data: updatedMaintenance }); + } catch (error) { + next(error); + } + }; + + get = async (req: Request, res: Response, next: NextFunction) => { + try { + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + const maintenance = await this.maintenanceService.get(id); + res.status(200).json({ message: "OK", data: maintenance }); + } catch (error) { + next(error); + } + }; + + delete = async (req: Request, res: Response, next: NextFunction) => { + try { + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + await this.maintenanceService.delete(id); + res.status(204).json({ message: "OK" }); + } catch (error) { + next(error); + } + }; +} + +export default MaintenanceController; diff --git a/server/src/controllers/v2/MonitorController.ts b/server/src/controllers/v2/MonitorController.ts new file mode 100644 index 000000000..c9a4ce7da --- /dev/null +++ b/server/src/controllers/v2/MonitorController.ts @@ -0,0 +1,157 @@ +import { Request, Response, NextFunction } from "express"; +import ApiError from "../../utils/ApiError.js"; +import MonitorService from "../../service/v2/business/MonitorService.js"; +import { MonitorType } from "../../db/v2/models/monitors/Monitor.js"; +class MonitorController { + private monitorService: MonitorService; + constructor(monitorService: MonitorService) { + this.monitorService = monitorService; + } + + create = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const monitor = await this.monitorService.create(tokenizedUser, req.body); + res.status(201).json({ + message: "Monitor created successfully", + data: monitor, + }); + } catch (error) { + next(error); + } + }; + + get = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const id = req.params.id; + if (!id) { + throw new ApiError("Monitor ID is required", 400); + } + + const range = req.query.range; + if (!range || typeof range !== "string") throw new ApiError("Range query parameter is required", 400); + + let monitor; + + const status = req.query.status; + if (status && typeof status !== "string") { + throw new ApiError("Status query parameter must be a string", 400); + } + + if (req.query.embedChecks === "true") { + monitor = await this.monitorService.getEmbedChecks(id, range, status); + } else { + monitor = await this.monitorService.get(id); + } + + res.status(200).json({ + message: "Monitor retrieved successfully", + data: monitor, + }); + } catch (error) { + next(error); + } + }; + + getAll = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + let monitors; + if (req.query.embedChecks === "true") { + const page = Math.max(1, Number(req.query.page) || 1); + const limit = Math.max(1, Number(req.query.limit) || 10); + const type: MonitorType[] = req.query.type as MonitorType[]; + + monitors = await this.monitorService.getAllEmbedChecks(page, limit, type); + } else { + monitors = await this.monitorService.getAll(); + } + + res.status(200).json({ + message: "Monitors retrieved successfully", + data: monitors, + }); + } catch (error) { + next(error); + } + }; + + toggleActive = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const id = req.params.id; + if (!id) { + throw new ApiError("Monitor ID is required", 400); + } + + const monitor = await this.monitorService.toggleActive(id, tokenizedUser); + res.status(200).json({ + message: "Monitor paused/unpaused successfully", + data: monitor, + }); + } catch (error) { + next(error); + } + }; + + update = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + + const id = req.params.id; + if (!id) { + throw new ApiError("Monitor ID is required", 400); + } + + const monitor = await this.monitorService.update(tokenizedUser, id, req.body); + res.status(200).json({ + message: "Monitor updated successfully", + data: monitor, + }); + } catch (error) { + next(error); + } + }; + + delete = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + const id = req.params.id; + if (!id) { + throw new ApiError("Monitor ID is required", 400); + } + await this.monitorService.delete(id); + + res.status(200).json({ + message: "Monitor deleted successfully", + }); + } catch (error) { + next(error); + } + }; +} + +export default MonitorController; diff --git a/server/src/controllers/v2/NotificationChannelController.ts b/server/src/controllers/v2/NotificationChannelController.ts new file mode 100644 index 000000000..e13f572f4 --- /dev/null +++ b/server/src/controllers/v2/NotificationChannelController.ts @@ -0,0 +1,96 @@ +import { Request, Response, NextFunction } from "express"; +import NotificationService from "../../service/v2/business/NotificationChannelService.js"; + +class NotificationChannelController { + private notificationService: NotificationService; + constructor(notificationService: NotificationService) { + this.notificationService = notificationService; + } + + create = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + const channel = await this.notificationService.create(tokenizedUser, req.body); + res.status(201).json({ message: "OK", data: channel }); + } catch (error) { + next(error); + } + }; + + getAll = async (req: Request, res: Response, next: NextFunction) => { + try { + const notificationChannels = await this.notificationService.getAll(); + res.status(200).json({ + message: "OK", + data: notificationChannels, + }); + } catch (error) { + next(error); + } + }; + + toggleActive = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + const notificationChannel = await this.notificationService.toggleActive(tokenizedUser, id); + res.status(200).json({ message: "OK", data: notificationChannel }); + } catch (error) { + next(error); + } + }; + + update = async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenizedUser = req.user; + if (!tokenizedUser) { + return res.status(401).json({ message: "Unauthorized" }); + } + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + const updatedChannel = await this.notificationService.update(tokenizedUser, id, req.body); + res.status(200).json({ message: "OK", data: updatedChannel }); + } catch (error) { + next(error); + } + }; + + get = async (req: Request, res: Response, next: NextFunction) => { + try { + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + const notificationChannel = await this.notificationService.get(id); + res.status(200).json({ message: "OK", data: notificationChannel }); + } catch (error) { + next(error); + } + }; + + delete = async (req: Request, res: Response, next: NextFunction) => { + try { + const id = req.params.id; + if (!id) { + return res.status(400).json({ message: "ID parameter is required" }); + } + await this.notificationService.delete(id); + res.status(204).json({ message: "OK" }); + } catch (error) { + next(error); + } + }; +} + +export default NotificationChannelController; diff --git a/server/src/controllers/v2/QueueController.ts b/server/src/controllers/v2/QueueController.ts new file mode 100644 index 000000000..510703894 --- /dev/null +++ b/server/src/controllers/v2/QueueController.ts @@ -0,0 +1,29 @@ +import { Request, Response, NextFunction } from "express"; +import QueueService from "../../service/v2/business/QueueService.js"; +class QueueController { + private queueService: QueueService; + constructor(queueService: QueueService) { + this.queueService = queueService; + } + + getJobs = async (req: Request, res: Response, next: NextFunction) => { + try { + const jobs = await this.queueService.getJobs(); + res.status(200).json({ message: "ok", data: jobs }); + } catch (error) { + next(error); + } + }; + + getMetrics = async (req: Request, res: Response, next: NextFunction) => { + const metrics = await this.queueService.getMetrics(); + res.status(200).json({ message: "ok", data: metrics }); + }; + + flush = async (req: Request, res: Response, next: NextFunction) => { + const result = await this.queueService.flush(); + res.status(200).json({ message: "ok", flushed: result }); + }; +} + +export default QueueController; diff --git a/server/src/middleware/v2/VerifyPermissions.ts b/server/src/middleware/v2/VerifyPermissions.ts new file mode 100644 index 000000000..64d573e00 --- /dev/null +++ b/server/src/middleware/v2/VerifyPermissions.ts @@ -0,0 +1,77 @@ +import { Request, Response, NextFunction } from "express"; +import ApiError from "../../utils/ApiError.js"; +import { User, IUser, Role, IRole } from "../../db/v2/models/index.js"; + +const rolesCache = new Map(); +// const CACHE_TTL = 30 * 60 * 1000; // 30 minutes +const CACHE_TTL = 1; // 30 minutes +const MAX_CACHE_SIZE = 1000; + +const getCachedRoles = async (userId: string) => { + if (rolesCache.size >= MAX_CACHE_SIZE) { + const oldestKey = rolesCache.keys().next().value; + if (!oldestKey) return null; + rolesCache.delete(oldestKey); + } + + const cached = rolesCache.get(userId); + + if (cached && Date.now() - cached.timestamp < CACHE_TTL) { + return cached.roles; + } + + const user: IUser | null = await User.findById(userId); + if (!user) { + return null; + } + + const roles = await Role.find({ _id: { $in: user.roles } }); + rolesCache.set(userId, { roles, timestamp: Date.now() }); + return roles; +}; + +const hasPermission = (roles: IRole[], requiredPermissions: string[]) => { + const userPermissions = [...new Set(roles.flatMap((role) => role.permissions))]; + + if (userPermissions.includes("*")) return true; + + const matches = (requiredPermission: string, userPermission: string) => { + if (userPermission === requiredPermission) return true; + if (userPermission.endsWith(".*")) { + const prefix = userPermission.slice(0, -2); + return requiredPermission.startsWith(prefix + "."); + } + return false; + }; + + return requiredPermissions.every((requiredPermission) => { + return userPermissions.some((userPermission) => matches(requiredPermission, userPermission)); + }); +}; + +const verifyPermission = (resourceActions: string[]) => { + return async (req: Request, res: Response, next: NextFunction) => { + const tokenizedUser = req.user; + + if (!tokenizedUser) { + throw new ApiError("No user", 400); + } + + const userId = tokenizedUser.sub; + if (!userId) { + throw new ApiError("No user ID", 400); + } + + const userRoles = await getCachedRoles(userId); + if (!userRoles) { + throw new ApiError("User roles not found", 400); + } + const allowed = hasPermission(userRoles, resourceActions); + if (!allowed) { + throw new ApiError("Insufficient permissions", 403); + } + next(); + }; +}; + +export { verifyPermission }; diff --git a/server/src/middleware/v2/VerifyToken.ts b/server/src/middleware/v2/VerifyToken.ts new file mode 100644 index 000000000..7dabd51cc --- /dev/null +++ b/server/src/middleware/v2/VerifyToken.ts @@ -0,0 +1,21 @@ +import { Request, Response, NextFunction } from "express"; +import { decode } from "../../utils/JWTUtils.js"; +import ApiError from "../../utils/ApiError.js"; +const verifyToken = (req: Request, res: Response, next: NextFunction) => { + const token = req.cookies.token; + + if (!token) { + const error = new ApiError("No token provided", 401); + return next(error); + } + + try { + const decoded = decode(token); + req.user = decoded; + next(); + } catch (error) { + next(error); + } +}; + +export { verifyToken }; diff --git a/server/src/routes/v2/auth.ts b/server/src/routes/v2/auth.ts index e69de29bb..919d0fd76 100644 --- a/server/src/routes/v2/auth.ts +++ b/server/src/routes/v2/auth.ts @@ -0,0 +1,33 @@ +import { Router } from "express"; + +import express from "express"; +import AuthController from "../../controllers/v2/AuthController.js"; +import { verifyToken } from "../../middleware/v2/VerifyToken.js"; + +const router = express.Router(); + +class AuthRoutes { + private controller: AuthController; + private router: Router; + constructor(authController: AuthController) { + this.controller = authController; + this.router = Router(); + this.initRoutes(); + } + + initRoutes = () => { + this.router.post("/register", this.controller.register); + this.router.post("/register/invite/:token", this.controller.registerWithInvite); + this.router.post("/login", this.controller.login); + this.router.post("/logout", this.controller.logout); + this.router.get("/me", verifyToken, this.controller.me); + this.router.post("/cleanup", this.controller.cleanup); + this.router.post("/cleanup-monitors", this.controller.cleanMonitors); + }; + + getRouter() { + return this.router; + } +} + +export default AuthRoutes; diff --git a/server/src/routes/v2/invite.ts b/server/src/routes/v2/invite.ts new file mode 100644 index 000000000..8066d0c77 --- /dev/null +++ b/server/src/routes/v2/invite.ts @@ -0,0 +1,30 @@ +import { Router } from "express"; +import InviteController from "../../controllers/v2/InviteController.js"; +import { verifyToken } from "../../middleware/v2/VerifyToken.js"; +import { verifyPermission } from "../../middleware/v2/VerifyPermissions.js"; + +class InviteRoutes { + private router; + private controller; + constructor(inviteController: InviteController) { + this.router = Router(); + this.controller = inviteController; + this.initRoutes(); + } + + initRoutes = () => { + this.router.post("/", verifyToken, verifyPermission(["invite.create"]), this.controller.create); + + this.router.get("/", verifyToken, verifyPermission(["invite.view"]), this.controller.getAll); + + this.router.get("/:token", verifyToken, verifyPermission(["invite.view"]), this.controller.get); + + this.router.delete("/:id", verifyToken, verifyPermission(["invite.delete"]), this.controller.delete); + }; + + getRouter() { + return this.router; + } +} + +export default InviteRoutes; diff --git a/server/src/routes/v2/maintenance.ts b/server/src/routes/v2/maintenance.ts new file mode 100644 index 000000000..62bc5f1cf --- /dev/null +++ b/server/src/routes/v2/maintenance.ts @@ -0,0 +1,34 @@ +import { Router } from "express"; +import MaintenanceController from "../../controllers/v2/MaintenanceController.js"; +import { verifyToken } from "../../middleware/v2/VerifyToken.js"; +import { verifyPermission } from "../../middleware/v2/VerifyPermissions.js"; + +class MaintenanceRoutes { + private router; + private controller; + constructor(maintenanceController: MaintenanceController) { + this.router = Router(); + this.controller = maintenanceController; + this.initRoutes(); + } + + initRoutes = () => { + this.router.post("/", verifyToken, verifyPermission(["maintenance.create"]), this.controller.create); + + this.router.get("/", verifyToken, verifyPermission(["maintenance.view"]), this.controller.getAll); + + this.router.patch("/:id/active", verifyToken, verifyPermission(["maintenance.update"]), this.controller.toggleActive); + + this.router.patch("/:id", verifyToken, verifyPermission(["maintenance.update"]), this.controller.update); + + this.router.get("/:id", verifyToken, verifyPermission(["maintenance.view"]), this.controller.get); + + this.router.delete("/:id", verifyToken, verifyPermission(["maintenance.delete"]), this.controller.delete); + }; + + getRouter() { + return this.router; + } +} + +export default MaintenanceRoutes; diff --git a/server/src/routes/v2/monitors.ts b/server/src/routes/v2/monitors.ts new file mode 100644 index 000000000..eb218f4c6 --- /dev/null +++ b/server/src/routes/v2/monitors.ts @@ -0,0 +1,34 @@ +import { Router } from "express"; +import MonitorController from "../../controllers/v2/MonitorController.js"; +import { verifyToken } from "../../middleware/v2/VerifyToken.js"; +import { verifyPermission } from "../../middleware/v2/VerifyPermissions.js"; + +class MonitorRoutes { + private router; + private controller; + constructor(monitorController: MonitorController) { + this.router = Router(); + this.controller = monitorController; + this.initRoutes(); + } + + initRoutes = () => { + this.router.post("/", verifyToken, verifyPermission(["monitors.create"]), this.controller.create); + + this.router.get("/", verifyToken, verifyPermission(["monitors.view"]), this.controller.getAll); + + this.router.patch("/:id/active", verifyToken, verifyPermission(["monitors.update"]), this.controller.toggleActive); + + this.router.patch("/:id", verifyToken, verifyPermission(["monitors.update"]), this.controller.update); + + this.router.get("/:id", verifyToken, verifyPermission(["monitors.view"]), this.controller.get); + + this.router.delete("/:id", verifyToken, verifyPermission(["monitors.delete"]), this.controller.delete); + }; + + getRouter() { + return this.router; + } +} + +export default MonitorRoutes; diff --git a/server/src/routes/v2/notificationChannels.ts b/server/src/routes/v2/notificationChannels.ts new file mode 100644 index 000000000..1db32902a --- /dev/null +++ b/server/src/routes/v2/notificationChannels.ts @@ -0,0 +1,34 @@ +import { Router } from "express"; +import NotificationController from "../../controllers/v2/NotificationChannelController.js"; +import { verifyToken } from "../../middleware/v2/VerifyToken.js"; +import { verifyPermission } from "../../middleware/v2/VerifyPermissions.js"; + +class NotificationChannelRoutes { + private router; + private controller; + constructor(notificationController: NotificationController) { + this.router = Router(); + this.controller = notificationController; + this.initRoutes(); + } + + initRoutes = () => { + this.router.post("/", verifyToken, verifyPermission(["notifications.create"]), this.controller.create); + + this.router.get("/", verifyToken, verifyPermission(["notifications.view"]), this.controller.getAll); + + this.router.patch("/:id/active", verifyToken, verifyPermission(["notifications.update"]), this.controller.toggleActive); + + this.router.patch("/:id", verifyToken, verifyPermission(["notifications.update"]), this.controller.update); + + this.router.get("/:id", verifyToken, verifyPermission(["notifications.view"]), this.controller.get); + + this.router.delete("/:id", verifyToken, verifyPermission(["notifications.delete"]), this.controller.delete); + }; + + getRouter() { + return this.router; + } +} + +export default NotificationChannelRoutes; diff --git a/server/src/routes/v2/queue.ts b/server/src/routes/v2/queue.ts new file mode 100644 index 000000000..428776e61 --- /dev/null +++ b/server/src/routes/v2/queue.ts @@ -0,0 +1,24 @@ +import QueueController from "../../controllers/v2/QueueController.js"; +import { Router } from "express"; + +class QueueRoutes { + private router; + private controller; + constructor(queueController: QueueController) { + this.router = Router(); + this.controller = queueController; + this.initRoutes(); + } + + initRoutes() { + this.router.get("/jobs", this.controller.getJobs); + this.router.get("/metrics", this.controller.getMetrics); + this.router.post("/flush", this.controller.flush); + } + + getRouter() { + return this.router; + } +} + +export default QueueRoutes; diff --git a/server/src/service/v2/business/AuthService.ts b/server/src/service/v2/business/AuthService.ts index cf49a71c6..67ece226e 100644 --- a/server/src/service/v2/business/AuthService.ts +++ b/server/src/service/v2/business/AuthService.ts @@ -4,6 +4,8 @@ import ApiError from "../../../utils/ApiError.js"; import { Types } from "mongoose"; import { IJobQueue } from "../infrastructure/JobQueue.js"; +const SERVICE_NAME = "AuthServiceV2"; + export const PERMISSIONS = { users: { all: "users.*", @@ -94,6 +96,8 @@ export interface IAuthService { } class AuthService implements IAuthService { + static SERVICE_NAME = SERVICE_NAME; + private jobQueue: IJobQueue; constructor(jobQueue: IJobQueue) { this.jobQueue = jobQueue; diff --git a/server/src/service/v2/business/CheckService.ts b/server/src/service/v2/business/CheckService.ts index 5c68891a9..7187fb5a3 100644 --- a/server/src/service/v2/business/CheckService.ts +++ b/server/src/service/v2/business/CheckService.ts @@ -6,12 +6,15 @@ import { StatusResponse } from "../infrastructure/NetworkService.js"; import type { ICapturePayload, ILighthousePayload } from "../infrastructure/NetworkService.js"; import mongoose from "mongoose"; +const SERVICE_NAME = "CheckServiceV2"; export interface ICheckService { buildCheck: (statusResponse: StatusResponse, type: MonitorType) => Promise; cleanupOrphanedChecks: () => Promise; } class CheckService implements ICheckService { + static SERVICE_NAME = SERVICE_NAME; + private isCapturePayload = (payload: any): payload is ICapturePayload => { if (!payload || typeof payload !== "object") return false; diff --git a/server/src/service/v2/business/InviteService.ts b/server/src/service/v2/business/InviteService.ts index 1c954d75f..b06b2a2c2 100644 --- a/server/src/service/v2/business/InviteService.ts +++ b/server/src/service/v2/business/InviteService.ts @@ -2,6 +2,7 @@ import crypto from "node:crypto"; import { ITokenizedUser, IInvite, Invite } from "../../../db/v2/models/index.js"; import ApiError from "../../../utils/ApiError.js"; +const SERVICE_NAME = "InviteServiceV2"; export interface IInviteService { create: (tokenizedUser: ITokenizedUser, invite: IInvite) => Promise<{ token: string }>; getAll: () => Promise; @@ -10,6 +11,7 @@ export interface IInviteService { } class InviteService implements IInviteService { + static SERVICE_NAME = SERVICE_NAME; constructor() {} create = async (tokenizedUser: ITokenizedUser, inviteData: IInvite) => { diff --git a/server/src/service/v2/business/MaintenanceService.ts b/server/src/service/v2/business/MaintenanceService.ts index ef8c8152f..55b2d787b 100644 --- a/server/src/service/v2/business/MaintenanceService.ts +++ b/server/src/service/v2/business/MaintenanceService.ts @@ -1,6 +1,8 @@ import { ITokenizedUser, IMaintenance, Maintenance } from "../../../db/v2/models/index.js"; import ApiError from "../../../utils/ApiError.js"; +const SERVICE_NAME = "MaintenanceServiceV2"; + export interface IMaintenanceService { create: ( tokenizedUser: ITokenizedUser, @@ -18,6 +20,7 @@ export interface IMaintenanceService { type MaintenanceCache = Map; class MaintenanceService implements IMaintenanceService { + static SERVICE_NAME = SERVICE_NAME; private maintenanceCache: MaintenanceCache; private lastRefresh: number; private CACHE_TTL_MS = 60 * 1000; diff --git a/server/src/service/v2/business/MonitorService.ts b/server/src/service/v2/business/MonitorService.ts new file mode 100644 index 000000000..61fc8b114 --- /dev/null +++ b/server/src/service/v2/business/MonitorService.ts @@ -0,0 +1,468 @@ +import mongoose from "mongoose"; + +import { IMonitor, Monitor, ITokenizedUser, MonitorStats, Check } from "../../../db/v2/models/index.js"; +import ApiError from "../../../utils/ApiError.js"; +import { IJobQueue } from "../infrastructure/JobQueue.js"; +import { MonitorWithChecksResponse } from "../../../types/index.js"; +import { MonitorStatus, MonitorType } from "../../../db/v2/models/monitors/Monitor.js"; + +const SERVICE_NAME = "MonitorServiceV2"; + +export interface IMonitorService { + create: (tokenizedUser: ITokenizedUser, monitorData: IMonitor) => Promise; + getAll: () => Promise; + getAllEmbedChecks: (page: number, limit: number, type: MonitorType[]) => Promise; + get: (monitorId: string) => Promise; + getEmbedChecks: (monitorId: string, range: string, status?: string) => Promise; + toggleActive: (monitorId: string, tokenizedUser: ITokenizedUser) => Promise; + update: (tokenizedUser: ITokenizedUser, monitorId: string, updateData: Partial) => Promise; + delete: (monitorId: string) => Promise; +} + +class MonitorService implements IMonitorService { + static SERVICE_NAME = SERVICE_NAME; + private jobQueue: IJobQueue; + constructor(jobQueue: IJobQueue) { + this.jobQueue = jobQueue; + } + + create = async (tokenizedUser: ITokenizedUser, monitorData: IMonitor) => { + const monitor = await Monitor.create({ + ...monitorData, + createdBy: tokenizedUser.sub, + updatedBy: tokenizedUser.sub, + }); + await MonitorStats.create({ + monitorId: monitor._id, + currentStreakStartedAt: Date.now(), + }); + await this.jobQueue.addJob(monitor); + return monitor; + }; + + getAll = async () => { + return Monitor.find(); + }; + + getAllEmbedChecks = async (page: number, limit: number, type: MonitorType[] = []) => { + const skip = (page - 1) * limit; + let find = {}; + if (type.length > 0) find = { type: { $in: type } }; + const monitors = await Monitor.find(find).skip(skip).limit(limit); + return monitors; + }; + + get = async (monitorId: string) => { + const monitor = await Monitor.findById(monitorId); + if (!monitor) { + throw new ApiError("Monitor not found", 404); + } + return monitor; + }; + + private getStartDate(range: string): Date { + const now = new Date(); + switch (range) { + case "30m": + return new Date(now.getTime() - 30 * 60 * 1000); + case "24h": + return new Date(now.getTime() - 24 * 60 * 60 * 1000); + case "7d": + return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000); + case "30d": + return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000); + default: + throw new ApiError("Invalid range parameter", 400); + } + } + + private getDateFormat(range: string): string { + switch (range) { + case "30m": + return "%Y-%m-%dT%H:%M:00Z"; + case "24h": + case "7d": + return "%Y-%m-%dT%H:00:00Z"; + case "30d": + return "%Y-%m-%d"; + default: + throw new ApiError("Invalid range parameter", 400); + } + } + + private getBaseGroup = (dateFormat: string): Record => { + return { + _id: { $dateToString: { format: dateFormat, date: "$createdAt" } }, + count: { $sum: 1 }, + avgResponseTime: { $avg: "$responseTime" }, + }; + }; + + private getBaseProjection = (): object => { + return { status: 1, responseTime: 1, createdAt: 1 }; + }; + + private getPageSpeedGroup = (dateFormat: string): Record => { + return { + _id: { $dateToString: { format: dateFormat, date: "$createdAt" } }, + count: { $sum: 1 }, + avgResponseTime: { $avg: "$responseTime" }, + accessibility: { $avg: "$lighthouse.accessibility" }, + bestPractices: { $avg: "$lighthouse.bestPractices" }, + seo: { $avg: "$lighthouse.seo" }, + performance: { $avg: "$lighthouse.performance" }, + cls: { $avg: "$lighthouse.audits.cls.score" }, + si: { $avg: "$lighthouse.audits.si.score" }, + fcp: { $avg: "$lighthouse.audits.fcp.score" }, + lcp: { $avg: "$lighthouse.audits.lcp.score" }, + tbt: { $avg: "$lighthouse.audits.tbt.score" }, + }; + }; + + private getPageSpeedProjection = (): object => { + const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 }; + projectStage["lighthouse.accessibility"] = 1; + projectStage["lighthouse.seo"] = 1; + projectStage["lighthouse.bestPractices"] = 1; + projectStage["lighthouse.performance"] = 1; + projectStage["lighthouse.audits.cls.score"] = 1; + projectStage["lighthouse.audits.si.score"] = 1; + projectStage["lighthouse.audits.fcp.score"] = 1; + projectStage["lighthouse.audits.lcp.score"] = 1; + projectStage["lighthouse.audits.tbt.score"] = 1; + return projectStage; + }; + + private getInfraGroup = (dateFormat: string): Record => { + return { + _id: { $dateToString: { format: dateFormat, date: "$createdAt" } }, + count: { $sum: 1 }, + avgResponseTime: { $avg: "$responseTime" }, + physicalCores: { $last: "$system.cpu.physical_core" }, + logicalCores: { $last: "$system.cpu.logical_core" }, + frequency: { $avg: "$system.cpu.frequency" }, + currentFrequency: { $last: "$system.cpu.current_frequency" }, + tempsArrays: { $push: "$system.cpu.temperature" }, + freePercent: { $avg: "$system.cpu.free_percent" }, + usedPercent: { $avg: "$system.cpu.usage_percent" }, + total_bytes: { $last: "$system.memory.total_bytes" }, + available_bytes: { $last: "$system.memory.available_bytes" }, + used_bytes: { $last: "$system.memory.used_bytes" }, + memory_usage_percent: { $avg: "$system.memory.usage_percent" }, + disksArray: { $push: "$system.disk" }, + os: { $last: "$system.host.os" }, + platform: { $last: "$system.host.platform" }, + kernel_version: { $last: "$system.host.kernel_version" }, + pretty_name: { $last: "$system.host.pretty_name" }, + netsArray: { $push: "$system.net" }, + }; + }; + + private getInfraProjection = (): object => { + const projectStage: any = { status: 1, responseTime: 1, createdAt: 1 }; + projectStage["system.cpu.physical_core"] = 1; + projectStage["system.cpu.logical_core"] = 1; + projectStage["system.cpu.frequency"] = 1; + projectStage["system.cpu.current_frequency"] = 1; + projectStage["system.cpu.temperature"] = 1; + projectStage["system.cpu.free_percent"] = 1; + projectStage["system.cpu.usage_percent"] = 1; + projectStage["system.memory.total_bytes"] = 1; + projectStage["system.memory.available_bytes"] = 1; + projectStage["system.memory.used_bytes"] = 1; + projectStage["system.memory.usage_percent"] = 1; + projectStage["system.disk"] = 1; + projectStage["system.host.os"] = 1; + projectStage["system.host.platform"] = 1; + projectStage["system.host.kernel_version"] = 1; + projectStage["system.host.pretty_name"] = 1; + projectStage["system.net"] = 1; + return projectStage; + }; + + private getFinalProjection = (type: string): object => { + if (type === "pagespeed") { + return { + _id: 1, + count: 1, + avgResponseTime: 1, + accessibility: "$accessibility", + seo: "$seo", + bestPractices: "$bestPractices", + performance: "$performance", + cls: "$cls", + si: "$si", + fcp: "$fcp", + lcp: "$lcp", + tbt: "$tbt", + }; + } + + if (type === "infrastructure") { + return { + _id: 1, + count: 1, + avgResponseTime: 1, + cpu: { + physicalCores: "$physicalCores", + logicalCores: "$logicalCores", + frequency: "$frequency", + currentFrequency: "$currentFrequency", + temperatures: { + $map: { + input: { + $range: [0, { $size: { $arrayElemAt: ["$tempsArrays", 0] } }], + }, + as: "idx", + in: { + $avg: { + $map: { + input: "$tempsArrays", + as: "arr", + in: { $arrayElemAt: ["$$arr", "$$idx"] }, + }, + }, + }, + }, + }, + freePercent: "$freePercent", + usedPercent: "$usedPercent", + }, + memory: { + total_bytes: "$total_bytes", + available_bytes: "$available_bytes", + used_bytes: "$used_bytes", + usage_percent: "$memory_usage_percent", + }, + disks: { + $map: { + input: { + $range: [0, { $size: { $arrayElemAt: ["$disksArray", 0] } }], + }, + as: "idx", + in: { + $let: { + vars: { + diskGroup: { + $map: { + input: "$disksArray", + as: "diskArr", + in: { $arrayElemAt: ["$$diskArr", "$$idx"] }, + }, + }, + }, + in: { + device: { $arrayElemAt: ["$$diskGroup.device", 0] }, + total_bytes: { $avg: "$$diskGroup.total_bytes" }, + free_bytes: { $avg: "$$diskGroup.free_bytes" }, + used_bytes: { $avg: "$$diskGroup.used_bytes" }, + usage_percent: { $avg: "$$diskGroup.usage_percent" }, + total_inodes: { $avg: "$$diskGroup.total_inodes" }, + free_inodes: { $avg: "$$diskGroup.free_inodes" }, + used_inodes: { $avg: "$$diskGroup.used_inodes" }, + inodes_usage_percent: { + $avg: "$$diskGroup.inodes_usage_percent", + }, + read_bytes: { $avg: "$$diskGroup.read_bytes" }, + write_bytes: { $avg: "$$diskGroup.write_bytes" }, + read_time: { $avg: "$$diskGroup.read_time" }, + write_time: { $avg: "$$diskGroup.write_time" }, + }, + }, + }, + }, + }, + host: { + os: "$os", + platform: "$platform", + kernel_version: "$kernel_version", + pretty_name: "$pretty_name", + }, + net: { + $map: { + input: { + $range: [0, { $size: { $arrayElemAt: ["$netsArray", 0] } }], + }, + as: "idx", + in: { + $let: { + vars: { + netGroup: { + $map: { + input: "$netsArray", + as: "netArr", + in: { $arrayElemAt: ["$$netArr", "$$idx"] }, + }, + }, + }, + in: { + name: { $arrayElemAt: ["$$netGroup.name", 0] }, + bytes_sent: { $avg: "$$netGroup.bytes_sent" }, + bytes_recv: { $avg: "$$netGroup.bytes_recv" }, + packets_sent: { $avg: "$$netGroup.packets_sent" }, + packets_recv: { $avg: "$$netGroup.packets_recv" }, + err_in: { $avg: "$$netGroup.err_in" }, + err_out: { $avg: "$$netGroup.err_out" }, + drop_in: { $avg: "$$netGroup.drop_in" }, + drop_out: { $avg: "$$netGroup.drop_out" }, + fifo_in: { $avg: "$$netGroup.fifo_in" }, + fifo_out: { $avg: "$$netGroup.fifo_out" }, + }, + }, + }, + }, + }, + }; + } + return {}; + }; + + getEmbedChecks = async (monitorId: string, range: string, status: string | undefined): Promise => { + const monitor = await Monitor.findById(monitorId); + if (!monitor) { + throw new ApiError("Monitor not found", 404); + } + const startDate = this.getStartDate(range); + const dateFormat = this.getDateFormat(range); + + // Build match stage + const matchStage: { + monitorId: mongoose.Types.ObjectId; + createdAt: { $gte: Date }; + status?: string; + } = { + monitorId: monitor._id, + createdAt: { $gte: startDate }, + }; + + if (status) { + matchStage.status = status; + } + + let groupClause; + + if (monitor.type === "pagespeed") { + groupClause = this.getPageSpeedGroup(dateFormat); + } else if (monitor.type === "infrastructure") { + groupClause = this.getInfraGroup(dateFormat); + } else { + groupClause = this.getBaseGroup(dateFormat); + } + + let projectStage; + if (monitor.type === "pagespeed") { + projectStage = this.getPageSpeedProjection(); + } else if (monitor.type === "infrastructure") { + projectStage = this.getInfraProjection(); + } else { + projectStage = this.getBaseProjection(); + } + + let finalProjection = {}; + if (monitor.type === "pagespeed" || monitor.type === "infrastructure") { + finalProjection = this.getFinalProjection(monitor.type); + } else { + finalProjection = { _id: 1, count: 1, avgResponseTime: 1 }; + } + + const checks = await Check.aggregate([ + { + $match: matchStage, + }, + { $sort: { createdAt: 1 } }, + { $project: projectStage }, + { $group: groupClause }, + { $sort: { _id: -1 } }, + { + $project: finalProjection, + }, + ]); + + // Get monitor stats + const monitorStats = await MonitorStats.findOne({ + monitorId: monitor._id, + }).lean(); + + if (!monitorStats) { + throw new ApiError("Monitor stats not found", 404); + } + + return { + monitor: monitor.toObject(), + checks, + stats: monitorStats, + }; + }; + + async toggleActive(id: string, tokenizedUser: ITokenizedUser) { + const pendingStatus: MonitorStatus = "initializing"; + const updatedMonitor = await Monitor.findOneAndUpdate( + { _id: id }, + [ + { + $set: { + isActive: { $not: "$isActive" }, + status: pendingStatus, + updatedBy: tokenizedUser.sub, + updatedAt: new Date(), + }, + }, + ], + { new: true } + ); + + if (!updatedMonitor) { + throw new ApiError("Monitor not found", 404); + } + + await this.jobQueue.updateJob(updatedMonitor); + + if (updatedMonitor?.isActive) { + await this.jobQueue.resumeJob(updatedMonitor); + } else { + await this.jobQueue.pauseJob(updatedMonitor); + } + return updatedMonitor; + } + + async update(tokenizedUser: ITokenizedUser, monitorId: string, updateData: Partial) { + const allowedFields: (keyof IMonitor)[] = ["name", "interval", "isActive", "n", "notificationChannels"]; + const safeUpdate: Partial = {}; + + for (const field of allowedFields) { + if (updateData[field] !== undefined) { + (safeUpdate as any)[field] = updateData[field]; + } + } + + const updatedMonitor = await Monitor.findByIdAndUpdate( + monitorId, + { + $set: { + ...safeUpdate, + updatedAt: new Date(), + updatedBy: tokenizedUser.sub, + }, + }, + { new: true, runValidators: true } + ); + + if (!updatedMonitor) { + throw new ApiError("Monitor not found", 404); + } + await this.jobQueue.updateJob(updatedMonitor); + return updatedMonitor; + } + + async delete(monitorId: string) { + const monitor = await Monitor.findById(monitorId); + if (!monitor) { + throw new ApiError("Monitor not found", 404); + } + await monitor.deleteOne(); + await this.jobQueue.deleteJob(monitor); + return true; + } +} + +export default MonitorService; diff --git a/server/src/service/v2/business/MonitorStatsService.ts b/server/src/service/v2/business/MonitorStatsService.ts index dc7d4acb3..089b43ec4 100644 --- a/server/src/service/v2/business/MonitorStatsService.ts +++ b/server/src/service/v2/business/MonitorStatsService.ts @@ -1,10 +1,13 @@ import { MonitorStats } from "../../../db/v2/models/index.js"; import { Monitor } from "../../../db/v2/models/index.js"; + +const SERVICE_NAME = "MonitorStatsServiceV2"; export interface IMonitorStatsService { cleanupOrphanedMonitorStats: () => Promise; } class MonitorStatsService implements IMonitorStatsService { + static SERVICE_NAME = SERVICE_NAME; constructor() {} async cleanupOrphanedMonitorStats() { diff --git a/server/src/service/v2/business/NotificationChannelService.ts b/server/src/service/v2/business/NotificationChannelService.ts index 35eae5b12..112efa691 100644 --- a/server/src/service/v2/business/NotificationChannelService.ts +++ b/server/src/service/v2/business/NotificationChannelService.ts @@ -1,6 +1,8 @@ import { ITokenizedUser, INotificationChannel, NotificationChannel, Monitor } from "../../../db/v2/models/index.js"; import ApiError from "../../../utils/ApiError.js"; +const SERVICE_NAME = "NotificationChannelServiceV2"; + export interface INotificationChannelService { create: ( tokenizedUser: ITokenizedUser, @@ -15,6 +17,8 @@ export interface INotificationChannelService { } class NotificationChannelService implements INotificationChannelService { + static SERVICE_NAME = SERVICE_NAME; + constructor() {} create = async (tokenizedUser: ITokenizedUser, notificationChannelData: INotificationChannel) => { diff --git a/server/src/service/v2/business/QueueService.ts b/server/src/service/v2/business/QueueService.ts index 20069072e..620544ba7 100644 --- a/server/src/service/v2/business/QueueService.ts +++ b/server/src/service/v2/business/QueueService.ts @@ -1,6 +1,9 @@ import { IJobQueue } from "../infrastructure/JobQueue.js"; +const SERVICE_NAME = "QueueServiceV2"; + class QueueService { + static SERVICE_NAME = SERVICE_NAME; private jobQueue: IJobQueue; constructor(jobQueue: IJobQueue) { diff --git a/server/src/service/v2/business/UserService.ts b/server/src/service/v2/business/UserService.ts index 46d221759..09c50dcbc 100644 --- a/server/src/service/v2/business/UserService.ts +++ b/server/src/service/v2/business/UserService.ts @@ -1,10 +1,12 @@ import { IUser, User } from "../../../db/v2/models/index.js"; +const SERVICE_NAME = "UserServiceV2"; export interface IUserService { getAllUsers(): Promise; } class UserService implements IUserService { + static SERVICE_NAME = SERVICE_NAME; async getAllUsers(): Promise { return await User.find(); } diff --git a/server/src/service/v2/infrastructure/JobGenerator.ts b/server/src/service/v2/infrastructure/JobGenerator.ts index 4f2beaf71..69b716920 100644 --- a/server/src/service/v2/infrastructure/JobGenerator.ts +++ b/server/src/service/v2/infrastructure/JobGenerator.ts @@ -7,12 +7,14 @@ import { INotificationService } from "./NotificationService.js"; import { IMaintenanceService } from "../business/MaintenanceService.js"; import ApiError from "../../../utils/ApiError.js"; +const SERVICE_NAME = "JobGeneratorV2"; export interface IJobGenerator { generateJob: () => (Monitor: IMonitor) => Promise; generateCleanupJob: () => () => Promise; } class JobGenerator implements IJobGenerator { + static SERVICE_NAME = SERVICE_NAME; private networkService: INetworkService; private checkService: ICheckService; private monitorStatsService: IMonitorStatsService; diff --git a/server/src/service/v2/infrastructure/JobQueue.ts b/server/src/service/v2/infrastructure/JobQueue.ts index af415d3f7..85479d8e5 100644 --- a/server/src/service/v2/infrastructure/JobQueue.ts +++ b/server/src/service/v2/infrastructure/JobQueue.ts @@ -2,6 +2,8 @@ import { IJob } from "super-simple-scheduler/dist/job/job.js"; import { Monitor, IMonitor } from "../../../db/v2/models/index.js"; import Scheduler from "super-simple-scheduler"; import { IJobGenerator } from "./JobGenerator.js"; + +const SERVICE_NAME = "JobQueueV2"; export interface IJobMetrics { jobs: number; activeJobs: number; @@ -36,6 +38,8 @@ export interface IJobQueue { } export default class JobQueue implements IJobQueue { + static SERVICE_NAME = SERVICE_NAME; + private scheduler: Scheduler; private static instance: JobQueue | null = null; private jobGenerator: any; diff --git a/server/src/service/v2/infrastructure/NetworkService.ts b/server/src/service/v2/infrastructure/NetworkService.ts index 90657bd2d..ab8427d84 100644 --- a/server/src/service/v2/infrastructure/NetworkService.ts +++ b/server/src/service/v2/infrastructure/NetworkService.ts @@ -1,4 +1,5 @@ import { Got, HTTPError } from "got"; +import got from "got"; import ping from "ping"; import { IMonitor } from "../../../db/v2/models/index.js"; import { GotTimings } from "../../../db/v2/models/checks/Check.js"; @@ -7,6 +8,8 @@ import type { ISystemInfo, ICaptureInfo, ILighthouseResult } from "../../../db/v import { MonitorType, MonitorStatus } from "../../../db/v2/models/monitors/Monitor.js"; import ApiError from "../../../utils/ApiError.js"; import { config } from "../../../config/index.js"; + +const SERVICE_NAME = "NetworkServiceV2"; export interface INetworkService { requestHttp: (monitor: IMonitor) => Promise; requestInfrastructure: (monitor: IMonitor) => Promise; @@ -36,9 +39,10 @@ export interface StatusResponse { } class NetworkService implements INetworkService { + static SERVICE_NAME = SERVICE_NAME; private got: Got; private NETWORK_ERROR: number; - constructor(got: Got) { + constructor() { this.got = got; this.NETWORK_ERROR = 5000; } diff --git a/server/src/service/v2/infrastructure/NotificationService.ts b/server/src/service/v2/infrastructure/NotificationService.ts index 4195958bd..2cc5e75d8 100644 --- a/server/src/service/v2/infrastructure/NotificationService.ts +++ b/server/src/service/v2/infrastructure/NotificationService.ts @@ -1,11 +1,14 @@ import UserService from "../business/UserService.js"; import { IMonitor, NotificationChannel } from "../../../db/v2/models/index.js"; import { EmailService, SlackService, DiscordService, WebhookService } from "./NotificationServices/index.js"; + +const SERVICE_NAME = "NotificationServiceV2"; export interface INotificationService { handleNotifications: (monitor: IMonitor) => Promise; } class NotificationService implements INotificationService { + static SERVICE_NAME = SERVICE_NAME; private emailService: EmailService; private slackService: SlackService; private discordService: DiscordService; diff --git a/server/src/service/v2/infrastructure/NotificationServices/Discord.ts b/server/src/service/v2/infrastructure/NotificationServices/Discord.ts index c9ec30700..c3a5d375a 100644 --- a/server/src/service/v2/infrastructure/NotificationServices/Discord.ts +++ b/server/src/service/v2/infrastructure/NotificationServices/Discord.ts @@ -2,7 +2,10 @@ import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.j import { IAlert, IMessageService } from "./IMessageService.js"; import got from "got"; import ApiError from "../../../../utils/ApiError.js"; + +const SERVICE_NAME = "DiscordServiceV2"; class DiscordService implements IMessageService { + static SERVICE_NAME = SERVICE_NAME; constructor() {} private toDiscordEmbeds = (alert: IAlert) => { diff --git a/server/src/service/v2/infrastructure/NotificationServices/Email.ts b/server/src/service/v2/infrastructure/NotificationServices/Email.ts index d1443ce08..4bed9a156 100644 --- a/server/src/service/v2/infrastructure/NotificationServices/Email.ts +++ b/server/src/service/v2/infrastructure/NotificationServices/Email.ts @@ -4,7 +4,10 @@ import nodemailer, { Transporter } from "nodemailer"; import { config } from "../../../../config/index.js"; import UserService from "../../business/UserService.js"; import ApiError from "../../../../utils/ApiError.js"; + +const SERVICE_NAME = "EmailServiceV2"; class EmailService implements IMessageService { + static SERVICE_NAME = SERVICE_NAME; private transporter: Transporter; private userService: UserService; diff --git a/server/src/service/v2/infrastructure/NotificationServices/Slack.ts b/server/src/service/v2/infrastructure/NotificationServices/Slack.ts index 50e2021e4..0be56ca56 100644 --- a/server/src/service/v2/infrastructure/NotificationServices/Slack.ts +++ b/server/src/service/v2/infrastructure/NotificationServices/Slack.ts @@ -2,7 +2,9 @@ import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.j import { IAlert, IMessageService } from "./IMessageService.js"; import got from "got"; +const SERVICE_NAME = "SlackServiceV2"; class SlackService implements IMessageService { + static SERVICE_NAME = SERVICE_NAME; constructor() {} private toSlackBlocks = (alert: IAlert) => { diff --git a/server/src/service/v2/infrastructure/NotificationServices/Webhook.ts b/server/src/service/v2/infrastructure/NotificationServices/Webhook.ts index b84b2b220..5dd89b16d 100644 --- a/server/src/service/v2/infrastructure/NotificationServices/Webhook.ts +++ b/server/src/service/v2/infrastructure/NotificationServices/Webhook.ts @@ -2,7 +2,11 @@ import { IMonitor, INotificationChannel } from "../../../../db/v2/models/index.j import { IAlert, IMessageService } from "./IMessageService.js"; import ApiError from "../../../../utils/ApiError.js"; import got from "got"; + +const SERVICE_NAME = "WebhookServiceV2"; class WebhookService implements IMessageService { + static SERVICE_NAME = SERVICE_NAME; + constructor() {} buildAlert = (monitor: IMonitor) => { diff --git a/server/src/service/v2/infrastructure/StatusService.ts b/server/src/service/v2/infrastructure/StatusService.ts index 87b0df0af..f2881b160 100644 --- a/server/src/service/v2/infrastructure/StatusService.ts +++ b/server/src/service/v2/infrastructure/StatusService.ts @@ -2,6 +2,7 @@ import { IMonitor, IMonitorStats, MonitorStats } from "../../../db/v2/models/ind import { StatusResponse } from "./NetworkService.js"; import ApiError from "../../../utils/ApiError.js"; +const SERVICE_NAME = "StatusServiceV2"; const MAX_LATEST_CHECKS = 25; export interface IStatusService { updateMonitorStatus: (monitor: IMonitor, status: StatusResponse) => Promise; @@ -14,6 +15,7 @@ export interface IStatusService { export type StatusChangeResult = [updatedMonitor: IMonitor, statusChanged: boolean]; class StatusService implements IStatusService { + static SERVICE_NAME = SERVICE_NAME; updateMonitorStatus = async (monitor: IMonitor, statusResponse: StatusResponse): Promise => { const newStatus = statusResponse.status; monitor.lastCheckedAt = new Date(); diff --git a/server/src/types/express.d.ts b/server/src/types/express.d.ts new file mode 100644 index 000000000..7c78a053c --- /dev/null +++ b/server/src/types/express.d.ts @@ -0,0 +1,10 @@ +import { ITokenizedUser } from "../db/models/index.ts"; + +declare global { + namespace Express { + interface Request { + user?: ITokenizedUser; + resource?: any; + } + } +} diff --git a/server/src/types/index.ts b/server/src/types/index.ts new file mode 100644 index 000000000..6e708b934 --- /dev/null +++ b/server/src/types/index.ts @@ -0,0 +1 @@ +export type { MonitorWithChecksResponse } from "./monitor-response-with-checks.js"; diff --git a/server/src/types/monitor-response-with-checks.ts b/server/src/types/monitor-response-with-checks.ts new file mode 100644 index 000000000..660ce0035 --- /dev/null +++ b/server/src/types/monitor-response-with-checks.ts @@ -0,0 +1,11 @@ +import { IMonitor, IMonitorStats } from "../db/v2/models/index.js"; + +export interface MonitorWithChecksResponse { + monitor: IMonitor; + checks: Array<{ + _id: string; + count: number; + avgResponseTime: number; + }>; + stats: IMonitorStats; +} From 811a6bf2225fb9808a5d34a333d453386f03bf4a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 25 Sep 2025 09:38:41 -0700 Subject: [PATCH 019/400] create and start v2 queue --- server/src/config/services.js | 10 ++++------ server/src/service/v1/infrastructure/bufferService.js | 10 +++++----- 2 files changed, 9 insertions(+), 11 deletions(-) diff --git a/server/src/config/services.js b/server/src/config/services.js index ec07c5e15..c1679868d 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -231,18 +231,12 @@ export const initializeServices = async ({ logger, envSettings, settingsService }); // V2 Services - const jobQueueV2 = new JobQueueV2(); - const authServiceV2 = new AuthServiceV2(jobQueueV2); const checkServiceV2 = new CheckServiceV2(); const inviteServiceV2 = new InviteServiceV2(); const maintenanceServiceV2 = new MaintenanceServiceV2(); - const monitorServiceV2 = new MonitorServiceV2(jobQueueV2); const monitorStatsServiceV2 = new MonitorStatsServiceV2(); const notificationChannelServiceV2 = new NotificationChannelServiceV2(); - const queueServiceV2 = new QueueServiceV2(jobQueueV2); const userServiceV2 = new UserServiceV2(); - - // V2 Infra const discordServiceV2 = new DiscordServiceV2(); const emailServiceV2 = new EmailServiceV2(userServiceV2); const slackServiceV2 = new SlackServiceV2(); @@ -258,6 +252,10 @@ export const initializeServices = async ({ logger, envSettings, settingsService notificationServiceV2, maintenanceServiceV2 ); + const jobQueueV2 = await JobQueueV2.create(jobGeneratorV2); + const authServiceV2 = new AuthServiceV2(jobQueueV2); + const monitorServiceV2 = new MonitorServiceV2(jobQueueV2); + const queueServiceV2 = new QueueServiceV2(jobQueueV2); const services = { //v1 diff --git a/server/src/service/v1/infrastructure/bufferService.js b/server/src/service/v1/infrastructure/bufferService.js index ed48cc654..459d25af3 100755 --- a/server/src/service/v1/infrastructure/bufferService.js +++ b/server/src/service/v1/infrastructure/bufferService.js @@ -64,11 +64,11 @@ class BufferService { }); } this.buffer = []; - this.logger.debug({ - message: `Flushed ${items} items`, - service: this.SERVICE_NAME, - method: "flushBuffer", - }); + // this.logger.debug({ + // message: `Flushed ${items} items`, + // service: this.SERVICE_NAME, + // method: "flushBuffer", + // }); items = 0; } } From 5957479d03e0ad84c8dca84eae95d64fffe2bc9e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 25 Sep 2025 09:46:11 -0700 Subject: [PATCH 020/400] express type --- server/package-lock.json | 98 ++++++++++++++++++++++++++++++++++++++++ server/package.json | 1 + 2 files changed, 99 insertions(+) diff --git a/server/package-lock.json b/server/package-lock.json index 2a522ae64..8c9eac34c 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -45,6 +45,7 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/express": "5.0.3", "@types/jsonwebtoken": "9.0.10", "@types/nodemailer": "7.0.1", "@types/ping": "0.4.4", @@ -3046,6 +3047,27 @@ "node": ">=10.13.0" } }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3053,12 +3075,44 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/express": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.3.tgz", + "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", + "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", "license": "MIT" }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -3084,6 +3138,13 @@ "@types/node": "*" } }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -3118,12 +3179,49 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/relateurl": { "version": "0.2.33", "resolved": "https://registry.npmjs.org/@types/relateurl/-/relateurl-0.2.33.tgz", "integrity": "sha512-bTQCKsVbIdzLqZhLkF5fcJQreE4y1ro4DIyVrlDNSCJRRwHhB8Z+4zXXa8jN6eDvc2HbRsEYgbvrnGvi54EpSw==", "license": "MIT" }, + "node_modules/@types/send": { + "version": "0.17.5", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", + "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", + "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", diff --git a/server/package.json b/server/package.json index 2fdcf58cb..3bd79722a 100755 --- a/server/package.json +++ b/server/package.json @@ -54,6 +54,7 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/express": "5.0.3", "@types/jsonwebtoken": "9.0.10", "@types/nodemailer": "7.0.1", "@types/ping": "0.4.4", From 4d89213e1c46228521d9e918cff3bb06887881b4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 25 Sep 2025 10:11:41 -0700 Subject: [PATCH 021/400] Add typescript config, move to v1 dir --- client/package.json | 4 +- client/src/Components/ActionsMenu/index.jsx | 2 +- .../src/Components/MonitorActions/index.jsx | 2 +- .../MonitorDetailsControlHeader/index.jsx | 6 +- .../MonitorDetailsControlHeader/status.jsx | 2 +- .../src/Components/PageStateWrapper/index.jsx | 2 +- client/src/Components/StatBox/index.jsx | 2 +- client/src/Hooks/{ => v1}/checkHooks.js | 4 +- client/src/Hooks/{ => v1}/inviteHooks.js | 2 +- client/src/Hooks/{ => v1}/logHooks.js | 4 +- client/src/Hooks/{ => v1}/monitorHooks.js | 6 +- client/src/Hooks/{ => v1}/settingsHooks.js | 4 +- client/src/Hooks/{ => v1}/useIsAdmin.js | 0 client/src/Hooks/{ => v1}/useMonitorUtils.js | 0 client/src/Hooks/{ => v1}/useNotifications.js | 6 +- client/src/Hooks/{ => v1}/useSendTestEmail.js | 4 +- client/src/Hooks/{ => v1}/userHooks.js | 4 +- client/src/Pages/{ => v1}/About/index.jsx | 4 +- .../Account/EditUser/hooks/editUser.js | 6 +- .../Pages/{ => v1}/Account/EditUser/index.jsx | 14 ++--- .../components/AddMemberMenu/index.jsx | 0 .../AddTeamMember/hooks/useAddTeamMember.jsx | 2 +- .../components/AddTeamMember/index.jsx | 18 +++--- .../Account/components/PasswordPanel.jsx | 14 ++--- .../Account/components/ProfilePanel.jsx | 18 +++--- .../Account/components/RoleTable/index.jsx | 4 +- .../{ => v1}/Account/components/TeamPanel.jsx | 22 +++---- client/src/Pages/{ => v1}/Account/index.jsx | 8 +-- client/src/Pages/{ => v1}/Auth/CheckEmail.jsx | 12 ++-- .../Pages/{ => v1}/Auth/ForgotPassword.jsx | 16 ++--- .../Auth/Login/hooks/useLoadingSubmit.jsx | 0 .../Auth/Login/hooks/useLoginForm.jsx | 0 .../Auth/Login/hooks/useLoginSubmit.jsx | 4 +- .../Auth/Login/hooks/useValidateLoginForm.jsx | 2 +- .../src/Pages/{ => v1}/Auth/Login/index.jsx | 16 ++--- .../{ => v1}/Auth/NewPasswordConfirmed.jsx | 10 ++-- .../Pages/{ => v1}/Auth/Register/index.jsx | 14 ++--- .../Pages/{ => v1}/Auth/SetNewPassword.jsx | 22 +++---- .../{ => v1}/Auth/components/AuthHeader.jsx | 6 +- .../Auth/components/AuthPageWrapper.jsx | 6 +- .../Auth/components/PasswordTooltip.jsx | 2 +- .../Auth/hooks/usePasswordFeedback.jsx | 2 +- .../Auth/hooks/useValidatePassword.jsx | 2 +- client/src/Pages/{ => v1}/Auth/index.css | 0 .../Components/IncidentTable/index.jsx | 22 +++---- .../Components/OptionsHeader/index.jsx | 4 +- .../Components/OptionsHeader/skeleton.jsx | 0 .../Components/StatusBoxes/StatusBox.jsx | 10 ++-- .../Components/StatusBoxes/index.jsx | 4 +- .../Components/StatusBoxes/skeleton.jsx | 0 client/src/Pages/{ => v1}/Incidents/index.jsx | 18 +++--- .../Create/Components/CustomAlertsSection.jsx | 6 +- .../Components/CustomThreshold/index.jsx | 4 +- .../Components/MonitorActionButtons.jsx | 2 +- .../Create/Components/MonitorStatusHeader.jsx | 4 +- .../hooks/useInfrastructureMonitorForm.jsx | 0 .../Create/hooks/useInfrastructureSubmit.jsx | 2 +- .../hooks/useValidateInfrastructureForm.jsx | 4 +- .../{ => v1}/Infrastructure/Create/index.jsx | 32 +++++----- .../AreaChartBoxes/InfraAreaChart.jsx | 6 +- .../Components/AreaChartBoxes/index.jsx | 8 +-- .../Components/AreaChartBoxes/skeleton.jsx | 0 .../Components/BaseContainer/index.jsx | 2 +- .../Details/Components/GaugeBoxes/Gauge.jsx | 4 +- .../Details/Components/GaugeBoxes/index.jsx | 6 +- .../Components/GaugeBoxes/skeleton.jsx | 0 .../Components/NetworkStats/NetworkCharts.jsx | 6 +- .../NetworkStats/NetworkStatBoxes.jsx | 6 +- .../Details/Components/NetworkStats/index.jsx | 8 +-- .../Components/NetworkStats/skeleton.jsx | 0 .../Details/Components/StatusBoxes/index.jsx | 8 +-- .../Details/Hooks/useHardwareUtils.jsx | 0 .../{ => v1}/Infrastructure/Details/index.jsx | 22 +++---- .../Monitors/Components/Filters/index.jsx | 2 +- .../Components/MonitorsTable/index.jsx | 16 ++--- .../Components/MonitorsTableMenu/index.jsx | 10 ++-- .../Infrastructure/Monitors/index.jsx | 22 +++---- .../src/Pages/{ => v1}/Integrations/index.css | 0 .../src/Pages/{ => v1}/Integrations/index.jsx | 6 +- .../Diagnostics/components/gauges/index.jsx | 4 +- .../Diagnostics/components/stats/index.jsx | 4 +- .../Pages/{ => v1}/Logs/Diagnostics/index.jsx | 12 ++-- .../{ => v1}/Logs/Diagnostics/utils/utils.js | 0 client/src/Pages/{ => v1}/Logs/Logs/index.jsx | 4 +- .../Queue/components/FailedJobTable/index.jsx | 4 +- .../Logs/Queue/components/JobTable/index.jsx | 6 +- .../Logs/Queue/components/Metrics/index.jsx | 4 +- .../Queue/components/MetricsTable/index.jsx | 2 +- .../src/Pages/{ => v1}/Logs/Queue/index.jsx | 8 +-- client/src/Pages/{ => v1}/Logs/index.jsx | 8 +-- .../Components/ConfigSelect/index.jsx | 2 +- .../Components/MonitorList/index.jsx | 0 .../Components/MonitorsConfig/index.jsx | 4 +- .../hooks/useMaintenanceActions.jsx | 4 +- .../hooks/useMaintenanceData.jsx | 4 +- .../Maintenance/CreateMaintenance/index.css | 0 .../Maintenance/CreateMaintenance/index.jsx | 26 ++++---- .../MaintenanceTable/ActionsMenu/index.jsx | 10 ++-- .../Maintenance/MaintenanceTable/index.jsx | 12 ++-- .../src/Pages/{ => v1}/Maintenance/index.css | 0 .../src/Pages/{ => v1}/Maintenance/index.jsx | 8 +-- client/src/Pages/{ => v1}/NotFound/index.jsx | 2 +- .../Notifications/components/ActionMenu.jsx | 0 .../{ => v1}/Notifications/create/index.jsx | 18 +++--- .../Pages/{ => v1}/Notifications/index.jsx | 10 ++-- .../src/Pages/{ => v1}/Notifications/utils.js | 0 .../Pages/{ => v1}/PageSpeed/Create/index.css | 0 .../Pages/{ => v1}/PageSpeed/Create/index.jsx | 32 +++++----- .../{ => v1}/PageSpeed/Create/skeleton.jsx | 0 .../Details/Components/Charts/AreaChart.jsx | 2 +- .../Components/Charts/AreaChartLegend.jsx | 6 +- .../Details/Components/Charts/PieChart.jsx | 0 .../Components/Charts/PieChartLegend.jsx | 4 +- .../Components/PageSpeedAreaChart/index.jsx | 10 ++-- .../PageSpeedAreaChart/skeleton.jsx | 0 .../Components/PageSpeedStatusBoxes/index.jsx | 6 +- .../Components/PerformanceReport/index.jsx | 10 ++-- .../Components/PerformanceReport/skeleton.jsx | 0 .../{ => v1}/PageSpeed/Details/index.jsx | 18 +++--- .../Monitors/Components/Card/index.jsx | 10 ++-- .../Monitors/Components/MonitorGrid/index.jsx | 2 +- .../{ => v1}/PageSpeed/Monitors/index.jsx | 18 +++--- .../src/Pages/{ => v1}/ServerUnreachable.jsx | 14 ++--- .../Pages/{ => v1}/Settings/SettingsAbout.jsx | 4 +- .../Settings/SettingsDemoMonitors.jsx | 4 +- .../Pages/{ => v1}/Settings/SettingsEmail.jsx | 12 ++-- .../Settings/SettingsGlobalThresholds.jsx | 4 +- .../{ => v1}/Settings/SettingsPagespeed.jsx | 6 +- .../Pages/{ => v1}/Settings/SettingsStats.jsx | 6 +- .../{ => v1}/Settings/SettingsTimeZone.jsx | 6 +- .../Pages/{ => v1}/Settings/SettingsUI.jsx | 4 +- .../Pages/{ => v1}/Settings/SettingsURL.jsx | 4 +- client/src/Pages/{ => v1}/Settings/index.jsx | 30 +++++----- .../Create/Components/MonitorList/index.jsx | 0 .../Create/Components/Progress/index.jsx | 4 +- .../Create/Components/Skeleton/index.jsx | 0 .../Create/Components/Tabs/ConfigStack.jsx | 2 +- .../Create/Components/Tabs/Content.jsx | 8 +-- .../Create/Components/Tabs/Settings.jsx | 16 ++--- .../Create/Components/Tabs/index.jsx | 6 +- .../Create/Hooks/useCreateStatusPage.jsx | 4 +- .../Create/Hooks/useMonitorsFetch.jsx | 4 +- .../{ => v1}/StatusPage/Create/index.jsx | 24 ++++---- .../Status/Components/AdminLink/index.jsx | 0 .../Components/ControlsHeader/index.jsx | 6 +- .../Status/Components/MonitorsList/index.jsx | 8 +-- .../Status/Components/Skeleton/index.jsx | 0 .../Status/Components/StatusBar/index.jsx | 0 .../Status/Hooks/useStatusPageDelete.jsx | 4 +- .../Status/Hooks/useStatusPageFetch.jsx | 6 +- .../{ => v1}/StatusPage/Status/index.jsx | 20 +++---- .../Components/StatusPagesTable/index.jsx | 4 +- .../StatusPages/Hooks/useStatusPagesFetch.jsx | 4 +- .../{ => v1}/StatusPage/StatusPages/index.jsx | 12 ++-- .../{ => v1}/Uptime/BulkImport/Upload.jsx | 0 .../{ => v1}/Uptime/BulkImport/index.jsx | 10 ++-- .../Pages/{ => v1}/Uptime/Create/index.css | 0 .../Pages/{ => v1}/Uptime/Create/index.jsx | 32 +++++----- .../Pages/{ => v1}/Uptime/Create/skeleton.jsx | 0 .../Details/Components/ChartBoxes/index.jsx | 18 +++--- .../Components/ChartBoxes/skeleton.jsx | 0 .../Components/Charts/CustomLabels.jsx | 2 +- .../Components/Charts/DownBarChart.jsx | 2 +- .../Components/Charts/ResponseGaugeChart.jsx | 0 .../Components/Charts/ResponseTimeChart.jsx | 8 +-- .../Charts/ResponseTimeChartSkeleton.jsx | 0 .../Details/Components/Charts/UpBarChart.jsx | 2 +- .../Components/ResponseTable/index.jsx | 14 ++--- .../Components/ResponseTable/skeleton.jsx | 0 .../Components/UptimeStatusBoxes/index.jsx | 8 +-- .../Details/Hooks/useCertificateFetch.jsx | 6 +- .../Pages/{ => v1}/Uptime/Details/index.jsx | 24 ++++---- .../Monitors/Components/Filter/index.jsx | 2 +- .../Components/LoadingSpinner/index.jsx | 0 .../Components/SearchComponent/index.jsx | 4 +- .../Monitors/Components/Skeleton/index.jsx | 0 .../Monitors/Components/StatusBoxes/index.jsx | 4 +- .../Components/StatusBoxes/skeleton.jsx | 0 .../Components/StatusBoxes/statusBox.jsx | 6 +- .../Components/UptimeDataTable/index.jsx | 16 ++--- .../Uptime/Monitors/Hooks/useDebounce.jsx | 0 .../Pages/{ => v1}/Uptime/Monitors/index.jsx | 26 ++++---- client/src/Routes/index.jsx | 60 +++++++++---------- client/tsconfig.app.json | 32 ++++++++++ client/tsconfig.json | 14 +++++ client/tsconfig.node.json | 27 +++++++++ client/{vite.config.js => vite.config.ts} | 6 +- 187 files changed, 708 insertions(+), 637 deletions(-) rename client/src/Hooks/{ => v1}/checkHooks.js (97%) rename client/src/Hooks/{ => v1}/inviteHooks.js (97%) rename client/src/Hooks/{ => v1}/logHooks.js (96%) rename client/src/Hooks/{ => v1}/monitorHooks.js (98%) rename client/src/Hooks/{ => v1}/settingsHooks.js (95%) rename client/src/Hooks/{ => v1}/useIsAdmin.js (100%) rename client/src/Hooks/{ => v1}/useMonitorUtils.js (100%) rename client/src/Hooks/{ => v1}/useNotifications.js (96%) rename client/src/Hooks/{ => v1}/useSendTestEmail.js (93%) rename client/src/Hooks/{ => v1}/userHooks.js (92%) rename client/src/Pages/{ => v1}/About/index.jsx (67%) rename client/src/Pages/{ => v1}/Account/EditUser/hooks/editUser.js (91%) rename client/src/Pages/{ => v1}/Account/EditUser/index.jsx (86%) rename client/src/Pages/{ => v1}/Account/components/AddMemberMenu/index.jsx (100%) rename client/src/Pages/{ => v1}/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx (93%) rename client/src/Pages/{ => v1}/Account/components/AddTeamMember/index.jsx (91%) rename client/src/Pages/{ => v1}/Account/components/PasswordPanel.jsx (92%) rename client/src/Pages/{ => v1}/Account/components/ProfilePanel.jsx (94%) rename client/src/Pages/{ => v1}/Account/components/RoleTable/index.jsx (87%) rename client/src/Pages/{ => v1}/Account/components/TeamPanel.jsx (92%) rename client/src/Pages/{ => v1}/Account/index.jsx (91%) rename client/src/Pages/{ => v1}/Auth/CheckEmail.jsx (93%) rename client/src/Pages/{ => v1}/Auth/ForgotPassword.jsx (91%) rename client/src/Pages/{ => v1}/Auth/Login/hooks/useLoadingSubmit.jsx (100%) rename client/src/Pages/{ => v1}/Auth/Login/hooks/useLoginForm.jsx (100%) rename client/src/Pages/{ => v1}/Auth/Login/hooks/useLoginSubmit.jsx (87%) rename client/src/Pages/{ => v1}/Auth/Login/hooks/useValidateLoginForm.jsx (90%) rename client/src/Pages/{ => v1}/Auth/Login/index.jsx (82%) rename client/src/Pages/{ => v1}/Auth/NewPasswordConfirmed.jsx (90%) rename client/src/Pages/{ => v1}/Auth/Register/index.jsx (94%) rename client/src/Pages/{ => v1}/Auth/SetNewPassword.jsx (91%) rename client/src/Pages/{ => v1}/Auth/components/AuthHeader.jsx (81%) rename client/src/Pages/{ => v1}/Auth/components/AuthPageWrapper.jsx (92%) rename client/src/Pages/{ => v1}/Auth/components/PasswordTooltip.jsx (98%) rename client/src/Pages/{ => v1}/Auth/hooks/usePasswordFeedback.jsx (96%) rename client/src/Pages/{ => v1}/Auth/hooks/useValidatePassword.jsx (96%) rename client/src/Pages/{ => v1}/Auth/index.css (100%) rename client/src/Pages/{ => v1}/Incidents/Components/IncidentTable/index.jsx (88%) rename client/src/Pages/{ => v1}/Incidents/Components/OptionsHeader/index.jsx (96%) rename client/src/Pages/{ => v1}/Incidents/Components/OptionsHeader/skeleton.jsx (100%) rename client/src/Pages/{ => v1}/Incidents/Components/StatusBoxes/StatusBox.jsx (87%) rename client/src/Pages/{ => v1}/Incidents/Components/StatusBoxes/index.jsx (92%) rename client/src/Pages/{ => v1}/Incidents/Components/StatusBoxes/skeleton.jsx (100%) rename client/src/Pages/{ => v1}/Incidents/index.jsx (81%) rename client/src/Pages/{ => v1}/Infrastructure/Create/Components/CustomAlertsSection.jsx (91%) rename client/src/Pages/{ => v1}/Infrastructure/Create/Components/CustomThreshold/index.jsx (95%) rename client/src/Pages/{ => v1}/Infrastructure/Create/Components/MonitorActionButtons.jsx (96%) rename client/src/Pages/{ => v1}/Infrastructure/Create/Components/MonitorStatusHeader.jsx (91%) rename client/src/Pages/{ => v1}/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx (100%) rename client/src/Pages/{ => v1}/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx (98%) rename client/src/Pages/{ => v1}/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx (85%) rename client/src/Pages/{ => v1}/Infrastructure/Create/index.jsx (92%) rename client/src/Pages/{ => v1}/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx (79%) rename client/src/Pages/{ => v1}/Infrastructure/Details/Components/AreaChartBoxes/index.jsx (93%) rename client/src/Pages/{ => v1}/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx (100%) rename client/src/Pages/{ => v1}/Infrastructure/Details/Components/BaseContainer/index.jsx (94%) rename client/src/Pages/{ => v1}/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx (93%) rename client/src/Pages/{ => v1}/Infrastructure/Details/Components/GaugeBoxes/index.jsx (93%) rename client/src/Pages/{ => v1}/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx (100%) rename client/src/Pages/{ => v1}/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx (93%) rename client/src/Pages/{ => v1}/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx (90%) rename client/src/Pages/{ => v1}/Infrastructure/Details/Components/NetworkStats/index.jsx (90%) rename client/src/Pages/{ => v1}/Infrastructure/Details/Components/NetworkStats/skeleton.jsx (100%) rename client/src/Pages/{ => v1}/Infrastructure/Details/Components/StatusBoxes/index.jsx (90%) rename client/src/Pages/{ => v1}/Infrastructure/Details/Hooks/useHardwareUtils.jsx (100%) rename client/src/Pages/{ => v1}/Infrastructure/Details/index.jsx (79%) rename client/src/Pages/{ => v1}/Infrastructure/Monitors/Components/Filters/index.jsx (96%) rename client/src/Pages/{ => v1}/Infrastructure/Monitors/Components/MonitorsTable/index.jsx (86%) rename client/src/Pages/{ => v1}/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx (93%) rename client/src/Pages/{ => v1}/Infrastructure/Monitors/index.jsx (80%) rename client/src/Pages/{ => v1}/Integrations/index.css (100%) rename client/src/Pages/{ => v1}/Integrations/index.jsx (94%) rename client/src/Pages/{ => v1}/Logs/Diagnostics/components/gauges/index.jsx (96%) rename client/src/Pages/{ => v1}/Logs/Diagnostics/components/stats/index.jsx (94%) rename client/src/Pages/{ => v1}/Logs/Diagnostics/index.jsx (82%) rename client/src/Pages/{ => v1}/Logs/Diagnostics/utils/utils.js (100%) rename client/src/Pages/{ => v1}/Logs/Logs/index.jsx (94%) rename client/src/Pages/{ => v1}/Logs/Queue/components/FailedJobTable/index.jsx (92%) rename client/src/Pages/{ => v1}/Logs/Queue/components/JobTable/index.jsx (92%) rename client/src/Pages/{ => v1}/Logs/Queue/components/Metrics/index.jsx (84%) rename client/src/Pages/{ => v1}/Logs/Queue/components/MetricsTable/index.jsx (94%) rename client/src/Pages/{ => v1}/Logs/Queue/index.jsx (86%) rename client/src/Pages/{ => v1}/Logs/index.jsx (84%) rename client/src/Pages/{ => v1}/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx (92%) rename client/src/Pages/{ => v1}/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx (100%) rename client/src/Pages/{ => v1}/Maintenance/CreateMaintenance/Components/MonitorsConfig/index.jsx (93%) rename client/src/Pages/{ => v1}/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx (93%) rename client/src/Pages/{ => v1}/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx (95%) rename client/src/Pages/{ => v1}/Maintenance/CreateMaintenance/index.css (100%) rename client/src/Pages/{ => v1}/Maintenance/CreateMaintenance/index.jsx (93%) rename client/src/Pages/{ => v1}/Maintenance/MaintenanceTable/ActionsMenu/index.jsx (91%) rename client/src/Pages/{ => v1}/Maintenance/MaintenanceTable/index.jsx (92%) rename client/src/Pages/{ => v1}/Maintenance/index.css (100%) rename client/src/Pages/{ => v1}/Maintenance/index.jsx (91%) rename client/src/Pages/{ => v1}/NotFound/index.jsx (96%) rename client/src/Pages/{ => v1}/Notifications/components/ActionMenu.jsx (100%) rename client/src/Pages/{ => v1}/Notifications/create/index.jsx (92%) rename client/src/Pages/{ => v1}/Notifications/index.jsx (89%) rename client/src/Pages/{ => v1}/Notifications/utils.js (100%) rename client/src/Pages/{ => v1}/PageSpeed/Create/index.css (100%) rename client/src/Pages/{ => v1}/PageSpeed/Create/index.jsx (91%) rename client/src/Pages/{ => v1}/PageSpeed/Create/skeleton.jsx (100%) rename client/src/Pages/{ => v1}/PageSpeed/Details/Components/Charts/AreaChart.jsx (99%) rename client/src/Pages/{ => v1}/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx (84%) rename client/src/Pages/{ => v1}/PageSpeed/Details/Components/Charts/PieChart.jsx (100%) rename client/src/Pages/{ => v1}/PageSpeed/Details/Components/Charts/PieChartLegend.jsx (92%) rename client/src/Pages/{ => v1}/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx (75%) rename client/src/Pages/{ => v1}/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx (100%) rename client/src/Pages/{ => v1}/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx (76%) rename client/src/Pages/{ => v1}/PageSpeed/Details/Components/PerformanceReport/index.jsx (78%) rename client/src/Pages/{ => v1}/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx (100%) rename client/src/Pages/{ => v1}/PageSpeed/Details/index.jsx (81%) rename client/src/Pages/{ => v1}/PageSpeed/Monitors/Components/Card/index.jsx (95%) rename client/src/Pages/{ => v1}/PageSpeed/Monitors/Components/MonitorGrid/index.jsx (88%) rename client/src/Pages/{ => v1}/PageSpeed/Monitors/index.jsx (69%) rename client/src/Pages/{ => v1}/ServerUnreachable.jsx (90%) rename client/src/Pages/{ => v1}/Settings/SettingsAbout.jsx (88%) rename client/src/Pages/{ => v1}/Settings/SettingsDemoMonitors.jsx (95%) rename client/src/Pages/{ => v1}/Settings/SettingsEmail.jsx (94%) rename client/src/Pages/{ => v1}/Settings/SettingsGlobalThresholds.jsx (94%) rename client/src/Pages/{ => v1}/Settings/SettingsPagespeed.jsx (91%) rename client/src/Pages/{ => v1}/Settings/SettingsStats.jsx (92%) rename client/src/Pages/{ => v1}/Settings/SettingsTimeZone.jsx (90%) rename client/src/Pages/{ => v1}/Settings/SettingsUI.jsx (91%) rename client/src/Pages/{ => v1}/Settings/SettingsURL.jsx (90%) rename client/src/Pages/{ => v1}/Settings/index.jsx (89%) rename client/src/Pages/{ => v1}/StatusPage/Create/Components/MonitorList/index.jsx (100%) rename client/src/Pages/{ => v1}/StatusPage/Create/Components/Progress/index.jsx (83%) rename client/src/Pages/{ => v1}/StatusPage/Create/Components/Skeleton/index.jsx (100%) rename client/src/Pages/{ => v1}/StatusPage/Create/Components/Tabs/ConfigStack.jsx (91%) rename client/src/Pages/{ => v1}/StatusPage/Create/Components/Tabs/Content.jsx (90%) rename client/src/Pages/{ => v1}/StatusPage/Create/Components/Tabs/Settings.jsx (87%) rename client/src/Pages/{ => v1}/StatusPage/Create/Components/Tabs/index.jsx (93%) rename client/src/Pages/{ => v1}/StatusPage/Create/Hooks/useCreateStatusPage.jsx (83%) rename client/src/Pages/{ => v1}/StatusPage/Create/Hooks/useMonitorsFetch.jsx (88%) rename client/src/Pages/{ => v1}/StatusPage/Create/index.jsx (92%) rename client/src/Pages/{ => v1}/StatusPage/Status/Components/AdminLink/index.jsx (100%) rename client/src/Pages/{ => v1}/StatusPage/Status/Components/ControlsHeader/index.jsx (92%) rename client/src/Pages/{ => v1}/StatusPage/Status/Components/MonitorsList/index.jsx (82%) rename client/src/Pages/{ => v1}/StatusPage/Status/Components/Skeleton/index.jsx (100%) rename client/src/Pages/{ => v1}/StatusPage/Status/Components/StatusBar/index.jsx (100%) rename client/src/Pages/{ => v1}/StatusPage/Status/Hooks/useStatusPageDelete.jsx (89%) rename client/src/Pages/{ => v1}/StatusPage/Status/Hooks/useStatusPageFetch.jsx (87%) rename client/src/Pages/{ => v1}/StatusPage/Status/index.jsx (85%) rename client/src/Pages/{ => v1}/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx (93%) rename client/src/Pages/{ => v1}/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx (86%) rename client/src/Pages/{ => v1}/StatusPage/StatusPages/index.jsx (70%) rename client/src/Pages/{ => v1}/Uptime/BulkImport/Upload.jsx (100%) rename client/src/Pages/{ => v1}/Uptime/BulkImport/index.jsx (89%) rename client/src/Pages/{ => v1}/Uptime/Create/index.css (100%) rename client/src/Pages/{ => v1}/Uptime/Create/index.jsx (94%) rename client/src/Pages/{ => v1}/Uptime/Create/skeleton.jsx (100%) rename client/src/Pages/{ => v1}/Uptime/Details/Components/ChartBoxes/index.jsx (86%) rename client/src/Pages/{ => v1}/Uptime/Details/Components/ChartBoxes/skeleton.jsx (100%) rename client/src/Pages/{ => v1}/Uptime/Details/Components/Charts/CustomLabels.jsx (93%) rename client/src/Pages/{ => v1}/Uptime/Details/Components/Charts/DownBarChart.jsx (97%) rename client/src/Pages/{ => v1}/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx (100%) rename client/src/Pages/{ => v1}/Uptime/Details/Components/Charts/ResponseTimeChart.jsx (60%) rename client/src/Pages/{ => v1}/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx (100%) rename client/src/Pages/{ => v1}/Uptime/Details/Components/Charts/UpBarChart.jsx (98%) rename client/src/Pages/{ => v1}/Uptime/Details/Components/ResponseTable/index.jsx (76%) rename client/src/Pages/{ => v1}/Uptime/Details/Components/ResponseTable/skeleton.jsx (100%) rename client/src/Pages/{ => v1}/Uptime/Details/Components/UptimeStatusBoxes/index.jsx (85%) rename client/src/Pages/{ => v1}/Uptime/Details/Hooks/useCertificateFetch.jsx (84%) rename client/src/Pages/{ => v1}/Uptime/Details/index.jsx (83%) rename client/src/Pages/{ => v1}/Uptime/Monitors/Components/Filter/index.jsx (98%) rename client/src/Pages/{ => v1}/Uptime/Monitors/Components/LoadingSpinner/index.jsx (100%) rename client/src/Pages/{ => v1}/Uptime/Monitors/Components/SearchComponent/index.jsx (89%) rename client/src/Pages/{ => v1}/Uptime/Monitors/Components/Skeleton/index.jsx (100%) rename client/src/Pages/{ => v1}/Uptime/Monitors/Components/StatusBoxes/index.jsx (91%) rename client/src/Pages/{ => v1}/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx (100%) rename client/src/Pages/{ => v1}/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx (90%) rename client/src/Pages/{ => v1}/Uptime/Monitors/Components/UptimeDataTable/index.jsx (90%) rename client/src/Pages/{ => v1}/Uptime/Monitors/Hooks/useDebounce.jsx (100%) rename client/src/Pages/{ => v1}/Uptime/Monitors/index.jsx (86%) create mode 100644 client/tsconfig.app.json create mode 100644 client/tsconfig.json create mode 100644 client/tsconfig.node.json rename client/{vite.config.js => vite.config.ts} (63%) diff --git a/client/package.json b/client/package.json index 3108f7121..9ebc99647 100644 --- a/client/package.json +++ b/client/package.json @@ -5,8 +5,8 @@ "type": "module", "scripts": { "dev": "vite", - "build": "vite build", - "build-dev": "vite build --mode development", + "build": "tsc -b && vite build", + "build-dev": "tsc -b && vite build --mode development", "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", "preview": "vite preview", "format": "prettier --write .", diff --git a/client/src/Components/ActionsMenu/index.jsx b/client/src/Components/ActionsMenu/index.jsx index 1b590a471..78d86e1b2 100644 --- a/client/src/Components/ActionsMenu/index.jsx +++ b/client/src/Components/ActionsMenu/index.jsx @@ -12,7 +12,7 @@ import { useNavigate } from "react-router-dom"; import { createToast } from "../../Utils/toastUtils"; import PropTypes from "prop-types"; -import { usePauseMonitor, useDeleteMonitor } from "../../Hooks/monitorHooks"; +import { usePauseMonitor, useDeleteMonitor } from "../../Hooks/v1/monitorHooks.js"; const ActionsMenu = ({ monitor, diff --git a/client/src/Components/MonitorActions/index.jsx b/client/src/Components/MonitorActions/index.jsx index ac5d89a47..9e88e26c5 100644 --- a/client/src/Components/MonitorActions/index.jsx +++ b/client/src/Components/MonitorActions/index.jsx @@ -11,7 +11,7 @@ import MenuList from "@mui/material/MenuList"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { createToast } from "../../Utils/toastUtils"; -import { useExportMonitors } from "../../Hooks/monitorHooks"; +import { useExportMonitors } from "../../Hooks/v1/monitorHooks.js"; const MonitorActions = ({ isLoading }) => { const [open, setOpen] = React.useState(false); diff --git a/client/src/Components/MonitorDetailsControlHeader/index.jsx b/client/src/Components/MonitorDetailsControlHeader/index.jsx index 4674a1fb7..cd56394f4 100644 --- a/client/src/Components/MonitorDetailsControlHeader/index.jsx +++ b/client/src/Components/MonitorDetailsControlHeader/index.jsx @@ -12,10 +12,10 @@ import EmailIcon from "@mui/icons-material/Email"; import PropTypes from "prop-types"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@mui/material/styles"; -import { usePauseMonitor } from "../../Hooks/monitorHooks"; -import { useSendTestEmail } from "../../Hooks/useSendTestEmail"; +import { usePauseMonitor } from "../../Hooks/v1/monitorHooks.js"; +import { useSendTestEmail } from "../../Hooks/v1/useSendTestEmail.js"; import { useTranslation } from "react-i18next"; -import { useTestAllNotifications } from "../../Hooks/useNotifications"; +import { useTestAllNotifications } from "../../Hooks/v1/useNotifications.js"; /** * MonitorDetailsControlHeader component displays the control header for monitor details. * It includes status display, pause/resume button, and a configure button for admins. diff --git a/client/src/Components/MonitorDetailsControlHeader/status.jsx b/client/src/Components/MonitorDetailsControlHeader/status.jsx index 65671a609..2cf749861 100644 --- a/client/src/Components/MonitorDetailsControlHeader/status.jsx +++ b/client/src/Components/MonitorDetailsControlHeader/status.jsx @@ -7,7 +7,7 @@ import Dot from "../../Components/Dot"; import { formatDurationRounded } from "../../Utils/timeUtils"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "../../Hooks/useMonitorUtils"; +import { useMonitorUtils } from "../../Hooks/v1/useMonitorUtils.js"; import { formatMonitorUrl } from "../../Utils/utils"; /** * Status component displays the status information of a monitor. diff --git a/client/src/Components/PageStateWrapper/index.jsx b/client/src/Components/PageStateWrapper/index.jsx index f971b2c9c..c335fcbb3 100644 --- a/client/src/Components/PageStateWrapper/index.jsx +++ b/client/src/Components/PageStateWrapper/index.jsx @@ -3,7 +3,7 @@ import Fallback from "../Fallback"; import { Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import { useIsAdmin } from "../../Hooks/useIsAdmin"; +import { useIsAdmin } from "../../Hooks/v1/useIsAdmin.js"; import PropTypes from "prop-types"; const PageStateWrapper = ({ networkError, diff --git a/client/src/Components/StatBox/index.jsx b/client/src/Components/StatBox/index.jsx index 7d093de60..15eb61f74 100644 --- a/client/src/Components/StatBox/index.jsx +++ b/client/src/Components/StatBox/index.jsx @@ -2,7 +2,7 @@ import { Stack, Typography } from "@mui/material"; import Image from "../Image"; import { useTheme } from "@mui/material/styles"; import PropTypes from "prop-types"; -import { useMonitorUtils } from "../../Hooks/useMonitorUtils"; +import { useMonitorUtils } from "../../Hooks/v1/useMonitorUtils.js"; /** * StatBox Component diff --git a/client/src/Hooks/checkHooks.js b/client/src/Hooks/v1/checkHooks.js similarity index 97% rename from client/src/Hooks/checkHooks.js rename to client/src/Hooks/v1/checkHooks.js index bdb1a73b7..96fd1fe1c 100644 --- a/client/src/Hooks/checkHooks.js +++ b/client/src/Hooks/v1/checkHooks.js @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; -import { networkService } from "../main"; -import { createToast } from "../Utils/toastUtils"; +import { networkService } from "../../main.jsx"; +import { createToast } from "../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; const useFetchChecksTeam = ({ diff --git a/client/src/Hooks/inviteHooks.js b/client/src/Hooks/v1/inviteHooks.js similarity index 97% rename from client/src/Hooks/inviteHooks.js rename to client/src/Hooks/v1/inviteHooks.js index fb577272a..b6a0851af 100644 --- a/client/src/Hooks/inviteHooks.js +++ b/client/src/Hooks/v1/inviteHooks.js @@ -1,5 +1,5 @@ import { useState } from "react"; -import { networkService } from "../main"; +import { networkService } from "../../main.jsx"; import { useTranslation } from "react-i18next"; const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST; diff --git a/client/src/Hooks/logHooks.js b/client/src/Hooks/v1/logHooks.js similarity index 96% rename from client/src/Hooks/logHooks.js rename to client/src/Hooks/v1/logHooks.js index 989870eea..ff4d13872 100644 --- a/client/src/Hooks/logHooks.js +++ b/client/src/Hooks/v1/logHooks.js @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; -import { networkService } from "../main"; -import { createToast } from "../Utils/toastUtils"; +import { networkService } from "../../main.jsx"; +import { createToast } from "../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; const useFetchLogs = () => { diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/v1/monitorHooks.js similarity index 98% rename from client/src/Hooks/monitorHooks.js rename to client/src/Hooks/v1/monitorHooks.js index eb1e7acdc..e6b4b5261 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/v1/monitorHooks.js @@ -1,8 +1,8 @@ import { useEffect, useState } from "react"; -import { networkService } from "../main"; -import { createToast } from "../Utils/toastUtils"; +import { networkService } from "../../main.jsx"; +import { createToast } from "../../Utils/toastUtils.jsx"; import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "./useMonitorUtils"; +import { useMonitorUtils } from "./useMonitorUtils.js"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Hooks/settingsHooks.js b/client/src/Hooks/v1/settingsHooks.js similarity index 95% rename from client/src/Hooks/settingsHooks.js rename to client/src/Hooks/v1/settingsHooks.js index 98a21c1fa..787e46ff9 100644 --- a/client/src/Hooks/settingsHooks.js +++ b/client/src/Hooks/v1/settingsHooks.js @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; -import { networkService } from "../main"; -import { createToast } from "../Utils/toastUtils"; +import { networkService } from "../../main.jsx"; +import { createToast } from "../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; const useFetchSettings = ({ setSettingsData, setIsApiKeySet, setIsEmailPasswordSet }) => { diff --git a/client/src/Hooks/useIsAdmin.js b/client/src/Hooks/v1/useIsAdmin.js similarity index 100% rename from client/src/Hooks/useIsAdmin.js rename to client/src/Hooks/v1/useIsAdmin.js diff --git a/client/src/Hooks/useMonitorUtils.js b/client/src/Hooks/v1/useMonitorUtils.js similarity index 100% rename from client/src/Hooks/useMonitorUtils.js rename to client/src/Hooks/v1/useMonitorUtils.js diff --git a/client/src/Hooks/useNotifications.js b/client/src/Hooks/v1/useNotifications.js similarity index 96% rename from client/src/Hooks/useNotifications.js rename to client/src/Hooks/v1/useNotifications.js index 539725958..d1b4a6652 100644 --- a/client/src/Hooks/useNotifications.js +++ b/client/src/Hooks/v1/useNotifications.js @@ -1,9 +1,9 @@ import { useState, useEffect, useCallback } from "react"; -import { createToast } from "../Utils/toastUtils"; -import { networkService } from "../main"; +import { createToast } from "../../Utils/toastUtils.jsx"; +import { networkService } from "../../main.jsx"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { NOTIFICATION_TYPES } from "../Pages/Notifications/utils"; +import { NOTIFICATION_TYPES } from "../../Pages/v1/Notifications/utils.js"; const useCreateNotification = () => { const navigate = useNavigate(); diff --git a/client/src/Hooks/useSendTestEmail.js b/client/src/Hooks/v1/useSendTestEmail.js similarity index 93% rename from client/src/Hooks/useSendTestEmail.js rename to client/src/Hooks/v1/useSendTestEmail.js index e280b1f99..0be77954a 100644 --- a/client/src/Hooks/useSendTestEmail.js +++ b/client/src/Hooks/v1/useSendTestEmail.js @@ -1,7 +1,7 @@ import { useState } from "react"; -import { networkService } from "../main"; +import { networkService } from "../../main.jsx"; import { useSelector } from "react-redux"; -import { createToast } from "../Utils/toastUtils"; +import { createToast } from "../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; const useSendTestEmail = () => { diff --git a/client/src/Hooks/userHooks.js b/client/src/Hooks/v1/userHooks.js similarity index 92% rename from client/src/Hooks/userHooks.js rename to client/src/Hooks/v1/userHooks.js index 8be32a082..71b156594 100644 --- a/client/src/Hooks/userHooks.js +++ b/client/src/Hooks/v1/userHooks.js @@ -1,6 +1,6 @@ import { useEffect, useState, useCallback } from "react"; -import { networkService } from "../main"; -import { createToast } from "../Utils/toastUtils"; +import { networkService } from "../../main.jsx"; +import { createToast } from "../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; export const useGetUser = (userId) => { diff --git a/client/src/Pages/About/index.jsx b/client/src/Pages/v1/About/index.jsx similarity index 67% rename from client/src/Pages/About/index.jsx rename to client/src/Pages/v1/About/index.jsx index 97040b2e3..0e68089d3 100644 --- a/client/src/Pages/About/index.jsx +++ b/client/src/Pages/v1/About/index.jsx @@ -1,6 +1,6 @@ import React from "react"; -import AppBar from "../../Components/Common/AppBar"; -import Footer from "../../Components/Common/Footer"; +import AppBar from "../../../Components/Common/AppBar.jsx"; +import Footer from "../../../Components/Common/Footer.jsx"; import { useTranslation } from "react-i18next"; const AboutUs = () => { diff --git a/client/src/Pages/Account/EditUser/hooks/editUser.js b/client/src/Pages/v1/Account/EditUser/hooks/editUser.js similarity index 91% rename from client/src/Pages/Account/EditUser/hooks/editUser.js rename to client/src/Pages/v1/Account/EditUser/hooks/editUser.js index 2ffa914a0..00660aead 100644 --- a/client/src/Pages/Account/EditUser/hooks/editUser.js +++ b/client/src/Pages/v1/Account/EditUser/hooks/editUser.js @@ -1,8 +1,8 @@ import { useState, useEffect } from "react"; -import { ROLES } from "../../../../Utils/roleUtils"; -import { editUserValidation } from "../../../../Validation/validation"; +import { ROLES } from "../../../../../Utils/roleUtils.js"; +import { editUserValidation } from "../../../../../Validation/validation.js"; import Joi from "joi"; -import { createToast } from "../../../../Utils/toastUtils"; +import { createToast } from "../../../../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; export const useEditUserForm = (user) => { const [searchInput, setSearchInput] = useState(""); diff --git a/client/src/Pages/Account/EditUser/index.jsx b/client/src/Pages/v1/Account/EditUser/index.jsx similarity index 86% rename from client/src/Pages/Account/EditUser/index.jsx rename to client/src/Pages/v1/Account/EditUser/index.jsx index e1eeead9d..658de5144 100644 --- a/client/src/Pages/Account/EditUser/index.jsx +++ b/client/src/Pages/v1/Account/EditUser/index.jsx @@ -2,19 +2,19 @@ import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import TextInput from "../../../Components/Inputs/TextInput"; -import Search from "../../../Components/Inputs/Search"; +import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; +import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; +import Search from "../../../../Components/Inputs/Search/index.jsx"; import Button from "@mui/material/Button"; -import RoleTable from "../components/RoleTable"; +import RoleTable from "../components/RoleTable/index.jsx"; // Utils import { useParams } from "react-router-dom"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import { useGetUser, useEditUser } from "../../../Hooks/userHooks"; -import { EDITABLE_ROLES, ROLES } from "../../../Utils/roleUtils"; -import { useEditUserForm, useValidateEditUserForm } from "./hooks/editUser"; +import { useGetUser, useEditUser } from "../../../../Hooks/v1/userHooks.js"; +import { EDITABLE_ROLES, ROLES } from "../../../../Utils/roleUtils.js"; +import { useEditUserForm, useValidateEditUserForm } from "./hooks/editUser.js"; const EditUser = () => { const { userId } = useParams(); diff --git a/client/src/Pages/Account/components/AddMemberMenu/index.jsx b/client/src/Pages/v1/Account/components/AddMemberMenu/index.jsx similarity index 100% rename from client/src/Pages/Account/components/AddMemberMenu/index.jsx rename to client/src/Pages/v1/Account/components/AddMemberMenu/index.jsx diff --git a/client/src/Pages/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx b/client/src/Pages/v1/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx similarity index 93% rename from client/src/Pages/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx rename to client/src/Pages/v1/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx index e42c504b2..58db019a3 100644 --- a/client/src/Pages/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx +++ b/client/src/Pages/v1/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { newOrChangedCredentials } from "../../../../../Validation/validation"; +import { newOrChangedCredentials } from "../../../../../../Validation/validation.js"; import { useTranslation } from "react-i18next"; const useAddTeamMember = () => { const { t } = useTranslation(); diff --git a/client/src/Pages/Account/components/AddTeamMember/index.jsx b/client/src/Pages/v1/Account/components/AddTeamMember/index.jsx similarity index 91% rename from client/src/Pages/Account/components/AddTeamMember/index.jsx rename to client/src/Pages/v1/Account/components/AddTeamMember/index.jsx index 2df8fb226..e17ed433b 100644 --- a/client/src/Pages/Account/components/AddTeamMember/index.jsx +++ b/client/src/Pages/v1/Account/components/AddTeamMember/index.jsx @@ -1,16 +1,16 @@ import { Button, Stack } from "@mui/material"; -import { GenericDialog } from "../../../../Components/Dialog/genericDialog"; -import TextInput from "../../../../Components/Inputs/TextInput"; -import Select from "../../../../Components/Inputs/Select"; -import { useGetInviteToken } from "../../../../Hooks/inviteHooks"; +import { GenericDialog } from "../../../../../Components/Dialog/genericDialog.jsx"; +import TextInput from "../../../../../Components/Inputs/TextInput/index.jsx"; +import Select from "../../../../../Components/Inputs/Select/index.jsx"; +import { useGetInviteToken } from "../../../../../Hooks/v1/inviteHooks.js"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import { createToast } from "../../../../Utils/toastUtils"; +import { createToast } from "../../../../../Utils/toastUtils.jsx"; import { useState } from "react"; -import PasswordTooltip from "../../../Auth/components/PasswordTooltip"; -import useAddTeamMember from "./hooks/useAddTeamMember"; -import usePasswordFeedback from "../../../Auth/hooks/usePasswordFeedback"; -import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments"; +import PasswordTooltip from "../../../Auth/components/PasswordTooltip.jsx"; +import useAddTeamMember from "./hooks/useAddTeamMember.jsx"; +import usePasswordFeedback from "../../../Auth/hooks/usePasswordFeedback.jsx"; +import { PasswordEndAdornment } from "../../../../../Components/Inputs/TextInput/Adornments/index.jsx"; import PropTypes from "prop-types"; const INITIAL_FORM_STATE = { diff --git a/client/src/Pages/Account/components/PasswordPanel.jsx b/client/src/Pages/v1/Account/components/PasswordPanel.jsx similarity index 92% rename from client/src/Pages/Account/components/PasswordPanel.jsx rename to client/src/Pages/v1/Account/components/PasswordPanel.jsx index dabd67706..11961bf58 100644 --- a/client/src/Pages/Account/components/PasswordPanel.jsx +++ b/client/src/Pages/v1/Account/components/PasswordPanel.jsx @@ -2,14 +2,14 @@ import TabPanel from "@mui/lab/TabPanel"; import { useState } from "react"; import { useTheme } from "@emotion/react"; import { Box, Stack, Typography, Button } from "@mui/material"; -import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments"; -import TextInput from "../../../Components/Inputs/TextInput"; -import { newOrChangedCredentials } from "../../../Validation/validation"; -import Alert from "../../../Components/Alert"; -import { update } from "../../../Features/Auth/authSlice"; +import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments/index.jsx"; +import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; +import { newOrChangedCredentials } from "../../../../Validation/validation.js"; +import Alert from "../../../../Components/Alert/index.jsx"; +import { update } from "../../../../Features/Auth/authSlice.js"; import { useDispatch, useSelector } from "react-redux"; -import { createToast } from "../../../Utils/toastUtils"; -import { getTouchedFieldErrors } from "../../../Validation/error"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; +import { getTouchedFieldErrors } from "../../../../Validation/error.js"; import { useTranslation } from "react-i18next"; const defaultPasswordsState = { diff --git a/client/src/Pages/Account/components/ProfilePanel.jsx b/client/src/Pages/v1/Account/components/ProfilePanel.jsx similarity index 94% rename from client/src/Pages/Account/components/ProfilePanel.jsx rename to client/src/Pages/v1/Account/components/ProfilePanel.jsx index 7c73e7aa3..8e607f4bd 100644 --- a/client/src/Pages/Account/components/ProfilePanel.jsx +++ b/client/src/Pages/v1/Account/components/ProfilePanel.jsx @@ -2,16 +2,16 @@ import { useTheme } from "@emotion/react"; import { useState } from "react"; import TabPanel from "@mui/lab/TabPanel"; import { Box, Button, Divider, Stack, Typography } from "@mui/material"; -import Avatar from "../../../Components/Avatar"; -import TextInput from "../../../Components/Inputs/TextInput"; -import ImageUpload from "../../../Components/Inputs/ImageUpload"; -import { newOrChangedCredentials } from "../../../Validation/validation"; +import Avatar from "../../../../Components/Avatar/index.jsx"; +import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; +import ImageUpload from "../../../../Components/Inputs/ImageUpload/index.jsx"; +import { newOrChangedCredentials } from "../../../../Validation/validation.js"; import { useDispatch, useSelector } from "react-redux"; -import { clearAuthState, deleteUser, update } from "../../../Features/Auth/authSlice"; -import { createToast } from "../../../Utils/toastUtils"; -import { logger } from "../../../Utils/Logger"; -import { GenericDialog } from "../../../Components/Dialog/genericDialog"; -import Dialog from "../../../Components/Dialog"; +import { clearAuthState, deleteUser, update } from "../../../../Features/Auth/authSlice.js"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; +import { logger } from "../../../../Utils/Logger.js"; +import { GenericDialog } from "../../../../Components/Dialog/genericDialog.jsx"; +import Dialog from "../../../../Components/Dialog/index.jsx"; import { useTranslation } from "react-i18next"; /** diff --git a/client/src/Pages/Account/components/RoleTable/index.jsx b/client/src/Pages/v1/Account/components/RoleTable/index.jsx similarity index 87% rename from client/src/Pages/Account/components/RoleTable/index.jsx rename to client/src/Pages/v1/Account/components/RoleTable/index.jsx index 9d6ba3a16..c42be7bcd 100644 --- a/client/src/Pages/Account/components/RoleTable/index.jsx +++ b/client/src/Pages/v1/Account/components/RoleTable/index.jsx @@ -1,9 +1,9 @@ import Typography from "@mui/material/Typography"; -import DataTable from "../../../../Components/Table"; +import DataTable from "../../../../../Components/Table/index.jsx"; import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded"; import { useTranslation } from "react-i18next"; -import { ROLES } from "../../../../Utils/roleUtils"; +import { ROLES } from "../../../../../Utils/roleUtils.js"; const RoleTable = ({ roles, handleDeleteRole }) => { const { t } = useTranslation(); diff --git a/client/src/Pages/Account/components/TeamPanel.jsx b/client/src/Pages/v1/Account/components/TeamPanel.jsx similarity index 92% rename from client/src/Pages/Account/components/TeamPanel.jsx rename to client/src/Pages/v1/Account/components/TeamPanel.jsx index fc1768642..8964cd277 100644 --- a/client/src/Pages/Account/components/TeamPanel.jsx +++ b/client/src/Pages/v1/Account/components/TeamPanel.jsx @@ -3,18 +3,18 @@ import TabPanel from "@mui/lab/TabPanel"; import { Button, ButtonGroup, Stack, Typography } from "@mui/material"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import TextInput from "../../../Components/Inputs/TextInput"; -import { newOrChangedCredentials } from "../../../Validation/validation"; -import { networkService } from "../../../main"; -import { createToast } from "../../../Utils/toastUtils"; -import Select from "../../../Components/Inputs/Select"; -import { GenericDialog } from "../../../Components/Dialog/genericDialog"; -import AddTeamMember from "../components/AddTeamMember"; -import DataTable from "../../../Components/Table"; -import { useGetInviteToken } from "../../../Hooks/inviteHooks"; +import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; +import { newOrChangedCredentials } from "../../../../Validation/validation.js"; +import { networkService } from "../../../../main.jsx"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; +import Select from "../../../../Components/Inputs/Select/index.jsx"; +import { GenericDialog } from "../../../../Components/Dialog/genericDialog.jsx"; +import AddTeamMember from "./AddTeamMember/index.jsx"; +import DataTable from "../../../../Components/Table/index.jsx"; +import { useGetInviteToken } from "../../../../Hooks/v1/inviteHooks.js"; import { useNavigate } from "react-router-dom"; -import { useIsSuperAdmin } from "../../../Hooks/useIsAdmin"; -import AddMemberMenu from "./AddMemberMenu"; +import { useIsSuperAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; +import AddMemberMenu from "./AddMemberMenu/index.jsx"; /** * TeamPanel component manages the organization and team members, * providing functionalities like renaming the organization, managing team members, diff --git a/client/src/Pages/Account/index.jsx b/client/src/Pages/v1/Account/index.jsx similarity index 91% rename from client/src/Pages/Account/index.jsx rename to client/src/Pages/v1/Account/index.jsx index 37c64aca6..a496858b7 100644 --- a/client/src/Pages/Account/index.jsx +++ b/client/src/Pages/v1/Account/index.jsx @@ -3,11 +3,11 @@ import PropTypes from "prop-types"; import { useNavigate } from "react-router-dom"; import { useSelector } from "react-redux"; import { Box, Tab, useTheme } from "@mui/material"; -import CustomTabList from "../../Components/Tab"; +import CustomTabList from "../../../Components/Tab/index.jsx"; import TabContext from "@mui/lab/TabContext"; -import ProfilePanel from "./components/ProfilePanel"; -import PasswordPanel from "./components/PasswordPanel"; -import TeamPanel from "./components/TeamPanel"; +import ProfilePanel from "./components/ProfilePanel.jsx"; +import PasswordPanel from "./components/PasswordPanel.jsx"; +import TeamPanel from "./components/TeamPanel.jsx"; import { useTranslation } from "react-i18next"; /** diff --git a/client/src/Pages/Auth/CheckEmail.jsx b/client/src/Pages/v1/Auth/CheckEmail.jsx similarity index 93% rename from client/src/Pages/Auth/CheckEmail.jsx rename to client/src/Pages/v1/Auth/CheckEmail.jsx index e1f5e43ed..95217bcb5 100644 --- a/client/src/Pages/Auth/CheckEmail.jsx +++ b/client/src/Pages/v1/Auth/CheckEmail.jsx @@ -3,13 +3,13 @@ import { useEffect, useState } from "react"; import { useTheme } from "@emotion/react"; import { useDispatch } from "react-redux"; import { useNavigate } from "react-router-dom"; -import { createToast } from "../../Utils/toastUtils"; -import { forgotPassword } from "../../Features/Auth/authSlice"; +import { createToast } from "../../../Utils/toastUtils.jsx"; +import { forgotPassword } from "../../../Features/Auth/authSlice.js"; import { Trans, useTranslation } from "react-i18next"; -import Background from "../../assets/Images/background-grid.svg?react"; -import EmailIcon from "../../assets/icons/email.svg?react"; -import Logo from "../../assets/icons/checkmate-icon.svg?react"; -import IconBox from "../../Components/IconBox"; +import Background from "../../../assets/Images/background-grid.svg?react"; +import EmailIcon from "../../../assets/icons/email.svg?react"; +import Logo from "../../../assets/icons/checkmate-icon.svg?react"; +import IconBox from "../../../Components/IconBox/index.jsx"; import "./index.css"; const CheckEmail = () => { diff --git a/client/src/Pages/Auth/ForgotPassword.jsx b/client/src/Pages/v1/Auth/ForgotPassword.jsx similarity index 91% rename from client/src/Pages/Auth/ForgotPassword.jsx rename to client/src/Pages/v1/Auth/ForgotPassword.jsx index 2a887756e..9836f5d77 100644 --- a/client/src/Pages/Auth/ForgotPassword.jsx +++ b/client/src/Pages/v1/Auth/ForgotPassword.jsx @@ -1,16 +1,16 @@ import { Box, Stack, Typography, Button } from "@mui/material"; import { useTheme } from "@emotion/react"; -import { createToast } from "../../Utils/toastUtils"; +import { createToast } from "../../../Utils/toastUtils.jsx"; import { useDispatch, useSelector } from "react-redux"; -import { forgotPassword } from "../../Features/Auth/authSlice"; +import { forgotPassword } from "../../../Features/Auth/authSlice.js"; import { useEffect, useState } from "react"; -import { newOrChangedCredentials } from "../../Validation/validation"; +import { newOrChangedCredentials } from "../../../Validation/validation.js"; import { useNavigate } from "react-router-dom"; -import TextInput from "../../Components/Inputs/TextInput"; -import Logo from "../../assets/icons/checkmate-icon.svg?react"; -import Key from "../../assets/icons/key.svg?react"; -import Background from "../../assets/Images/background-grid.svg?react"; -import IconBox from "../../Components/IconBox"; +import TextInput from "../../../Components/Inputs/TextInput/index.jsx"; +import Logo from "../../../assets/icons/checkmate-icon.svg?react"; +import Key from "../../../assets/icons/key.svg?react"; +import Background from "../../../assets/Images/background-grid.svg?react"; +import IconBox from "../../../Components/IconBox/index.jsx"; import { Trans, useTranslation } from "react-i18next"; import "./index.css"; diff --git a/client/src/Pages/Auth/Login/hooks/useLoadingSubmit.jsx b/client/src/Pages/v1/Auth/Login/hooks/useLoadingSubmit.jsx similarity index 100% rename from client/src/Pages/Auth/Login/hooks/useLoadingSubmit.jsx rename to client/src/Pages/v1/Auth/Login/hooks/useLoadingSubmit.jsx diff --git a/client/src/Pages/Auth/Login/hooks/useLoginForm.jsx b/client/src/Pages/v1/Auth/Login/hooks/useLoginForm.jsx similarity index 100% rename from client/src/Pages/Auth/Login/hooks/useLoginForm.jsx rename to client/src/Pages/v1/Auth/Login/hooks/useLoginForm.jsx diff --git a/client/src/Pages/Auth/Login/hooks/useLoginSubmit.jsx b/client/src/Pages/v1/Auth/Login/hooks/useLoginSubmit.jsx similarity index 87% rename from client/src/Pages/Auth/Login/hooks/useLoginSubmit.jsx rename to client/src/Pages/v1/Auth/Login/hooks/useLoginSubmit.jsx index 6bc5716ff..0eed9bd07 100644 --- a/client/src/Pages/Auth/Login/hooks/useLoginSubmit.jsx +++ b/client/src/Pages/v1/Auth/Login/hooks/useLoginSubmit.jsx @@ -1,7 +1,7 @@ import { useDispatch } from "react-redux"; -import { login } from "../../../../Features/Auth/authSlice"; +import { login } from "../../../../../Features/Auth/authSlice.js"; import { useNavigate } from "react-router-dom"; -import { createToast } from "../../../../Utils/toastUtils"; +import { createToast } from "../../../../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; const useLoginSubmit = () => { diff --git a/client/src/Pages/Auth/Login/hooks/useValidateLoginForm.jsx b/client/src/Pages/v1/Auth/Login/hooks/useValidateLoginForm.jsx similarity index 90% rename from client/src/Pages/Auth/Login/hooks/useValidateLoginForm.jsx rename to client/src/Pages/v1/Auth/Login/hooks/useValidateLoginForm.jsx index e5d045be8..ca9bef879 100644 --- a/client/src/Pages/Auth/Login/hooks/useValidateLoginForm.jsx +++ b/client/src/Pages/v1/Auth/Login/hooks/useValidateLoginForm.jsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { loginCredentials } from "../../../../Validation/validation"; +import { loginCredentials } from "../../../../../Validation/validation.js"; const useValidateLoginForm = () => { const [errors, setErrors] = useState({ diff --git a/client/src/Pages/Auth/Login/index.jsx b/client/src/Pages/v1/Auth/Login/index.jsx similarity index 82% rename from client/src/Pages/Auth/Login/index.jsx rename to client/src/Pages/v1/Auth/Login/index.jsx index d06ecef6d..901edcd8b 100644 --- a/client/src/Pages/Auth/Login/index.jsx +++ b/client/src/Pages/v1/Auth/Login/index.jsx @@ -1,17 +1,17 @@ // Components import Stack from "@mui/material/Stack"; import Button from "@mui/material/Button"; -import TextInput from "../../../Components/Inputs/TextInput"; -import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments"; -import TextLink from "../../../Components/TextLink"; -import AuthPageWrapper from "../components/AuthPageWrapper"; +import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; +import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments/index.jsx"; +import TextLink from "../../../../Components/TextLink/index.jsx"; +import AuthPageWrapper from "../components/AuthPageWrapper.jsx"; // Utils import { useTheme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; -import useLoginForm from "./hooks/useLoginForm"; -import useValidateLoginForm from "./hooks/useValidateLoginForm"; -import useLoginSubmit from "./hooks/useLoginSubmit"; -import useLoadingSubmit from "./hooks/useLoadingSubmit"; +import useLoginForm from "./hooks/useLoginForm.jsx"; +import useValidateLoginForm from "./hooks/useValidateLoginForm.jsx"; +import useLoginSubmit from "./hooks/useLoginSubmit.jsx"; +import useLoadingSubmit from "./hooks/useLoadingSubmit.jsx"; const Login = () => { // Hooks diff --git a/client/src/Pages/Auth/NewPasswordConfirmed.jsx b/client/src/Pages/v1/Auth/NewPasswordConfirmed.jsx similarity index 90% rename from client/src/Pages/Auth/NewPasswordConfirmed.jsx rename to client/src/Pages/v1/Auth/NewPasswordConfirmed.jsx index 7ace77abc..2bede1ae4 100644 --- a/client/src/Pages/Auth/NewPasswordConfirmed.jsx +++ b/client/src/Pages/v1/Auth/NewPasswordConfirmed.jsx @@ -2,11 +2,11 @@ import { Box, Button, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; import { useDispatch } from "react-redux"; -import { clearAuthState } from "../../Features/Auth/authSlice"; -import Background from "../../assets/Images/background-grid.svg?react"; -import ConfirmIcon from "../../assets/icons/check-outlined.svg?react"; -import Logo from "../../assets/icons/checkmate-icon.svg?react"; -import IconBox from "../../Components/IconBox"; +import { clearAuthState } from "../../../Features/Auth/authSlice.js"; +import Background from "../../../assets/Images/background-grid.svg?react"; +import ConfirmIcon from "../../../assets/icons/check-outlined.svg?react"; +import Logo from "../../../assets/icons/checkmate-icon.svg?react"; +import IconBox from "../../../Components/IconBox/index.jsx"; import { Trans, useTranslation } from "react-i18next"; import "./index.css"; diff --git a/client/src/Pages/Auth/Register/index.jsx b/client/src/Pages/v1/Auth/Register/index.jsx similarity index 94% rename from client/src/Pages/Auth/Register/index.jsx rename to client/src/Pages/v1/Auth/Register/index.jsx index 2b45d2d15..a903602d4 100644 --- a/client/src/Pages/Auth/Register/index.jsx +++ b/client/src/Pages/v1/Auth/Register/index.jsx @@ -1,10 +1,10 @@ // Components import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import TextInput from "../../../Components/Inputs/TextInput"; +import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; import Button from "@mui/material/Button"; import Box from "@mui/material/Box"; -import PasswordTooltip from "../components/PasswordTooltip"; +import PasswordTooltip from "../components/PasswordTooltip.jsx"; // Utils import { useTheme } from "@emotion/react"; @@ -13,11 +13,11 @@ import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { useDispatch, useSelector } from "react-redux"; import { useParams } from "react-router-dom"; -import { networkService } from "../../../main"; -import { newOrChangedCredentials } from "../../../Validation/validation"; -import { register } from "../../../Features/Auth/authSlice"; -import AuthPageWrapper from "../components/AuthPageWrapper"; -import { createToast } from "../../../Utils/toastUtils"; +import { networkService } from "../../../../main.jsx"; +import { newOrChangedCredentials } from "../../../../Validation/validation.js"; +import { register } from "../../../../Features/Auth/authSlice.js"; +import AuthPageWrapper from "../components/AuthPageWrapper.jsx"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; import PropTypes from "prop-types"; const getFeedbackStatus = (form, errors, field, criteria) => { diff --git a/client/src/Pages/Auth/SetNewPassword.jsx b/client/src/Pages/v1/Auth/SetNewPassword.jsx similarity index 91% rename from client/src/Pages/Auth/SetNewPassword.jsx rename to client/src/Pages/v1/Auth/SetNewPassword.jsx index b4899f47c..e9df89d88 100644 --- a/client/src/Pages/Auth/SetNewPassword.jsx +++ b/client/src/Pages/v1/Auth/SetNewPassword.jsx @@ -4,18 +4,18 @@ import { useNavigate } from "react-router-dom"; import { useParams } from "react-router-dom"; import { useTheme } from "@emotion/react"; import { Box, Stack, Typography, Button } from "@mui/material"; -import { setNewPassword } from "../../Features/Auth/authSlice"; -import { createToast } from "../../Utils/toastUtils"; -import { newOrChangedCredentials } from "../../Validation/validation"; -import Check from "../../Components/Check/Check"; -import TextInput from "../../Components/Inputs/TextInput"; -import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments"; -import IconBox from "../../Components/IconBox"; -import LockIcon from "../../assets/icons/lock.svg?react"; -import Logo from "../../assets/icons/checkmate-icon.svg?react"; -import Background from "../../assets/Images/background-grid.svg?react"; +import { setNewPassword } from "../../../Features/Auth/authSlice.js"; +import { createToast } from "../../../Utils/toastUtils.jsx"; +import { newOrChangedCredentials } from "../../../Validation/validation.js"; +import Check from "../../../Components/Check/Check.jsx"; +import TextInput from "../../../Components/Inputs/TextInput/index.jsx"; +import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments/index.jsx"; +import IconBox from "../../../Components/IconBox/index.jsx"; +import LockIcon from "../../../assets/icons/lock.svg?react"; +import Logo from "../../../assets/icons/checkmate-icon.svg?react"; +import Background from "../../../assets/Images/background-grid.svg?react"; import "./index.css"; -import { useValidatePassword } from "./hooks/useValidatePassword"; +import { useValidatePassword } from "./hooks/useValidatePassword.jsx"; import { Trans, useTranslation } from "react-i18next"; const SetNewPassword = () => { diff --git a/client/src/Pages/Auth/components/AuthHeader.jsx b/client/src/Pages/v1/Auth/components/AuthHeader.jsx similarity index 81% rename from client/src/Pages/Auth/components/AuthHeader.jsx rename to client/src/Pages/v1/Auth/components/AuthHeader.jsx index ce454c065..067c47d92 100644 --- a/client/src/Pages/Auth/components/AuthHeader.jsx +++ b/client/src/Pages/v1/Auth/components/AuthHeader.jsx @@ -1,9 +1,9 @@ // Components import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import Logo from "../../../assets/icons/checkmate-icon.svg?react"; -import LanguageSelector from "../../../Components/LanguageSelector"; -import ThemeSwitch from "../../../Components/ThemeSwitch"; +import Logo from "../../../../assets/icons/checkmate-icon.svg?react"; +import LanguageSelector from "../../../../Components/LanguageSelector.jsx"; +import ThemeSwitch from "../../../../Components/ThemeSwitch/index.jsx"; // Utils import { useTheme } from "@mui/material/styles"; diff --git a/client/src/Pages/Auth/components/AuthPageWrapper.jsx b/client/src/Pages/v1/Auth/components/AuthPageWrapper.jsx similarity index 92% rename from client/src/Pages/Auth/components/AuthPageWrapper.jsx rename to client/src/Pages/v1/Auth/components/AuthPageWrapper.jsx index b83be19c5..7e358e6ab 100644 --- a/client/src/Pages/Auth/components/AuthPageWrapper.jsx +++ b/client/src/Pages/v1/Auth/components/AuthPageWrapper.jsx @@ -1,10 +1,10 @@ -import Background from "../../../assets/Images/background-grid.svg?react"; +import Background from "../../../../assets/Images/background-grid.svg?react"; import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; -import AuthHeader from "../components/AuthHeader"; +import AuthHeader from "./AuthHeader.jsx"; import { useTheme } from "@mui/material/styles"; -import Logo from "../../../assets/icons/checkmate-icon.svg?react"; +import Logo from "../../../../assets/icons/checkmate-icon.svg?react"; import PropTypes from "prop-types"; const AuthPageWrapper = ({ children, heading, welcome }) => { diff --git a/client/src/Pages/Auth/components/PasswordTooltip.jsx b/client/src/Pages/v1/Auth/components/PasswordTooltip.jsx similarity index 98% rename from client/src/Pages/Auth/components/PasswordTooltip.jsx rename to client/src/Pages/v1/Auth/components/PasswordTooltip.jsx index bc2f28ec7..ff3c2f385 100644 --- a/client/src/Pages/Auth/components/PasswordTooltip.jsx +++ b/client/src/Pages/v1/Auth/components/PasswordTooltip.jsx @@ -1,4 +1,4 @@ -import Check from "../../../Components/Check/Check"; +import Check from "../../../../Components/Check/Check.jsx"; import Stack from "@mui/material/Stack"; import { Tooltip, useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/Auth/hooks/usePasswordFeedback.jsx b/client/src/Pages/v1/Auth/hooks/usePasswordFeedback.jsx similarity index 96% rename from client/src/Pages/Auth/hooks/usePasswordFeedback.jsx rename to client/src/Pages/v1/Auth/hooks/usePasswordFeedback.jsx index 4159e6166..3c199e25f 100644 --- a/client/src/Pages/Auth/hooks/usePasswordFeedback.jsx +++ b/client/src/Pages/v1/Auth/hooks/usePasswordFeedback.jsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { newOrChangedCredentials } from "../../../Validation/validation"; +import { newOrChangedCredentials } from "../../../../Validation/validation.js"; const usePasswordFeedback = () => { const [feedback, setFeedback] = useState({}); diff --git a/client/src/Pages/Auth/hooks/useValidatePassword.jsx b/client/src/Pages/v1/Auth/hooks/useValidatePassword.jsx similarity index 96% rename from client/src/Pages/Auth/hooks/useValidatePassword.jsx rename to client/src/Pages/v1/Auth/hooks/useValidatePassword.jsx index 102d8d73e..231b8eeb8 100644 --- a/client/src/Pages/Auth/hooks/useValidatePassword.jsx +++ b/client/src/Pages/v1/Auth/hooks/useValidatePassword.jsx @@ -1,5 +1,5 @@ import { useMemo, useState } from "react"; -import { newOrChangedCredentials } from "../../../Validation/validation"; +import { newOrChangedCredentials } from "../../../../Validation/validation.js"; const getFeedbackStatus = (form, errors, field, criteria) => { const fieldErrors = errors[field]; diff --git a/client/src/Pages/Auth/index.css b/client/src/Pages/v1/Auth/index.css similarity index 100% rename from client/src/Pages/Auth/index.css rename to client/src/Pages/v1/Auth/index.css diff --git a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx b/client/src/Pages/v1/Incidents/Components/IncidentTable/index.jsx similarity index 88% rename from client/src/Pages/Incidents/Components/IncidentTable/index.jsx rename to client/src/Pages/v1/Incidents/Components/IncidentTable/index.jsx index df9174290..e204d7167 100644 --- a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx +++ b/client/src/Pages/v1/Incidents/Components/IncidentTable/index.jsx @@ -1,26 +1,26 @@ //Components import Stack from "@mui/material/Stack"; -import DataTable from "../../../../Components/Table"; +import DataTable from "../../../../../Components/Table/index.jsx"; import Table from "@mui/material/Table"; import TableBody from "@mui/material/TableBody"; import TableRow from "@mui/material/TableRow"; import TableCell from "@mui/material/TableCell"; -import TableSkeleton from "../../../../Components/Table/skeleton"; -import Pagination from "../../../../Components/Table/TablePagination"; -import { StatusLabel } from "../../../../Components/Label"; -import { HttpStatusLabel } from "../../../../Components/HttpStatusLabel"; -import GenericFallback from "../../../../Components/GenericFallback"; -import NetworkError from "../../../../Components/GenericFallback/NetworkError"; +import TableSkeleton from "../../../../../Components/Table/skeleton.jsx"; +import Pagination from "../../../../../Components/Table/TablePagination/index.jsx"; +import { StatusLabel } from "../../../../../Components/Label/index.jsx"; +import { HttpStatusLabel } from "../../../../../Components/HttpStatusLabel/index.jsx"; +import GenericFallback from "../../../../../Components/GenericFallback/index.jsx"; +import NetworkError from "../../../../../Components/GenericFallback/NetworkError.jsx"; //Utils -import { formatDateWithTz } from "../../../../Utils/timeUtils"; +import { formatDateWithTz } from "../../../../../Utils/timeUtils.js"; import { useSelector } from "react-redux"; import { useState } from "react"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; -import { useFetchChecksTeam } from "../../../../Hooks/checkHooks"; -import { useFetchChecksByMonitor } from "../../../../Hooks/checkHooks"; -import { useResolveIncident } from "../../../../Hooks/checkHooks"; +import { useFetchChecksTeam } from "../../../../../Hooks/v1/checkHooks.js"; +import { useFetchChecksByMonitor } from "../../../../../Hooks/v1/checkHooks.js"; +import { useResolveIncident } from "../../../../../Hooks/v1/checkHooks.js"; import { Button, Typography, useTheme } from "@mui/material"; import { lighten } from "@mui/material/styles"; diff --git a/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx b/client/src/Pages/v1/Incidents/Components/OptionsHeader/index.jsx similarity index 96% rename from client/src/Pages/Incidents/Components/OptionsHeader/index.jsx rename to client/src/Pages/v1/Incidents/Components/OptionsHeader/index.jsx index 259880223..b1bde1175 100644 --- a/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx +++ b/client/src/Pages/v1/Incidents/Components/OptionsHeader/index.jsx @@ -1,11 +1,11 @@ // Components import { Stack, Typography, Button, ButtonGroup } from "@mui/material"; -import Select from "../../../../Components/Inputs/Select"; +import Select from "../../../../../Components/Inputs/Select/index.jsx"; import PropTypes from "prop-types"; //Utils import { useTheme } from "@emotion/react"; -import SkeletonLayout from "./skeleton"; +import SkeletonLayout from "./skeleton.jsx"; import { useTranslation } from "react-i18next"; const OptionsHeader = ({ diff --git a/client/src/Pages/Incidents/Components/OptionsHeader/skeleton.jsx b/client/src/Pages/v1/Incidents/Components/OptionsHeader/skeleton.jsx similarity index 100% rename from client/src/Pages/Incidents/Components/OptionsHeader/skeleton.jsx rename to client/src/Pages/v1/Incidents/Components/OptionsHeader/skeleton.jsx diff --git a/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx b/client/src/Pages/v1/Incidents/Components/StatusBoxes/StatusBox.jsx similarity index 87% rename from client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx rename to client/src/Pages/v1/Incidents/Components/StatusBoxes/StatusBox.jsx index 7b20e3d51..cd7078e6c 100644 --- a/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx +++ b/client/src/Pages/v1/Incidents/Components/StatusBoxes/StatusBox.jsx @@ -1,11 +1,11 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { Box, Stack, Typography } from "@mui/material"; -import Background from "../../../../assets/Images/background-grid.svg?react"; -import AlertIcon from "../../../../assets/icons/alert-icon.svg?react"; -import CheckIcon from "../../../../assets/icons/check-icon.svg?react"; -import CloseIcon from "../../../../assets/icons/close-icon.svg?react"; -import WarningIcon from "../../../../assets/icons/warning-icon.svg?react"; +import Background from "../../../../../assets/Images/background-grid.svg?react"; +import AlertIcon from "../../../../../assets/icons/alert-icon.svg?react"; +import CheckIcon from "../../../../../assets/icons/check-icon.svg?react"; +import CloseIcon from "../../../../../assets/icons/close-icon.svg?react"; +import WarningIcon from "../../../../../assets/icons/warning-icon.svg?react"; const StatusBox = ({ title, value, status }) => { const theme = useTheme(); diff --git a/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx b/client/src/Pages/v1/Incidents/Components/StatusBoxes/index.jsx similarity index 92% rename from client/src/Pages/Incidents/Components/StatusBoxes/index.jsx rename to client/src/Pages/v1/Incidents/Components/StatusBoxes/index.jsx index 6ba2f1820..3bcd3fd3a 100644 --- a/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx +++ b/client/src/Pages/v1/Incidents/Components/StatusBoxes/index.jsx @@ -1,9 +1,9 @@ import PropTypes from "prop-types"; import { Stack } from "@mui/material"; -import StatusBox from "./StatusBox"; +import StatusBox from "./StatusBox.jsx"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import SkeletonLayout from "./skeleton"; +import SkeletonLayout from "./skeleton.jsx"; const StatusBoxes = ({ isLoading, summary }) => { const theme = useTheme(); diff --git a/client/src/Pages/Incidents/Components/StatusBoxes/skeleton.jsx b/client/src/Pages/v1/Incidents/Components/StatusBoxes/skeleton.jsx similarity index 100% rename from client/src/Pages/Incidents/Components/StatusBoxes/skeleton.jsx rename to client/src/Pages/v1/Incidents/Components/StatusBoxes/skeleton.jsx diff --git a/client/src/Pages/Incidents/index.jsx b/client/src/Pages/v1/Incidents/index.jsx similarity index 81% rename from client/src/Pages/Incidents/index.jsx rename to client/src/Pages/v1/Incidents/index.jsx index 7696c453b..b9281f97c 100644 --- a/client/src/Pages/Incidents/index.jsx +++ b/client/src/Pages/v1/Incidents/index.jsx @@ -1,19 +1,19 @@ // Components import { Stack } from "@mui/material"; -import Breadcrumbs from "../../Components/Breadcrumbs"; -import GenericFallback from "../../Components/GenericFallback"; -import IncidentTable from "./Components/IncidentTable"; -import OptionsHeader from "./Components/OptionsHeader"; -import StatusBoxes from "./Components/StatusBoxes"; +import Breadcrumbs from "../../../Components/Breadcrumbs/index.jsx"; +import GenericFallback from "../../../Components/GenericFallback/index.jsx"; +import IncidentTable from "./Components/IncidentTable/index.jsx"; +import OptionsHeader from "./Components/OptionsHeader/index.jsx"; +import StatusBoxes from "./Components/StatusBoxes/index.jsx"; import { Box, Button } from "@mui/material"; //Utils import { useTheme } from "@emotion/react"; -import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks"; -import { useFetchChecksSummaryByTeamId } from "../../Hooks/checkHooks"; -import { useAcknowledgeChecks } from "../../Hooks/checkHooks"; +import { useFetchMonitorsByTeamId } from "../../../Hooks/v1/monitorHooks.js"; +import { useFetchChecksSummaryByTeamId } from "../../../Hooks/v1/checkHooks.js"; +import { useAcknowledgeChecks } from "../../../Hooks/v1/checkHooks.js"; import { useState, useEffect } from "react"; -import NetworkError from "../../Components/GenericFallback/NetworkError"; +import NetworkError from "../../../Components/GenericFallback/NetworkError.jsx"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; diff --git a/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx b/client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx similarity index 91% rename from client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx rename to client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx index 5a6f5e981..299331a79 100644 --- a/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx +++ b/client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx @@ -1,7 +1,7 @@ -import ConfigBox from "../../../../Components/ConfigBox"; +import ConfigBox from "../../../../../Components/ConfigBox/index.jsx"; import { Box, Stack, Typography } from "@mui/material"; -import { CustomThreshold } from "../Components/CustomThreshold"; -import { capitalizeFirstLetter } from "../../../../Utils/stringUtils"; +import { CustomThreshold } from "./CustomThreshold/index.jsx"; +import { capitalizeFirstLetter } from "../../../../../Utils/stringUtils.js"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; import PropTypes from "prop-types"; diff --git a/client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx b/client/src/Pages/v1/Infrastructure/Create/Components/CustomThreshold/index.jsx similarity index 95% rename from client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx rename to client/src/Pages/v1/Infrastructure/Create/Components/CustomThreshold/index.jsx index 865698f44..4edc0b766 100644 --- a/client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Create/Components/CustomThreshold/index.jsx @@ -1,6 +1,6 @@ import { Box, Stack, Typography } from "@mui/material"; -import TextInput from "../../../../../Components/Inputs/TextInput"; -import Checkbox from "../../../../../Components/Inputs/Checkbox"; +import TextInput from "../../../../../../Components/Inputs/TextInput/index.jsx"; +import Checkbox from "../../../../../../Components/Inputs/Checkbox/index.jsx"; import { useTheme } from "@emotion/react"; import PropTypes from "prop-types"; diff --git a/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx b/client/src/Pages/v1/Infrastructure/Create/Components/MonitorActionButtons.jsx similarity index 96% rename from client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx rename to client/src/Pages/v1/Infrastructure/Create/Components/MonitorActionButtons.jsx index becda3e3a..16df8ad01 100644 --- a/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx +++ b/client/src/Pages/v1/Infrastructure/Create/Components/MonitorActionButtons.jsx @@ -4,7 +4,7 @@ import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline"; import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; -import Dialog from "../../../../Components/Dialog"; +import Dialog from "../../../../../Components/Dialog/index.jsx"; import PropTypes from "prop-types"; const MonitorActionButtons = ({ monitor, isBusy, handlePause, handleRemove }) => { diff --git a/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx b/client/src/Pages/v1/Infrastructure/Create/Components/MonitorStatusHeader.jsx similarity index 91% rename from client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx rename to client/src/Pages/v1/Infrastructure/Create/Components/MonitorStatusHeader.jsx index 2ed48bf64..4313c34c0 100644 --- a/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx +++ b/client/src/Pages/v1/Infrastructure/Create/Components/MonitorStatusHeader.jsx @@ -1,8 +1,8 @@ import { Box, Stack, Tooltip, Typography } from "@mui/material"; -import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils"; +import { useMonitorUtils } from "../../../../../Hooks/v1/useMonitorUtils.js"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import PulseDot from "../../../../Components/Animated/PulseDot"; +import PulseDot from "../../../../../Components/Animated/PulseDot.jsx"; import PropTypes from "prop-types"; const MonitorStatusHeader = ({ monitor, infrastructureMonitor }) => { const theme = useTheme(); diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx b/client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx similarity index 100% rename from client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx rename to client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx b/client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx similarity index 98% rename from client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx rename to client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx index 9b3ee0d04..0fd5c0081 100644 --- a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx +++ b/client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx @@ -1,4 +1,4 @@ -import { useCreateMonitor, useUpdateMonitor } from "../../../../Hooks/monitorHooks"; +import { useCreateMonitor, useUpdateMonitor } from "../../../../../Hooks/v1/monitorHooks.js"; const useInfrastructureSubmit = () => { const [createMonitor, isCreating] = useCreateMonitor(); const [updateMonitor, isUpdating] = useUpdateMonitor(); diff --git a/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx b/client/src/Pages/v1/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx similarity index 85% rename from client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx rename to client/src/Pages/v1/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx index 080f441c4..73097d0d8 100644 --- a/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx +++ b/client/src/Pages/v1/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { infrastructureMonitorValidation } from "../../../../Validation/validation"; -import { createToast } from "../../../../Utils/toastUtils"; +import { infrastructureMonitorValidation } from "../../../../../Validation/validation.js"; +import { createToast } from "../../../../../Utils/toastUtils.jsx"; const useValidateInfrastructureForm = () => { const [errors, setErrors] = useState({}); diff --git a/client/src/Pages/Infrastructure/Create/index.jsx b/client/src/Pages/v1/Infrastructure/Create/index.jsx similarity index 92% rename from client/src/Pages/Infrastructure/Create/index.jsx rename to client/src/Pages/v1/Infrastructure/Create/index.jsx index a9d16c926..06ecdff6c 100644 --- a/client/src/Pages/Infrastructure/Create/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Create/index.jsx @@ -1,18 +1,18 @@ //Components -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import ConfigBox from "../../../Components/ConfigBox"; -import FieldWrapper from "../../../Components/Inputs/FieldWrapper"; -import Link from "../../../Components/Link"; -import Select from "../../../Components/Inputs/Select"; -import TextInput from "../../../Components/Inputs/TextInput"; +import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; +import ConfigBox from "../../../../Components/ConfigBox/index.jsx"; +import FieldWrapper from "../../../../Components/Inputs/FieldWrapper/index.jsx"; +import Link from "../../../../Components/Link/index.jsx"; +import Select from "../../../../Components/Inputs/Select/index.jsx"; +import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material"; -import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; -import MonitorStatusHeader from "./Components/MonitorStatusHeader"; -import MonitorActionButtons from "./Components/MonitorActionButtons"; -import CustomAlertsSection from "./Components/CustomAlertsSection"; +import { HttpAdornment } from "../../../../Components/Inputs/TextInput/Adornments/index.jsx"; +import MonitorStatusHeader from "./Components/MonitorStatusHeader.jsx"; +import MonitorActionButtons from "./Components/MonitorActionButtons.jsx"; +import CustomAlertsSection from "./Components/CustomAlertsSection.jsx"; // Utils -import NotificationsConfig from "../../../Components/NotificationConfig"; -import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications"; +import NotificationsConfig from "../../../../Components/NotificationConfig/index.jsx"; +import { useGetNotificationsByTeamId } from "../../../../Hooks/v1/useNotifications.js"; import { useParams } from "react-router-dom"; import { useState, useEffect } from "react"; import { useTheme } from "@emotion/react"; @@ -22,10 +22,10 @@ import { useFetchGlobalSettings, useFetchHardwareMonitorById, usePauseMonitor, -} from "../../../Hooks/monitorHooks"; -import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm"; -import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm"; -import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit"; +} from "../../../../Hooks/v1/monitorHooks.js"; +import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm.jsx"; +import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm.jsx"; +import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit.jsx"; const CreateInfrastructureMonitor = () => { const { monitorId } = useParams(); diff --git a/client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx similarity index 79% rename from client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx rename to client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx index 9544bcf2a..47f33a5e6 100644 --- a/client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx +++ b/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx @@ -1,10 +1,10 @@ // Components import { Typography } from "@mui/material"; -import BaseContainer from "../BaseContainer"; -import AreaChart from "../../../../../Components/Charts/AreaChart"; +import BaseContainer from "../BaseContainer/index.jsx"; +import AreaChart from "../../../../../../Components/Charts/AreaChart/index.jsx"; // Utils import { useTheme } from "@emotion/react"; -import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; const InfraAreaChart = ({ config }) => { const theme = useTheme(); const { getDimensions } = useHardwareUtils(); diff --git a/client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/index.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/index.jsx similarity index 93% rename from client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/index.jsx rename to client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/index.jsx index 0eb3aa36d..3dfb4edd8 100644 --- a/client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/index.jsx @@ -1,7 +1,7 @@ // Components import { Stack } from "@mui/material"; -import InfraAreaChart from "./InfraAreaChart"; -import SkeletonLayout from "./skeleton"; +import InfraAreaChart from "./InfraAreaChart.jsx"; +import SkeletonLayout from "./skeleton.jsx"; // Utils import { @@ -9,9 +9,9 @@ import { TzTick, InfrastructureTooltip, TemperatureTooltip, -} from "../../../../../Components/Charts/Utils/chartUtils"; +} from "../../../../../../Components/Charts/Utils/chartUtils.jsx"; import { useTheme } from "@emotion/react"; -import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; import { useTranslation } from "react-i18next"; const AreaChartBoxes = ({ shouldRender, monitor, dateRange }) => { const theme = useTheme(); diff --git a/client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx similarity index 100% rename from client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx rename to client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx diff --git a/client/src/Pages/Infrastructure/Details/Components/BaseContainer/index.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/BaseContainer/index.jsx similarity index 94% rename from client/src/Pages/Infrastructure/Details/Components/BaseContainer/index.jsx rename to client/src/Pages/v1/Infrastructure/Details/Components/BaseContainer/index.jsx index 246769751..dd73d1a65 100644 --- a/client/src/Pages/Infrastructure/Details/Components/BaseContainer/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Details/Components/BaseContainer/index.jsx @@ -11,7 +11,7 @@ import { Box } from "@mui/material"; // Utils import { useTheme } from "@emotion/react"; -import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; import PropTypes from "prop-types"; const BaseContainer = ({ children, sx = {} }) => { diff --git a/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx similarity index 93% rename from client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx rename to client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx index 70d36bd00..5c55119ad 100644 --- a/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx +++ b/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx @@ -1,6 +1,6 @@ // Components -import CustomGauge from "../../../../../Components/Charts/CustomGauge"; -import BaseContainer from "../BaseContainer"; +import CustomGauge from "../../../../../../Components/Charts/CustomGauge/index.jsx"; +import BaseContainer from "../BaseContainer/index.jsx"; import { Stack, Typography, Box } from "@mui/material"; // Utils import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx similarity index 93% rename from client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx rename to client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx index 3c1c68f8b..432881e7e 100644 --- a/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx @@ -1,11 +1,11 @@ // Components import { Stack } from "@mui/material"; -import Gauge from "./Gauge"; -import SkeletonLayout from "./skeleton"; +import Gauge from "./Gauge.jsx"; +import SkeletonLayout from "./skeleton.jsx"; import PropTypes from "prop-types"; // Utils -import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx similarity index 100% rename from client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx rename to client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx similarity index 93% rename from client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx rename to client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx index 9157a3432..5cfdba350 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx +++ b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx @@ -1,15 +1,15 @@ import PropTypes from "prop-types"; import { Stack, Typography } from "@mui/material"; -import InfraAreaChart from "../../../../../Pages/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart"; +import InfraAreaChart from "../AreaChartBoxes/InfraAreaChart.jsx"; import { TzTick, InfrastructureTooltip, NetworkTick, -} from "../../../../../Components/Charts/Utils/chartUtils"; +} from "../../../../../../Components/Charts/Utils/chartUtils.jsx"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; const NetworkCharts = ({ ethernetData, dateRange }) => { const theme = useTheme(); diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx similarity index 90% rename from client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx rename to client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx index 98ba90dae..52ccf0805 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx +++ b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx @@ -1,9 +1,9 @@ import PropTypes from "prop-types"; -import StatusBoxes from "../../../../../Components/StatusBoxes"; -import StatBox from "../../../../../Components/StatBox"; +import StatusBoxes from "../../../../../../Components/StatusBoxes/index.jsx"; +import StatBox from "../../../../../../Components/StatBox/index.jsx"; import { Typography } from "@mui/material"; import { useTranslation } from "react-i18next"; -import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; function formatNumber(num) { return num != null ? num.toLocaleString() : "0"; diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/index.jsx similarity index 90% rename from client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx rename to client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/index.jsx index 41d2b0a87..e0d6986e1 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/index.jsx @@ -3,10 +3,10 @@ import { useState, useEffect } from "react"; import { Box } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useTheme } from "@emotion/react"; -import Select from "../../../../../Components/Inputs/Select"; -import NetworkStatBoxes from "./NetworkStatBoxes"; -import NetworkCharts from "./NetworkCharts"; -import MonitorTimeFrameHeader from "../../../../../Components/MonitorTimeFrameHeader"; +import Select from "../../../../../../Components/Inputs/Select/index.jsx"; +import NetworkStatBoxes from "./NetworkStatBoxes.jsx"; +import NetworkCharts from "./NetworkCharts.jsx"; +import MonitorTimeFrameHeader from "../../../../../../Components/MonitorTimeFrameHeader/index.jsx"; const getAvailableInterfaces = (net) => { return (net || []).map((iface) => iface.name).filter(Boolean); diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/skeleton.jsx similarity index 100% rename from client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx rename to client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/skeleton.jsx diff --git a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/StatusBoxes/index.jsx similarity index 90% rename from client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx rename to client/src/Pages/v1/Infrastructure/Details/Components/StatusBoxes/index.jsx index 6c11e7c82..47862449e 100644 --- a/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Details/Components/StatusBoxes/index.jsx @@ -1,11 +1,11 @@ // Components import { Stack, Typography } from "@mui/material"; -import StatusBoxes from "../../../../../Components/StatusBoxes"; -import StatBox from "../../../../../Components/StatBox"; +import StatusBoxes from "../../../../../../Components/StatusBoxes/index.jsx"; +import StatBox from "../../../../../../Components/StatBox/index.jsx"; //Utils -import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils"; -import { useHardwareUtils } from "../../Hooks/useHardwareUtils"; +import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js"; +import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; import { useTranslation } from "react-i18next"; const InfraStatBoxes = ({ shouldRender, monitor }) => { diff --git a/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx b/client/src/Pages/v1/Infrastructure/Details/Hooks/useHardwareUtils.jsx similarity index 100% rename from client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx rename to client/src/Pages/v1/Infrastructure/Details/Hooks/useHardwareUtils.jsx diff --git a/client/src/Pages/Infrastructure/Details/index.jsx b/client/src/Pages/v1/Infrastructure/Details/index.jsx similarity index 79% rename from client/src/Pages/Infrastructure/Details/index.jsx rename to client/src/Pages/v1/Infrastructure/Details/index.jsx index a16f3906b..04691d985 100644 --- a/client/src/Pages/Infrastructure/Details/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Details/index.jsx @@ -1,20 +1,20 @@ // Components import { Stack, Typography, Tab } from "@mui/material"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import MonitorDetailsControlHeader from "../../../Components/MonitorDetailsControlHeader"; -import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader"; -import StatusBoxes from "./Components/StatusBoxes"; -import GaugeBoxes from "./Components/GaugeBoxes"; -import AreaChartBoxes from "./Components/AreaChartBoxes"; -import GenericFallback from "../../../Components/GenericFallback"; -import NetworkStats from "./Components/NetworkStats"; -import CustomTabList from "../../../Components/Tab"; +import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; +import MonitorDetailsControlHeader from "../../../../Components/MonitorDetailsControlHeader/index.jsx"; +import MonitorTimeFrameHeader from "../../../../Components/MonitorTimeFrameHeader/index.jsx"; +import StatusBoxes from "./Components/StatusBoxes/index.jsx"; +import GaugeBoxes from "./Components/GaugeBoxes/index.jsx"; +import AreaChartBoxes from "./Components/AreaChartBoxes/index.jsx"; +import GenericFallback from "../../../../Components/GenericFallback/index.jsx"; +import NetworkStats from "./Components/NetworkStats/index.jsx"; +import CustomTabList from "../../../../Components/Tab/index.jsx"; import TabContext from "@mui/lab/TabContext"; // Utils import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "../../../Hooks/useIsAdmin"; -import { useFetchHardwareMonitorById } from "../../../Hooks/monitorHooks"; +import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; +import { useFetchHardwareMonitorById } from "../../../../Hooks/v1/monitorHooks.js"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; diff --git a/client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx b/client/src/Pages/v1/Infrastructure/Monitors/Components/Filters/index.jsx similarity index 96% rename from client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx rename to client/src/Pages/v1/Infrastructure/Monitors/Components/Filters/index.jsx index 3c2511c2f..bef632e88 100644 --- a/client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Monitors/Components/Filters/index.jsx @@ -1,6 +1,6 @@ import { useTheme } from "@emotion/react"; import PropTypes from "prop-types"; -import FilterHeader from "../../../../../Components/FilterHeader"; +import FilterHeader from "../../../../../../Components/FilterHeader/index.jsx"; import { useMemo } from "react"; import { Box, Button } from "@mui/material"; import ClearIcon from "@mui/icons-material/Clear"; diff --git a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx b/client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTable/index.jsx similarity index 86% rename from client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx rename to client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTable/index.jsx index 7ee603ef7..801a9bc6a 100644 --- a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTable/index.jsx @@ -1,18 +1,18 @@ // Components import { Box } from "@mui/material"; -import DataTable from "../../../../../Components/Table"; -import Host from "../../../../../Components/Host"; -import { StatusLabel } from "../../../../../Components/Label"; +import DataTable from "../../../../../../Components/Table/index.jsx"; +import Host from "../../../../../../Components/Host/index.jsx"; +import { StatusLabel } from "../../../../../../Components/Label/index.jsx"; import { Stack } from "@mui/material"; -import { InfrastructureMenu } from "../MonitorsTableMenu"; -import LoadingSpinner from "../../../../Uptime/Monitors/Components/LoadingSpinner"; +import { InfrastructureMenu } from "../MonitorsTableMenu/index.jsx"; +import LoadingSpinner from "../../../../Uptime/Monitors/Components/LoadingSpinner/index.jsx"; // Assets -import CPUChipIcon from "../../../../../assets/icons/cpu-chip.svg?react"; -import CustomGauge from "../../../../../Components/Charts/CustomGauge"; +import CPUChipIcon from "../../../../../../assets/icons/cpu-chip.svg?react"; +import CustomGauge from "../../../../../../Components/Charts/CustomGauge/index.jsx"; // Utils import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils"; +import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js"; import { useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx b/client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx similarity index 93% rename from client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx rename to client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx index 147f1c297..c2270c691 100644 --- a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx @@ -3,13 +3,13 @@ import { useRef, useState } from "react"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; -import { createToast } from "../../../../../Utils/toastUtils"; +import { createToast } from "../../../../../../Utils/toastUtils.jsx"; import { IconButton, Menu, MenuItem } from "@mui/material"; -import Settings from "../../../../../assets/icons/settings-bold.svg?react"; +import Settings from "../../../../../../assets/icons/settings-bold.svg?react"; import PropTypes from "prop-types"; -import Dialog from "../../../../../Components/Dialog"; -import { networkService } from "../../../../../Utils/NetworkService.js"; -import { usePauseMonitor } from "../../../../../Hooks/monitorHooks"; +import Dialog from "../../../../../../Components/Dialog/index.jsx"; +import { networkService } from "../../../../../../Utils/NetworkService.js"; +import { usePauseMonitor } from "../../../../../../Hooks/v1/monitorHooks.js"; import { useTranslation } from "react-i18next"; /** diff --git a/client/src/Pages/Infrastructure/Monitors/index.jsx b/client/src/Pages/v1/Infrastructure/Monitors/index.jsx similarity index 80% rename from client/src/Pages/Infrastructure/Monitors/index.jsx rename to client/src/Pages/v1/Infrastructure/Monitors/index.jsx index bfd2270fe..7fbfa1167 100644 --- a/client/src/Pages/Infrastructure/Monitors/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Monitors/index.jsx @@ -1,21 +1,21 @@ // Components import { Stack } from "@mui/material"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import MonitorCountHeader from "../../../Components/MonitorCountHeader"; -import MonitorCreateHeader from "../../../Components/MonitorCreateHeader"; -import MonitorsTable from "./Components/MonitorsTable"; -import Pagination from "../../..//Components/Table/TablePagination"; -import PageStateWrapper from "../../../Components/PageStateWrapper"; -import Filter from "./Components/Filters"; -import SearchComponent from "../../Uptime/Monitors/Components/SearchComponent"; +import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; +import MonitorCountHeader from "../../../../Components/MonitorCountHeader/index.jsx"; +import MonitorCreateHeader from "../../../../Components/MonitorCreateHeader/index.jsx"; +import MonitorsTable from "./Components/MonitorsTable/index.jsx"; +import Pagination from "../../../../Components/Table/TablePagination/index.jsx"; +import PageStateWrapper from "../../../../Components/PageStateWrapper/index.jsx"; +import Filter from "./Components/Filters/index.jsx"; +import SearchComponent from "../../Uptime/Monitors/Components/SearchComponent/index.jsx"; // Utils import { useTheme } from "@emotion/react"; import { useEffect, useState } from "react"; -import { useIsAdmin } from "../../../Hooks/useIsAdmin"; +import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; import { useTranslation } from "react-i18next"; -import { useFetchMonitorsByTeamId } from "../../../Hooks/monitorHooks"; +import { useFetchMonitorsByTeamId } from "../../../../Hooks/v1/monitorHooks.js"; import { useDispatch, useSelector } from "react-redux"; -import { setRowsPerPage } from "../../../Features/UI/uiSlice"; +import { setRowsPerPage } from "../../../../Features/UI/uiSlice.js"; // Constants const TYPES = ["hardware"]; const BREADCRUMBS = [{ name: `infrastructure`, path: "/infrastructure" }]; diff --git a/client/src/Pages/Integrations/index.css b/client/src/Pages/v1/Integrations/index.css similarity index 100% rename from client/src/Pages/Integrations/index.css rename to client/src/Pages/v1/Integrations/index.css diff --git a/client/src/Pages/Integrations/index.jsx b/client/src/Pages/v1/Integrations/index.jsx similarity index 94% rename from client/src/Pages/Integrations/index.jsx rename to client/src/Pages/v1/Integrations/index.jsx index fe0faa5cf..dde6b52ee 100644 --- a/client/src/Pages/Integrations/index.jsx +++ b/client/src/Pages/v1/Integrations/index.jsx @@ -1,9 +1,9 @@ import PropTypes from "prop-types"; import { Stack, Typography, Grid, Button } from "@mui/material"; import { useTheme } from "@emotion/react"; -import Discord from "../../assets/icons/discord-icon.svg?react"; -import Slack from "../../assets/icons/slack-icon.svg?react"; -import Zapier from "../../assets/icons/zapier-icon.svg?react"; +import Discord from "../../../assets/icons/discord-icon.svg?react"; +import Slack from "../../../assets/icons/slack-icon.svg?react"; +import Zapier from "../../../assets/icons/zapier-icon.svg?react"; import { useTranslation } from "react-i18next"; import "./index.css"; diff --git a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx b/client/src/Pages/v1/Logs/Diagnostics/components/gauges/index.jsx similarity index 96% rename from client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx rename to client/src/Pages/v1/Logs/Diagnostics/components/gauges/index.jsx index 5bb227b7c..082be2587 100644 --- a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx +++ b/client/src/Pages/v1/Logs/Diagnostics/components/gauges/index.jsx @@ -1,11 +1,11 @@ import Stack from "@mui/material/Stack"; -import CustomGauge from "../../../../../Components/Charts/CustomGauge"; +import CustomGauge from "../../../../../../Components/Charts/CustomGauge/index.jsx"; import Typography from "@mui/material/Typography"; // Utils import { useTheme } from "@emotion/react"; import PropTypes from "prop-types"; -import { getPercentage, formatBytes } from "../../utils/utils"; +import { getPercentage, formatBytes } from "../../utils/utils.js"; import { useTranslation } from "react-i18next"; import { Box } from "@mui/material"; diff --git a/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx b/client/src/Pages/v1/Logs/Diagnostics/components/stats/index.jsx similarity index 94% rename from client/src/Pages/Logs/Diagnostics/components/stats/index.jsx rename to client/src/Pages/v1/Logs/Diagnostics/components/stats/index.jsx index 103d1c698..b99f34506 100644 --- a/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx +++ b/client/src/Pages/v1/Logs/Diagnostics/components/stats/index.jsx @@ -7,8 +7,8 @@ import CircularProgress from "@mui/material/CircularProgress"; import { useTheme } from "@emotion/react"; import PropTypes from "prop-types"; -import { getHumanReadableDuration } from "../../../../../Utils/timeUtils"; -import { formatBytes } from "../../utils/utils"; +import { getHumanReadableDuration } from "../../../../../../Utils/timeUtils.js"; +import { formatBytes } from "../../utils/utils.js"; import { useTranslation } from "react-i18next"; const StatsCard = ({ title, value, unit = "", isLoading }) => { diff --git a/client/src/Pages/Logs/Diagnostics/index.jsx b/client/src/Pages/v1/Logs/Diagnostics/index.jsx similarity index 82% rename from client/src/Pages/Logs/Diagnostics/index.jsx rename to client/src/Pages/v1/Logs/Diagnostics/index.jsx index 72ee47f48..d7a1a8a7c 100644 --- a/client/src/Pages/Logs/Diagnostics/index.jsx +++ b/client/src/Pages/v1/Logs/Diagnostics/index.jsx @@ -1,14 +1,14 @@ import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; -import Gauges from "./components/gauges"; +import Gauges from "./components/gauges/index.jsx"; import Button from "@mui/material/Button"; -import StatBox from "../../../Components/StatBox"; -import StatusBoxes from "../../../Components/StatusBoxes"; +import StatBox from "../../../../Components/StatBox/index.jsx"; +import StatusBoxes from "../../../../Components/StatusBoxes/index.jsx"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import { useFetchDiagnostics } from "../../../Hooks/logHooks"; -import { getHumanReadableDuration } from "../../../Utils/timeUtils"; -import { formatBytes, getPercentage } from "./utils/utils"; +import { useFetchDiagnostics } from "../../../../Hooks/v1/logHooks.js"; +import { getHumanReadableDuration } from "../../../../Utils/timeUtils.js"; +import { formatBytes, getPercentage } from "./utils/utils.js"; const Diagnostics = () => { // Local state diff --git a/client/src/Pages/Logs/Diagnostics/utils/utils.js b/client/src/Pages/v1/Logs/Diagnostics/utils/utils.js similarity index 100% rename from client/src/Pages/Logs/Diagnostics/utils/utils.js rename to client/src/Pages/v1/Logs/Diagnostics/utils/utils.js diff --git a/client/src/Pages/Logs/Logs/index.jsx b/client/src/Pages/v1/Logs/Logs/index.jsx similarity index 94% rename from client/src/Pages/Logs/Logs/index.jsx rename to client/src/Pages/v1/Logs/Logs/index.jsx index b44b3fd1a..5c7ad9fa9 100644 --- a/client/src/Pages/Logs/Logs/index.jsx +++ b/client/src/Pages/v1/Logs/Logs/index.jsx @@ -1,10 +1,10 @@ import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; -import Select from "../../../Components/Inputs/Select"; +import Select from "../../../../Components/Inputs/Select/index.jsx"; import Typography from "@mui/material/Typography"; import Divider from "@mui/material/Divider"; -import { useFetchLogs } from "../../../Hooks/logHooks"; +import { useFetchLogs } from "../../../../Hooks/v1/logHooks.js"; import { useTheme } from "@emotion/react"; import { useState } from "react"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/Logs/Queue/components/FailedJobTable/index.jsx b/client/src/Pages/v1/Logs/Queue/components/FailedJobTable/index.jsx similarity index 92% rename from client/src/Pages/Logs/Queue/components/FailedJobTable/index.jsx rename to client/src/Pages/v1/Logs/Queue/components/FailedJobTable/index.jsx index 94fc2f5c3..46be9520c 100644 --- a/client/src/Pages/Logs/Queue/components/FailedJobTable/index.jsx +++ b/client/src/Pages/v1/Logs/Queue/components/FailedJobTable/index.jsx @@ -1,10 +1,10 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import DataTable from "../../../../../Components/Table"; +import DataTable from "../../../../../../Components/Table/index.jsx"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; -import { TypeToPathMap } from "../../../../../Utils/monitorUtils"; +import { TypeToPathMap } from "../../../../../../Utils/monitorUtils.js"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/Logs/Queue/components/JobTable/index.jsx b/client/src/Pages/v1/Logs/Queue/components/JobTable/index.jsx similarity index 92% rename from client/src/Pages/Logs/Queue/components/JobTable/index.jsx rename to client/src/Pages/v1/Logs/Queue/components/JobTable/index.jsx index fab85ec5d..4ac4069bd 100644 --- a/client/src/Pages/Logs/Queue/components/JobTable/index.jsx +++ b/client/src/Pages/v1/Logs/Queue/components/JobTable/index.jsx @@ -1,13 +1,13 @@ import Stack from "@mui/material/Stack"; -import DataTable from "../../../../../Components/Table"; +import DataTable from "../../../../../../Components/Table/index.jsx"; import Typography from "@mui/material/Typography"; // Utils import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; -import { TypeToPathMap } from "../../../../../Utils/monitorUtils"; +import { TypeToPathMap } from "../../../../../../Utils/monitorUtils.js"; import { useTranslation } from "react-i18next"; -import { createHeaderFactory } from "../../../../../Components/Table/TableUtils"; +import { createHeaderFactory } from "../../../../../../Components/Table/TableUtils.js"; const JobTable = ({ jobs = [] }) => { const theme = useTheme(); diff --git a/client/src/Pages/Logs/Queue/components/Metrics/index.jsx b/client/src/Pages/v1/Logs/Queue/components/Metrics/index.jsx similarity index 84% rename from client/src/Pages/Logs/Queue/components/Metrics/index.jsx rename to client/src/Pages/v1/Logs/Queue/components/Metrics/index.jsx index b0112fa99..dd9fdcc6a 100644 --- a/client/src/Pages/Logs/Queue/components/Metrics/index.jsx +++ b/client/src/Pages/v1/Logs/Queue/components/Metrics/index.jsx @@ -1,6 +1,6 @@ import Stack from "@mui/material/Stack"; -import StatBox from "../../../../../Components/StatBox"; -import StatusBoxes from "../../../../../Components/StatusBoxes"; +import StatBox from "../../../../../../Components/StatBox/index.jsx"; +import StatusBoxes from "../../../../../../Components/StatusBoxes/index.jsx"; import { useTranslation } from "react-i18next"; import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/Logs/Queue/components/MetricsTable/index.jsx b/client/src/Pages/v1/Logs/Queue/components/MetricsTable/index.jsx similarity index 94% rename from client/src/Pages/Logs/Queue/components/MetricsTable/index.jsx rename to client/src/Pages/v1/Logs/Queue/components/MetricsTable/index.jsx index 204b90035..58f13fd5d 100644 --- a/client/src/Pages/Logs/Queue/components/MetricsTable/index.jsx +++ b/client/src/Pages/v1/Logs/Queue/components/MetricsTable/index.jsx @@ -1,6 +1,6 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import DataTable from "../../../../../Components/Table"; +import DataTable from "../../../../../../Components/Table/index.jsx"; // Utils import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/Logs/Queue/index.jsx b/client/src/Pages/v1/Logs/Queue/index.jsx similarity index 86% rename from client/src/Pages/Logs/Queue/index.jsx rename to client/src/Pages/v1/Logs/Queue/index.jsx index fde9073fc..10eca4d3b 100644 --- a/client/src/Pages/Logs/Queue/index.jsx +++ b/client/src/Pages/v1/Logs/Queue/index.jsx @@ -1,8 +1,8 @@ // Components import Stack from "@mui/material/Stack"; -import JobTable from "./components/JobTable"; -import Metrics from "./components/Metrics"; -import FailedJobTable from "./components/FailedJobTable"; +import JobTable from "./components/JobTable/index.jsx"; +import Metrics from "./components/Metrics/index.jsx"; +import FailedJobTable from "./components/FailedJobTable/index.jsx"; import ButtonGroup from "@mui/material/ButtonGroup"; import Button from "@mui/material/Button"; import Typography from "@mui/material/Typography"; @@ -10,7 +10,7 @@ import Divider from "@mui/material/Divider"; // Utils import { useState } from "react"; -import { useFetchQueueData, useFlushQueue } from "../../../Hooks/logHooks"; +import { useFetchQueueData, useFlushQueue } from "../../../../Hooks/v1/logHooks.js"; import { useTranslation } from "react-i18next"; import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/Logs/index.jsx b/client/src/Pages/v1/Logs/index.jsx similarity index 84% rename from client/src/Pages/Logs/index.jsx rename to client/src/Pages/v1/Logs/index.jsx index 79c89f282..c6dfe4d99 100644 --- a/client/src/Pages/Logs/index.jsx +++ b/client/src/Pages/v1/Logs/index.jsx @@ -1,10 +1,10 @@ import Stack from "@mui/material/Stack"; -import Breadcrumbs from "../../Components/Breadcrumbs"; +import Breadcrumbs from "../../../Components/Breadcrumbs/index.jsx"; import Tabs from "@mui/material/Tabs"; import Tab from "@mui/material/Tab"; -import Queue from "./Queue"; -import LogsComponent from "./Logs"; -import Diagnostics from "./Diagnostics"; +import Queue from "./Queue/index.jsx"; +import LogsComponent from "./Logs/index.jsx"; +import Diagnostics from "./Diagnostics/index.jsx"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx b/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx similarity index 92% rename from client/src/Pages/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx rename to client/src/Pages/v1/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx index 1627ef3d5..b43bcbfc7 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx +++ b/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx @@ -1,4 +1,4 @@ -import Select from "../../../../../Components/Inputs/Select"; +import Select from "../../../../../../Components/Inputs/Select/index.jsx"; import PropTypes from "prop-types"; const ConfigSelect = ({ configSelection, valueSelect, onChange, ...props }) => { const getValueById = (config, id) => { diff --git a/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx b/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx similarity index 100% rename from client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx rename to client/src/Pages/v1/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx diff --git a/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorsConfig/index.jsx b/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/MonitorsConfig/index.jsx similarity index 93% rename from client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorsConfig/index.jsx rename to client/src/Pages/v1/Maintenance/CreateMaintenance/Components/MonitorsConfig/index.jsx index e792734ff..91e6a1531 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorsConfig/index.jsx +++ b/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/MonitorsConfig/index.jsx @@ -1,7 +1,7 @@ import PropTypes from "prop-types"; -import Search from "../../../../../Components/Inputs/Search"; +import Search from "../../../../../../Components/Inputs/Search/index.jsx"; import { useState } from "react"; -import ConfigRow from "../../../../../Components/ConfigRow"; +import ConfigRow from "../../../../../../Components/ConfigRow/index.jsx"; /** * MonitorsConfig is a component that allows users to select and apply a maintenance window to a list of monitors. diff --git a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx b/client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx similarity index 93% rename from client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx rename to client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx index 84333a14f..a75cbe206 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx +++ b/client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx @@ -1,4 +1,4 @@ -import { networkService } from "../../../../main"; +import { networkService } from "../../../../../main.jsx"; import dayjs from "dayjs"; import { MS_PER_SECOND, @@ -6,7 +6,7 @@ import { MS_PER_HOUR, MS_PER_DAY, MS_PER_WEEK, -} from "../../../../Utils/timeUtils"; +} from "../../../../../Utils/timeUtils.js"; const useMaintenanceActions = () => { const MS_LOOKUP = { diff --git a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx b/client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx similarity index 95% rename from client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx rename to client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx index 8ba9b5e80..d21906e41 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx +++ b/client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx @@ -1,4 +1,4 @@ -import { networkService } from "../../../../main"; +import { networkService } from "../../../../../main.jsx"; import dayjs from "dayjs"; import { MS_PER_SECOND, @@ -6,7 +6,7 @@ import { MS_PER_HOUR, MS_PER_DAY, MS_PER_WEEK, -} from "../../../../Utils/timeUtils"; +} from "../../../../../Utils/timeUtils.js"; const useMaintenanceData = () => { const REVERSE_REPEAT_LOOKUP = { 0: "none", diff --git a/client/src/Pages/Maintenance/CreateMaintenance/index.css b/client/src/Pages/v1/Maintenance/CreateMaintenance/index.css similarity index 100% rename from client/src/Pages/Maintenance/CreateMaintenance/index.css rename to client/src/Pages/v1/Maintenance/CreateMaintenance/index.css diff --git a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx b/client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx similarity index 93% rename from client/src/Pages/Maintenance/CreateMaintenance/index.jsx rename to client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx index 9dbf9d4aa..da3e72d8a 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx +++ b/client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx @@ -2,27 +2,27 @@ import { Box, Button, Stack, Typography } from "@mui/material"; import { useSelector } from "react-redux"; import { useTheme } from "@emotion/react"; import { useEffect, useState } from "react"; -import ConfigBox from "../../../Components/ConfigBox"; +import ConfigBox from "../../../../Components/ConfigBox/index.jsx"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { LocalizationProvider } from "@mui/x-date-pickers"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker"; -import { maintenanceWindowValidation } from "../../../Validation/validation"; -import { createToast } from "../../../Utils/toastUtils"; -import MonitorList from "./Components/MonitorList"; -import ConfigSelect from "./Components/ConfigSelect"; -import useMaintenanceData from "./hooks/useMaintenanceData"; -import useMaintenanceActions from "./hooks/useMaintenanceActions"; +import { maintenanceWindowValidation } from "../../../../Validation/validation.js"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; +import MonitorList from "./Components/MonitorList/index.jsx"; +import ConfigSelect from "./Components/ConfigSelect/index.jsx"; +import useMaintenanceData from "./hooks/useMaintenanceData.jsx"; +import useMaintenanceActions from "./hooks/useMaintenanceActions.jsx"; -import TextInput from "../../../Components/Inputs/TextInput"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import CalendarIcon from "../../../assets/icons/calendar.svg?react"; +import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; +import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; +import CalendarIcon from "../../../../assets/icons/calendar.svg?react"; import "./index.css"; -import Search from "../../../Components/Inputs/Search"; -import { logger } from "../../../Utils/Logger"; +import Search from "../../../../Components/Inputs/Search/index.jsx"; +import { logger } from "../../../../Utils/Logger.js"; import { useNavigate, useParams } from "react-router-dom"; -import { buildErrors, hasValidationErrors } from "../../../Validation/error"; +import { buildErrors, hasValidationErrors } from "../../../../Validation/error.js"; import { useTranslation } from "react-i18next"; import dayjs from "dayjs"; diff --git a/client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx b/client/src/Pages/v1/Maintenance/MaintenanceTable/ActionsMenu/index.jsx similarity index 91% rename from client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx rename to client/src/Pages/v1/Maintenance/MaintenanceTable/ActionsMenu/index.jsx index aa1c48433..cc51697f7 100644 --- a/client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx +++ b/client/src/Pages/v1/Maintenance/MaintenanceTable/ActionsMenu/index.jsx @@ -3,14 +3,14 @@ import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; import { useSelector } from "react-redux"; import { IconButton, Menu, MenuItem } from "@mui/material"; -import { logger } from "../../../../Utils/Logger"; -import Settings from "../../../../assets/icons/settings-bold.svg?react"; +import { logger } from "../../../../../Utils/Logger.js"; +import Settings from "../../../../../assets/icons/settings-bold.svg?react"; import PropTypes from "prop-types"; -import { networkService } from "../../../../main"; -import { createToast } from "../../../../Utils/toastUtils"; +import { networkService } from "../../../../../main.jsx"; +import { createToast } from "../../../../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; -import Dialog from "../../../../Components/Dialog"; +import Dialog from "../../../../../Components/Dialog/index.jsx"; const ActionsMenu = ({ /* isAdmin, */ maintenanceWindow, updateCallback }) => { maintenanceWindow; diff --git a/client/src/Pages/Maintenance/MaintenanceTable/index.jsx b/client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx similarity index 92% rename from client/src/Pages/Maintenance/MaintenanceTable/index.jsx rename to client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx index bb5842e9f..fb18cd380 100644 --- a/client/src/Pages/Maintenance/MaintenanceTable/index.jsx +++ b/client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx @@ -1,15 +1,15 @@ import PropTypes from "prop-types"; import { Box } from "@mui/material"; -import DataTable from "../../../Components/Table"; -import Pagination from "../../../Components/Table/TablePagination"; +import DataTable from "../../../../Components/Table/index.jsx"; +import Pagination from "../../../../Components/Table/TablePagination/index.jsx"; import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; -import ActionsMenu from "./ActionsMenu"; +import ActionsMenu from "./ActionsMenu/index.jsx"; import { memo } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { formatDurationRounded } from "../../../Utils/timeUtils"; -import { StatusLabel } from "../../../Components/Label"; -import { setRowsPerPage } from "../../../Features/UI/uiSlice"; +import { formatDurationRounded } from "../../../../Utils/timeUtils.js"; +import { StatusLabel } from "../../../../Components/Label/index.jsx"; +import { setRowsPerPage } from "../../../../Features/UI/uiSlice.js"; import { useTranslation } from "react-i18next"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; diff --git a/client/src/Pages/Maintenance/index.css b/client/src/Pages/v1/Maintenance/index.css similarity index 100% rename from client/src/Pages/Maintenance/index.css rename to client/src/Pages/v1/Maintenance/index.css diff --git a/client/src/Pages/Maintenance/index.jsx b/client/src/Pages/v1/Maintenance/index.jsx similarity index 91% rename from client/src/Pages/Maintenance/index.jsx rename to client/src/Pages/v1/Maintenance/index.jsx index ee142f524..a993c153f 100644 --- a/client/src/Pages/Maintenance/index.jsx +++ b/client/src/Pages/v1/Maintenance/index.jsx @@ -2,13 +2,13 @@ import { Stack, Button } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useState, useEffect } from "react"; import "./index.css"; -import MaintenanceTable from "./MaintenanceTable"; +import MaintenanceTable from "./MaintenanceTable/index.jsx"; import { useSelector } from "react-redux"; -import { networkService } from "../../main"; -import Breadcrumbs from "../../Components/Breadcrumbs"; +import { networkService } from "../../../main.jsx"; +import Breadcrumbs from "../../../Components/Breadcrumbs/index.jsx"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import PageStateWrapper from "../../Components/PageStateWrapper"; +import PageStateWrapper from "../../../Components/PageStateWrapper/index.jsx"; const Maintenance = () => { const theme = useTheme(); diff --git a/client/src/Pages/NotFound/index.jsx b/client/src/Pages/v1/NotFound/index.jsx similarity index 96% rename from client/src/Pages/NotFound/index.jsx rename to client/src/Pages/v1/NotFound/index.jsx index fdae92cb5..48825c9f6 100644 --- a/client/src/Pages/NotFound/index.jsx +++ b/client/src/Pages/v1/NotFound/index.jsx @@ -1,5 +1,5 @@ import PropTypes from "prop-types"; -import NotFoundSvg from "../../../src/assets/Images/sushi_404.svg"; +import NotFoundSvg from "../../../assets/Images/sushi_404.svg"; import { Button, Stack, Typography } from "@mui/material"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/Notifications/components/ActionMenu.jsx b/client/src/Pages/v1/Notifications/components/ActionMenu.jsx similarity index 100% rename from client/src/Pages/Notifications/components/ActionMenu.jsx rename to client/src/Pages/v1/Notifications/components/ActionMenu.jsx diff --git a/client/src/Pages/Notifications/create/index.jsx b/client/src/Pages/v1/Notifications/create/index.jsx similarity index 92% rename from client/src/Pages/Notifications/create/index.jsx rename to client/src/Pages/v1/Notifications/create/index.jsx index 6d8afe665..0d541736d 100644 --- a/client/src/Pages/Notifications/create/index.jsx +++ b/client/src/Pages/v1/Notifications/create/index.jsx @@ -1,13 +1,13 @@ // Components import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; +import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; import Button from "@mui/material/Button"; -import ConfigBox from "../../../Components/ConfigBox"; +import ConfigBox from "../../../../Components/ConfigBox/index.jsx"; import Box from "@mui/material/Box"; -import Select from "../../../Components/Inputs/Select"; -import TextInput from "../../../Components/Inputs/TextInput"; -import Dialog from "../../../Components/Dialog"; +import Select from "../../../../Components/Inputs/Select/index.jsx"; +import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; +import Dialog from "../../../../Components/Dialog/index.jsx"; // Utils import { useState } from "react"; @@ -18,9 +18,9 @@ import { useEditNotification, useTestNotification, useDeleteNotification, -} from "../../../Hooks/useNotifications"; -import { notificationValidation } from "../../../Validation/validation"; -import { createToast } from "../../../Utils/toastUtils"; +} from "../../../../Hooks/v1/useNotifications.js"; +import { notificationValidation } from "../../../../Validation/validation.js"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; import { useParams, useNavigate } from "react-router-dom"; import { @@ -29,7 +29,7 @@ import { DESCRIPTION_MAP, LABEL_MAP, PLACEHOLDER_MAP, -} from "../utils"; +} from "../utils.js"; // Setup diff --git a/client/src/Pages/Notifications/index.jsx b/client/src/Pages/v1/Notifications/index.jsx similarity index 89% rename from client/src/Pages/Notifications/index.jsx rename to client/src/Pages/v1/Notifications/index.jsx index 910fbf1fc..5649224b6 100644 --- a/client/src/Pages/Notifications/index.jsx +++ b/client/src/Pages/v1/Notifications/index.jsx @@ -1,11 +1,11 @@ // Components import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import Breadcrumbs from "../../Components/Breadcrumbs"; +import Breadcrumbs from "../../../Components/Breadcrumbs/index.jsx"; import Button from "@mui/material/Button"; -import DataTable from "../../Components/Table"; -import ActionMenu from "./components/ActionMenu"; -import PageStateWrapper from "../../Components/PageStateWrapper"; +import DataTable from "../../../Components/Table/index.jsx"; +import ActionMenu from "./components/ActionMenu.jsx"; +import PageStateWrapper from "../../../Components/PageStateWrapper/index.jsx"; // Utils import { useState } from "react"; @@ -14,7 +14,7 @@ import { useNavigate } from "react-router-dom"; import { useGetNotificationsByTeamId, useDeleteNotification, -} from "../../Hooks/useNotifications"; +} from "../../../Hooks/v1/useNotifications.js"; import { useTranslation } from "react-i18next"; const Notifications = () => { diff --git a/client/src/Pages/Notifications/utils.js b/client/src/Pages/v1/Notifications/utils.js similarity index 100% rename from client/src/Pages/Notifications/utils.js rename to client/src/Pages/v1/Notifications/utils.js diff --git a/client/src/Pages/PageSpeed/Create/index.css b/client/src/Pages/v1/PageSpeed/Create/index.css similarity index 100% rename from client/src/Pages/PageSpeed/Create/index.css rename to client/src/Pages/v1/PageSpeed/Create/index.css diff --git a/client/src/Pages/PageSpeed/Create/index.jsx b/client/src/Pages/v1/PageSpeed/Create/index.jsx similarity index 91% rename from client/src/Pages/PageSpeed/Create/index.jsx rename to client/src/Pages/v1/PageSpeed/Create/index.jsx index 46bc7f977..7cc244848 100644 --- a/client/src/Pages/PageSpeed/Create/index.jsx +++ b/client/src/Pages/v1/PageSpeed/Create/index.jsx @@ -1,37 +1,37 @@ // Components import { Box, Stack, Tooltip, Typography, Button, ButtonGroup } from "@mui/material"; -import ConfigBox from "../../../Components/ConfigBox"; -import Select from "../../../Components/Inputs/Select"; -import TextInput from "../../../Components/Inputs/TextInput"; +import ConfigBox from "../../../../Components/ConfigBox/index.jsx"; +import Select from "../../../../Components/Inputs/Select/index.jsx"; +import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import PulseDot from "../../../Components/Animated/PulseDot"; +import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; +import PulseDot from "../../../../Components/Animated/PulseDot.jsx"; import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; -import SkeletonLayout from "./skeleton"; -import NotificationsConfig from "../../../Components/NotificationConfig"; -import Dialog from "../../../Components/Dialog"; -import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; -import Radio from "../../../Components/Inputs/Radio"; +import SkeletonLayout from "./skeleton.jsx"; +import NotificationsConfig from "../../../../Components/NotificationConfig/index.jsx"; +import Dialog from "../../../../Components/Dialog/index.jsx"; +import { HttpAdornment } from "../../../../Components/Inputs/TextInput/Adornments/index.jsx"; +import Radio from "../../../../Components/Inputs/Radio/index.jsx"; // Utils import { useState, useEffect } from "react"; import { useSelector } from "react-redux"; -import { monitorValidation } from "../../../Validation/validation"; -import { parseDomainName } from "../../../Utils/monitorUtils"; +import { monitorValidation } from "../../../../Validation/validation.js"; +import { parseDomainName } from "../../../../Utils/monitorUtils.js"; import { useTranslation } from "react-i18next"; -import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications"; +import { useGetNotificationsByTeamId } from "../../../../Hooks/v1/useNotifications.js"; import { useTheme } from "@emotion/react"; -import { createToast } from "../../../Utils/toastUtils"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; import { useParams } from "react-router"; -import { useMonitorUtils } from "../../../Hooks/useMonitorUtils"; +import { useMonitorUtils } from "../../../../Hooks/v1/useMonitorUtils.js"; import { useCreateMonitor, useFetchMonitorById, useDeleteMonitor, useUpdateMonitor, usePauseMonitor, -} from "../../../Hooks/monitorHooks"; +} from "../../../../Hooks/v1/monitorHooks.js"; const PageSpeedSetup = () => { const { monitorId } = useParams(); diff --git a/client/src/Pages/PageSpeed/Create/skeleton.jsx b/client/src/Pages/v1/PageSpeed/Create/skeleton.jsx similarity index 100% rename from client/src/Pages/PageSpeed/Create/skeleton.jsx rename to client/src/Pages/v1/PageSpeed/Create/skeleton.jsx diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChart.jsx similarity index 99% rename from client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx rename to client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChart.jsx index 40fec0ab6..5c79cc06c 100644 --- a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx +++ b/client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChart.jsx @@ -11,7 +11,7 @@ import { import { useTheme } from "@emotion/react"; import { useMemo, useState } from "react"; import { Box, Stack, Typography } from "@mui/material"; -import { formatDateWithTz } from "../../../../../Utils/timeUtils"; +import { formatDateWithTz } from "../../../../../../Utils/timeUtils.js"; import { useSelector } from "react-redux"; const config = { diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx similarity index 84% rename from client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx rename to client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx index 72811fa2a..cb6f18bd2 100644 --- a/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx +++ b/client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx @@ -1,7 +1,7 @@ import { Box, Typography, Divider } from "@mui/material"; -import Checkbox from "../../../../../Components/Inputs/Checkbox"; -import MetricsIcon from "../../../../../assets/icons/ruler-icon.svg?react"; -import LegendBox from "../../../../../Components/Charts/LegendBox"; +import Checkbox from "../../../../../../Components/Inputs/Checkbox/index.jsx"; +import MetricsIcon from "../../../../../../assets/icons/ruler-icon.svg?react"; +import LegendBox from "../../../../../../Components/Charts/LegendBox/index.jsx"; import { useTranslation } from "react-i18next"; import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/PieChart.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/Charts/PieChart.jsx similarity index 100% rename from client/src/Pages/PageSpeed/Details/Components/Charts/PieChart.jsx rename to client/src/Pages/v1/PageSpeed/Details/Components/Charts/PieChart.jsx diff --git a/client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/Charts/PieChartLegend.jsx similarity index 92% rename from client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx rename to client/src/Pages/v1/PageSpeed/Details/Components/Charts/PieChartLegend.jsx index 18ca0733c..fc4b190d8 100644 --- a/client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx +++ b/client/src/Pages/v1/PageSpeed/Details/Components/Charts/PieChartLegend.jsx @@ -1,7 +1,7 @@ import { Stack, Box, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; -import LegendBox from "../../../../../Components/Charts/LegendBox"; -import SpeedometerIcon from "../../../../../assets/icons/speedometer-icon.svg?react"; +import LegendBox from "../../../../../../Components/Charts/LegendBox/index.jsx"; +import SpeedometerIcon from "../../../../../../assets/icons/speedometer-icon.svg?react"; import PropTypes from "prop-types"; const PieChartLegend = ({ audits }) => { diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx similarity index 75% rename from client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx rename to client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx index 582648e23..bb45117fe 100644 --- a/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx +++ b/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx @@ -1,8 +1,8 @@ -import ChartBox from "../../../../../Components/Charts/ChartBox"; -import AreaChart from "../Charts/AreaChart"; -import AreaChartLegend from "../Charts/AreaChartLegend"; -import SkeletonLayout from "./skeleton"; -import ScoreIcon from "../../../../../assets/icons/monitor-graph-line.svg?react"; +import ChartBox from "../../../../../../Components/Charts/ChartBox/index.jsx"; +import AreaChart from "../Charts/AreaChart.jsx"; +import AreaChartLegend from "../Charts/AreaChartLegend.jsx"; +import SkeletonLayout from "./skeleton.jsx"; +import ScoreIcon from "../../../../../../assets/icons/monitor-graph-line.svg?react"; import { Stack } from "@mui/material"; import PropTypes from "prop-types"; diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx similarity index 100% rename from client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx rename to client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx similarity index 76% rename from client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx rename to client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx index cb79198e9..0811ae707 100644 --- a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx +++ b/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx @@ -1,7 +1,7 @@ -import StatusBoxes from "../../../../../Components/StatusBoxes"; -import StatBox from "../../../../../Components/StatBox"; +import StatusBoxes from "../../../../../../Components/StatusBoxes/index.jsx"; +import StatBox from "../../../../../../Components/StatBox/index.jsx"; import { Typography } from "@mui/material"; -import { getHumanReadableDuration } from "../../../../../Utils/timeUtils"; +import { getHumanReadableDuration } from "../../../../../../Utils/timeUtils.js"; import { useTranslation } from "react-i18next"; const PageSpeedStatusBoxes = ({ shouldRender, monitor }) => { diff --git a/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/index.jsx similarity index 78% rename from client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx rename to client/src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/index.jsx index 2f809ebbb..ce4704144 100644 --- a/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx +++ b/client/src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/index.jsx @@ -1,10 +1,10 @@ -import ChartBox from "../../../../../Components/Charts/ChartBox"; -import PerformanceIcon from "../../../../../assets/icons/performance-report.svg?react"; -import PieChart from "../Charts/PieChart"; +import ChartBox from "../../../../../../Components/Charts/ChartBox/index.jsx"; +import PerformanceIcon from "../../../../../../assets/icons/performance-report.svg?react"; +import PieChart from "../Charts/PieChart.jsx"; import { Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; -import PieChartLegend from "../Charts/PieChartLegend"; -import SkeletonLayout from "./skeleton"; +import PieChartLegend from "../Charts/PieChartLegend.jsx"; +import SkeletonLayout from "./skeleton.jsx"; import { useTranslation } from "react-i18next"; const PerformanceReport = ({ shouldRender, audits }) => { diff --git a/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx similarity index 100% rename from client/src/Pages/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx rename to client/src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx diff --git a/client/src/Pages/PageSpeed/Details/index.jsx b/client/src/Pages/v1/PageSpeed/Details/index.jsx similarity index 81% rename from client/src/Pages/PageSpeed/Details/index.jsx rename to client/src/Pages/v1/PageSpeed/Details/index.jsx index cfc799b36..cf9652bce 100644 --- a/client/src/Pages/PageSpeed/Details/index.jsx +++ b/client/src/Pages/v1/PageSpeed/Details/index.jsx @@ -1,17 +1,17 @@ // Components import { Stack, Typography } from "@mui/material"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader"; -import PageSpeedStatusBoxes from "./Components/PageSpeedStatusBoxes"; -import MonitorDetailsControlHeader from "../../../Components/MonitorDetailsControlHeader"; -import PageSpeedAreaChart from "./Components/PageSpeedAreaChart"; -import PerformanceReport from "./Components/PerformanceReport"; -import GenericFallback from "../../../Components/GenericFallback"; +import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; +import MonitorTimeFrameHeader from "../../../../Components/MonitorTimeFrameHeader/index.jsx"; +import PageSpeedStatusBoxes from "./Components/PageSpeedStatusBoxes/index.jsx"; +import MonitorDetailsControlHeader from "../../../../Components/MonitorDetailsControlHeader/index.jsx"; +import PageSpeedAreaChart from "./Components/PageSpeedAreaChart/index.jsx"; +import PerformanceReport from "./Components/PerformanceReport/index.jsx"; +import GenericFallback from "../../../../Components/GenericFallback/index.jsx"; // Utils import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "../../../Hooks/useIsAdmin"; +import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; import { useParams } from "react-router-dom"; -import { useFetchStatsByMonitorId } from "../../../Hooks/monitorHooks"; +import { useFetchStatsByMonitorId } from "../../../../Hooks/v1/monitorHooks.js"; import { useState } from "react"; import { useTranslation } from "react-i18next"; // Constants diff --git a/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx b/client/src/Pages/v1/PageSpeed/Monitors/Components/Card/index.jsx similarity index 95% rename from client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx rename to client/src/Pages/v1/PageSpeed/Monitors/Components/Card/index.jsx index 94d6b137b..db1f6e712 100644 --- a/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx +++ b/client/src/Pages/v1/PageSpeed/Monitors/Components/Card/index.jsx @@ -1,17 +1,17 @@ import PropTypes from "prop-types"; // import PageSpeedIcon from "../../../../assets/icons/page-speed.svg?react"; -import PageSpeedIcon from "../../../../../assets/icons/page-speed.svg?react"; -import { StatusLabel } from "../../../../../Components/Label"; +import PageSpeedIcon from "../../../../../../assets/icons/page-speed.svg?react"; +import { StatusLabel } from "../../../../../../Components/Label/index.jsx"; import { Box, Grid, Stack, Typography } from "@mui/material"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts"; import { useSelector } from "react-redux"; -import { formatDateWithTz, formatDurationSplit } from "../../../../../Utils/timeUtils"; -import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils"; +import { formatDateWithTz, formatDurationSplit } from "../../../../../../Utils/timeUtils.js"; +import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import IconBox from "../../../../../Components/IconBox"; +import IconBox from "../../../../../../Components/IconBox/index.jsx"; /** * CustomToolTip displays a tooltip with formatted date and score information. * @param {Object} props diff --git a/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx b/client/src/Pages/v1/PageSpeed/Monitors/Components/MonitorGrid/index.jsx similarity index 88% rename from client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx rename to client/src/Pages/v1/PageSpeed/Monitors/Components/MonitorGrid/index.jsx index 6266ebdc9..6b0a800fb 100644 --- a/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx +++ b/client/src/Pages/v1/PageSpeed/Monitors/Components/MonitorGrid/index.jsx @@ -1,5 +1,5 @@ import { Grid, Grid2 } from "@mui/material"; -import Card from "../Card"; +import Card from "../Card/index.jsx"; const MonitorGrid = ({ shouldRender, monitors }) => { return ( diff --git a/client/src/Pages/PageSpeed/Monitors/index.jsx b/client/src/Pages/v1/PageSpeed/Monitors/index.jsx similarity index 69% rename from client/src/Pages/PageSpeed/Monitors/index.jsx rename to client/src/Pages/v1/PageSpeed/Monitors/index.jsx index b5b13ba6e..e568e8d82 100644 --- a/client/src/Pages/PageSpeed/Monitors/index.jsx +++ b/client/src/Pages/v1/PageSpeed/Monitors/index.jsx @@ -1,18 +1,18 @@ // Components import { useState } from "react"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; +import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; import { Stack } from "@mui/material"; -import CreateMonitorHeader from "../../../Components/MonitorCreateHeader"; -import MonitorCountHeader from "../../../Components/MonitorCountHeader"; -import MonitorGrid from "./Components/MonitorGrid"; -import PageStateWrapper from "../../../Components/PageStateWrapper"; -import FallbackPageSpeedWarning from "../../../Components/Fallback/FallbackPageSpeedWarning"; +import CreateMonitorHeader from "../../../../Components/MonitorCreateHeader/index.jsx"; +import MonitorCountHeader from "../../../../Components/MonitorCountHeader/index.jsx"; +import MonitorGrid from "./Components/MonitorGrid/index.jsx"; +import PageStateWrapper from "../../../../Components/PageStateWrapper/index.jsx"; +import FallbackPageSpeedWarning from "../../../../Components/Fallback/FallbackPageSpeedWarning.jsx"; // Utils import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "../../../Hooks/useIsAdmin"; -import { useFetchMonitorsByTeamId } from "../../../Hooks/monitorHooks"; -import { useFetchSettings } from "../../../Hooks/settingsHooks"; +import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; +import { useFetchMonitorsByTeamId } from "../../../../Hooks/v1/monitorHooks.js"; +import { useFetchSettings } from "../../../../Hooks/v1/settingsHooks.js"; // Constants const BREADCRUMBS = [{ name: `pagespeed`, path: "/pagespeed" }]; const TYPES = ["pagespeed"]; diff --git a/client/src/Pages/ServerUnreachable.jsx b/client/src/Pages/v1/ServerUnreachable.jsx similarity index 90% rename from client/src/Pages/ServerUnreachable.jsx rename to client/src/Pages/v1/ServerUnreachable.jsx index 466633144..0db697543 100644 --- a/client/src/Pages/ServerUnreachable.jsx +++ b/client/src/Pages/v1/ServerUnreachable.jsx @@ -2,14 +2,14 @@ import React, { useState } from "react"; import { Box, Typography, Button, Stack } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; -import { networkService } from "../Utils/NetworkService"; -import Alert from "../Components/Alert"; -import { createToast } from "../Utils/toastUtils"; +import { networkService } from "../../Utils/NetworkService.js"; +import Alert from "../../Components/Alert/index.jsx"; +import { createToast } from "../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; -import Background from "../assets/Images/background-grid.svg?react"; -import Logo from "../assets/icons/checkmate-icon.svg?react"; -import ThemeSwitch from "../Components/ThemeSwitch"; -import LanguageSelector from "../Components/LanguageSelector"; +import Background from "../../assets/Images/background-grid.svg?react"; +import Logo from "../../assets/icons/checkmate-icon.svg?react"; +import ThemeSwitch from "../../Components/ThemeSwitch/index.jsx"; +import LanguageSelector from "../../Components/LanguageSelector.jsx"; const ServerUnreachable = () => { const theme = useTheme(); diff --git a/client/src/Pages/Settings/SettingsAbout.jsx b/client/src/Pages/v1/Settings/SettingsAbout.jsx similarity index 88% rename from client/src/Pages/Settings/SettingsAbout.jsx rename to client/src/Pages/v1/Settings/SettingsAbout.jsx index a80351393..3d5f5224e 100644 --- a/client/src/Pages/Settings/SettingsAbout.jsx +++ b/client/src/Pages/v1/Settings/SettingsAbout.jsx @@ -1,10 +1,10 @@ import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; -import ConfigBox from "../../Components/ConfigBox"; +import ConfigBox from "../../../Components/ConfigBox/index.jsx"; // Utils import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import Link from "../../Components/Link"; +import Link from "../../../Components/Link/index.jsx"; const SettingsAbout = () => { const theme = useTheme(); diff --git a/client/src/Pages/Settings/SettingsDemoMonitors.jsx b/client/src/Pages/v1/Settings/SettingsDemoMonitors.jsx similarity index 95% rename from client/src/Pages/Settings/SettingsDemoMonitors.jsx rename to client/src/Pages/v1/Settings/SettingsDemoMonitors.jsx index 13e93421e..96f907970 100644 --- a/client/src/Pages/Settings/SettingsDemoMonitors.jsx +++ b/client/src/Pages/v1/Settings/SettingsDemoMonitors.jsx @@ -1,12 +1,12 @@ import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import Button from "@mui/material/Button"; -import ConfigBox from "../../Components/ConfigBox"; +import ConfigBox from "../../../Components/ConfigBox/index.jsx"; // Utils import { useTheme } from "@emotion/react"; import { PropTypes } from "prop-types"; import { useTranslation } from "react-i18next"; -import Dialog from "../../Components/Dialog"; +import Dialog from "../../../Components/Dialog/index.jsx"; import { useState } from "react"; const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) => { diff --git a/client/src/Pages/Settings/SettingsEmail.jsx b/client/src/Pages/v1/Settings/SettingsEmail.jsx similarity index 94% rename from client/src/Pages/Settings/SettingsEmail.jsx rename to client/src/Pages/v1/Settings/SettingsEmail.jsx index 1f86b1984..c2389b277 100644 --- a/client/src/Pages/Settings/SettingsEmail.jsx +++ b/client/src/Pages/v1/Settings/SettingsEmail.jsx @@ -1,19 +1,19 @@ import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; -import ConfigBox from "../../Components/ConfigBox"; -import TextInput from "../../Components/Inputs/TextInput"; +import ConfigBox from "../../../Components/ConfigBox/index.jsx"; +import TextInput from "../../../Components/Inputs/TextInput/index.jsx"; import Button from "@mui/material/Button"; import Stack from "@mui/material/Stack"; import { Switch } from "@mui/material"; -import TextLink from "../../Components/TextLink"; +import TextLink from "../../../Components/TextLink/index.jsx"; // Utils import { useTheme } from "@emotion/react"; import { PropTypes } from "prop-types"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments"; -import { useSendTestEmail } from "../../Hooks/useSendTestEmail"; -import { createToast } from "../../Utils/toastUtils"; +import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments/index.jsx"; +import { useSendTestEmail } from "../../../Hooks/v1/useSendTestEmail.js"; +import { createToast } from "../../../Utils/toastUtils.jsx"; const SettingsEmail = ({ isAdmin, diff --git a/client/src/Pages/Settings/SettingsGlobalThresholds.jsx b/client/src/Pages/v1/Settings/SettingsGlobalThresholds.jsx similarity index 94% rename from client/src/Pages/Settings/SettingsGlobalThresholds.jsx rename to client/src/Pages/v1/Settings/SettingsGlobalThresholds.jsx index acc101ab4..e238c6475 100644 --- a/client/src/Pages/Settings/SettingsGlobalThresholds.jsx +++ b/client/src/Pages/v1/Settings/SettingsGlobalThresholds.jsx @@ -1,8 +1,8 @@ import Box from "@mui/material/Box"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import ConfigBox from "../../Components/ConfigBox"; -import TextInput from "../../Components/Inputs/TextInput"; +import ConfigBox from "../../../Components/ConfigBox/index.jsx"; +import TextInput from "../../../Components/Inputs/TextInput/index.jsx"; import { useTheme } from "@emotion/react"; import { PropTypes } from "prop-types"; diff --git a/client/src/Pages/Settings/SettingsPagespeed.jsx b/client/src/Pages/v1/Settings/SettingsPagespeed.jsx similarity index 91% rename from client/src/Pages/Settings/SettingsPagespeed.jsx rename to client/src/Pages/v1/Settings/SettingsPagespeed.jsx index 157c75dac..83b2e23c7 100644 --- a/client/src/Pages/Settings/SettingsPagespeed.jsx +++ b/client/src/Pages/v1/Settings/SettingsPagespeed.jsx @@ -2,9 +2,9 @@ import Box from "@mui/material/Box"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import Button from "@mui/material/Button"; -import ConfigBox from "../../Components/ConfigBox"; -import TextInput from "../../Components/Inputs/TextInput"; -import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments"; +import ConfigBox from "../../../Components/ConfigBox/index.jsx"; +import TextInput from "../../../Components/Inputs/TextInput/index.jsx"; +import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments/index.jsx"; // Utils import { useTheme } from "@emotion/react"; import { PropTypes } from "prop-types"; diff --git a/client/src/Pages/Settings/SettingsStats.jsx b/client/src/Pages/v1/Settings/SettingsStats.jsx similarity index 92% rename from client/src/Pages/Settings/SettingsStats.jsx rename to client/src/Pages/v1/Settings/SettingsStats.jsx index 740ac3a37..52f739ecf 100644 --- a/client/src/Pages/Settings/SettingsStats.jsx +++ b/client/src/Pages/v1/Settings/SettingsStats.jsx @@ -2,9 +2,9 @@ import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import Button from "@mui/material/Button"; import Stack from "@mui/material/Stack"; -import ConfigBox from "../../Components/ConfigBox"; -import TextInput from "../../Components/Inputs/TextInput"; -import Dialog from "../../Components/Dialog"; +import ConfigBox from "../../../Components/ConfigBox/index.jsx"; +import TextInput from "../../../Components/Inputs/TextInput/index.jsx"; +import Dialog from "../../../Components/Dialog/index.jsx"; // Utils import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/Settings/SettingsTimeZone.jsx b/client/src/Pages/v1/Settings/SettingsTimeZone.jsx similarity index 90% rename from client/src/Pages/Settings/SettingsTimeZone.jsx rename to client/src/Pages/v1/Settings/SettingsTimeZone.jsx index ee5cf48de..59b2008f4 100644 --- a/client/src/Pages/Settings/SettingsTimeZone.jsx +++ b/client/src/Pages/v1/Settings/SettingsTimeZone.jsx @@ -1,9 +1,9 @@ import Box from "@mui/material/Box"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import ConfigBox from "../../Components/ConfigBox"; -import Search from "../../Components/Inputs/Search"; -import timezones from "../../Utils/timezones.json"; +import ConfigBox from "../../../Components/ConfigBox/index.jsx"; +import Search from "../../../Components/Inputs/Search/index.jsx"; +import timezones from "../../../Utils/timezones.json"; // Utils import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/Settings/SettingsUI.jsx b/client/src/Pages/v1/Settings/SettingsUI.jsx similarity index 91% rename from client/src/Pages/Settings/SettingsUI.jsx rename to client/src/Pages/v1/Settings/SettingsUI.jsx index aab5f863d..91f4cea3c 100644 --- a/client/src/Pages/Settings/SettingsUI.jsx +++ b/client/src/Pages/v1/Settings/SettingsUI.jsx @@ -1,8 +1,8 @@ import Box from "@mui/material/Box"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import ConfigBox from "../../Components/ConfigBox"; -import Select from "../../Components/Inputs/Select"; +import ConfigBox from "../../../Components/ConfigBox/index.jsx"; +import Select from "../../../Components/Inputs/Select/index.jsx"; // Utils import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/Settings/SettingsURL.jsx b/client/src/Pages/v1/Settings/SettingsURL.jsx similarity index 90% rename from client/src/Pages/Settings/SettingsURL.jsx rename to client/src/Pages/v1/Settings/SettingsURL.jsx index 24c49cad1..105658d4f 100644 --- a/client/src/Pages/Settings/SettingsURL.jsx +++ b/client/src/Pages/v1/Settings/SettingsURL.jsx @@ -1,8 +1,8 @@ import Box from "@mui/material/Box"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import ConfigBox from "../../Components/ConfigBox"; -import Select from "../../Components/Inputs/Select"; +import ConfigBox from "../../../Components/ConfigBox/index.jsx"; +import Select from "../../../Components/Inputs/Select/index.jsx"; // Utils import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx similarity index 89% rename from client/src/Pages/Settings/index.jsx rename to client/src/Pages/v1/Settings/index.jsx index 92f58b4c0..d27685ed6 100644 --- a/client/src/Pages/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -1,31 +1,31 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import Breadcrumbs from "../../Components/Breadcrumbs"; -import SettingsTimeZone from "./SettingsTimeZone"; -import SettingsUI from "./SettingsUI"; -import SettingsURL from "./SettingsURL"; -import SettingsPagespeed from "./SettingsPagespeed"; -import SettingsDemoMonitors from "./SettingsDemoMonitors"; -import SettingsAbout from "./SettingsAbout"; -import SettingsEmail from "./SettingsEmail"; -import SettingsGlobalThresholds from "./SettingsGlobalThresholds"; +import Breadcrumbs from "../../../Components/Breadcrumbs/index.jsx"; +import SettingsTimeZone from "./SettingsTimeZone.jsx"; +import SettingsUI from "./SettingsUI.jsx"; +import SettingsURL from "./SettingsURL.jsx"; +import SettingsPagespeed from "./SettingsPagespeed.jsx"; +import SettingsDemoMonitors from "./SettingsDemoMonitors.jsx"; +import SettingsAbout from "./SettingsAbout.jsx"; +import SettingsEmail from "./SettingsEmail.jsx"; +import SettingsGlobalThresholds from "./SettingsGlobalThresholds.jsx"; import Button from "@mui/material/Button"; // Utils -import { settingsValidation } from "../../Validation/validation"; +import { settingsValidation } from "../../../Validation/validation.js"; import { useState } from "react"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; import { useSelector, useDispatch } from "react-redux"; -import { setTimezone, setMode, setLanguage, setShowURL } from "../../Features/UI/uiSlice"; -import SettingsStats from "./SettingsStats"; +import { setTimezone, setMode, setLanguage, setShowURL } from "../../../Features/UI/uiSlice.js"; +import SettingsStats from "./SettingsStats.jsx"; -import { useFetchSettings, useSaveSettings } from "../../Hooks/settingsHooks"; -import { useIsAdmin } from "../../Hooks/useIsAdmin"; +import { useFetchSettings, useSaveSettings } from "../../../Hooks/v1/settingsHooks.js"; +import { useIsAdmin } from "../../../Hooks/v1/useIsAdmin.js"; import { useAddDemoMonitors, useDeleteAllMonitors, useDeleteMonitorStats, -} from "../../Hooks/monitorHooks"; +} from "../../../Hooks/v1/monitorHooks.js"; // Constants const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }]; diff --git a/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx b/client/src/Pages/v1/StatusPage/Create/Components/MonitorList/index.jsx similarity index 100% rename from client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx rename to client/src/Pages/v1/StatusPage/Create/Components/MonitorList/index.jsx diff --git a/client/src/Pages/StatusPage/Create/Components/Progress/index.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Progress/index.jsx similarity index 83% rename from client/src/Pages/StatusPage/Create/Components/Progress/index.jsx rename to client/src/Pages/v1/StatusPage/Create/Components/Progress/index.jsx index 14c4ce0f5..c9f7d5f8f 100644 --- a/client/src/Pages/StatusPage/Create/Components/Progress/index.jsx +++ b/client/src/Pages/v1/StatusPage/Create/Components/Progress/index.jsx @@ -1,8 +1,8 @@ import { Button, Box } from "@mui/material"; -import ProgressUpload from "../../../../../Components/ProgressBars"; +import ProgressUpload from "../../../../../../Components/ProgressBars/index.jsx"; import ImageIcon from "@mui/icons-material/Image"; import { useTranslation } from "react-i18next"; -import { formatBytes } from "../../../../../Utils/fileUtils"; +import { formatBytes } from "../../../../../../Utils/fileUtils.js"; const Progress = ({ isLoading, progressValue, logo, logoType, removeLogo, errors }) => { const { t } = useTranslation(); if (isLoading) { diff --git a/client/src/Pages/StatusPage/Create/Components/Skeleton/index.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Skeleton/index.jsx similarity index 100% rename from client/src/Pages/StatusPage/Create/Components/Skeleton/index.jsx rename to client/src/Pages/v1/StatusPage/Create/Components/Skeleton/index.jsx diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/ConfigStack.jsx similarity index 91% rename from client/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx rename to client/src/Pages/v1/StatusPage/Create/Components/Tabs/ConfigStack.jsx index e313f0e85..fb5278c4e 100644 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx +++ b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/ConfigStack.jsx @@ -1,5 +1,5 @@ import { Stack, Typography } from "@mui/material"; -import ConfigBox from "../../../../../Components/ConfigBox"; +import ConfigBox from "../../../../../../Components/ConfigBox/index.jsx"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Content.jsx similarity index 90% rename from client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx rename to client/src/Pages/v1/StatusPage/Create/Components/Tabs/Content.jsx index 327a2fb0a..23e713c5a 100644 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx +++ b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Content.jsx @@ -1,14 +1,14 @@ // Components import { Stack, Typography } from "@mui/material"; import { TabPanel } from "@mui/lab"; -import MonitorList from "../MonitorList"; -import Search from "../../../../../Components/Inputs/Search"; -import Checkbox from "../../../../../Components/Inputs/Checkbox"; +import MonitorList from "../MonitorList/index.jsx"; +import Search from "../../../../../../Components/Inputs/Search/index.jsx"; +import Checkbox from "../../../../../../Components/Inputs/Checkbox/index.jsx"; // Utils import { useState } from "react"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import ConfigStack from "./ConfigStack"; +import ConfigStack from "./ConfigStack.jsx"; const Content = ({ tabValue, form, diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Settings.jsx similarity index 87% rename from client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx rename to client/src/Pages/v1/StatusPage/Create/Components/Tabs/Settings.jsx index dd4345158..a4612e3da 100644 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx +++ b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Settings.jsx @@ -1,17 +1,17 @@ // Components import { Stack, Typography } from "@mui/material"; import { TabPanel } from "@mui/lab"; -import ConfigBox from "../../../../../Components/ConfigBox"; -import Checkbox from "../../../../../Components/Inputs/Checkbox"; -import TextInput from "../../../../../Components/Inputs/TextInput"; -import Search from "../../../../../Components/Inputs/Search"; -import ImageUpload from "../../../../../Components/Inputs/ImageUpload"; -import ColorPicker from "../../../../../Components/Inputs/ColorPicker"; -import Progress from "../Progress"; +import ConfigBox from "../../../../../../Components/ConfigBox/index.jsx"; +import Checkbox from "../../../../../../Components/Inputs/Checkbox/index.jsx"; +import TextInput from "../../../../../../Components/Inputs/TextInput/index.jsx"; +import Search from "../../../../../../Components/Inputs/Search/index.jsx"; +import ImageUpload from "../../../../../../Components/Inputs/ImageUpload/index.jsx"; +import ColorPicker from "../../../../../../Components/Inputs/ColorPicker/index.jsx"; +import Progress from "../Progress/index.jsx"; // Utils import { useTheme } from "@emotion/react"; -import timezones from "../../../../../Utils/timezones.json"; +import timezones from "../../../../../../Utils/timezones.json"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; import { useMemo, useState, useCallback } from "react"; diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/index.jsx similarity index 93% rename from client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx rename to client/src/Pages/v1/StatusPage/Create/Components/Tabs/index.jsx index 60d86e1c9..971b8d85c 100644 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx +++ b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/index.jsx @@ -1,12 +1,12 @@ // Components import { TabContext } from "@mui/lab"; import { Tab } from "@mui/material"; -import Settings from "./Settings"; -import Content from "./Content"; +import Settings from "./Settings.jsx"; +import Content from "./Content.jsx"; // Utils import PropTypes from "prop-types"; -import CustomTabList from "../../../../../Components/Tab"; +import CustomTabList from "../../../../../../Components/Tab/index.jsx"; const Tabs = ({ isCreate, diff --git a/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx b/client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx similarity index 83% rename from client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx rename to client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx index a2ac5f55d..808598052 100644 --- a/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx +++ b/client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { networkService } from "../../../../main"; -import { createToast } from "../../../../Utils/toastUtils"; +import { networkService } from "../../../../../main.jsx"; +import { createToast } from "../../../../../Utils/toastUtils.jsx"; const useCreateStatusPage = (isCreate, url) => { const [isLoading, setIsLoading] = useState(false); diff --git a/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx b/client/src/Pages/v1/StatusPage/Create/Hooks/useMonitorsFetch.jsx similarity index 88% rename from client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx rename to client/src/Pages/v1/StatusPage/Create/Hooks/useMonitorsFetch.jsx index f380bed9a..2a7db202a 100644 --- a/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx +++ b/client/src/Pages/v1/StatusPage/Create/Hooks/useMonitorsFetch.jsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; -import { networkService } from "../../../../main"; +import { networkService } from "../../../../../main.jsx"; import { useSelector } from "react-redux"; -import { createToast } from "../../../../Utils/toastUtils"; +import { createToast } from "../../../../../Utils/toastUtils.jsx"; const useMonitorsFetch = () => { const { user } = useSelector((state) => state.auth); diff --git a/client/src/Pages/StatusPage/Create/index.jsx b/client/src/Pages/v1/StatusPage/Create/index.jsx similarity index 92% rename from client/src/Pages/StatusPage/Create/index.jsx rename to client/src/Pages/v1/StatusPage/Create/index.jsx index d4cbc0fd5..674a158ac 100644 --- a/client/src/Pages/StatusPage/Create/index.jsx +++ b/client/src/Pages/v1/StatusPage/Create/index.jsx @@ -1,23 +1,23 @@ // Components import { Stack, Button, Typography } from "@mui/material"; -import Tabs from "./Components/Tabs"; -import GenericFallback from "../../../Components/GenericFallback"; -import SkeletonLayout from "./Components/Skeleton"; -import Dialog from "../../../Components/Dialog"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; +import Tabs from "./Components/Tabs/index.jsx"; +import GenericFallback from "../../../../Components/GenericFallback/index.jsx"; +import SkeletonLayout from "./Components/Skeleton/index.jsx"; +import Dialog from "../../../../Components/Dialog/index.jsx"; +import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; //Utils import { useTheme } from "@emotion/react"; import { useState, useEffect, useRef, useCallback } from "react"; -import { statusPageValidation } from "../../../Validation/validation"; -import { buildErrors } from "../../../Validation/error"; -import { useMonitorsFetch } from "./Hooks/useMonitorsFetch"; -import { useCreateStatusPage } from "./Hooks/useCreateStatusPage"; -import { createToast } from "../../../Utils/toastUtils"; +import { statusPageValidation } from "../../../../Validation/validation.js"; +import { buildErrors } from "../../../../Validation/error.js"; +import { useMonitorsFetch } from "./Hooks/useMonitorsFetch.jsx"; +import { useCreateStatusPage } from "./Hooks/useCreateStatusPage.jsx"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; import { useNavigate } from "react-router-dom"; -import { useStatusPageFetch } from "../Status/Hooks/useStatusPageFetch"; +import { useStatusPageFetch } from "../Status/Hooks/useStatusPageFetch.jsx"; import { useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { useStatusPageDelete } from "../Status/Hooks/useStatusPageDelete"; +import { useStatusPageDelete } from "../Status/Hooks/useStatusPageDelete.jsx"; //Constants const ERROR_TAB_MAPPING = [ ["companyName", "url", "timezone", "color", "isPublished", "logo"], diff --git a/client/src/Pages/StatusPage/Status/Components/AdminLink/index.jsx b/client/src/Pages/v1/StatusPage/Status/Components/AdminLink/index.jsx similarity index 100% rename from client/src/Pages/StatusPage/Status/Components/AdminLink/index.jsx rename to client/src/Pages/v1/StatusPage/Status/Components/AdminLink/index.jsx diff --git a/client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx b/client/src/Pages/v1/StatusPage/Status/Components/ControlsHeader/index.jsx similarity index 92% rename from client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx rename to client/src/Pages/v1/StatusPage/Status/Components/ControlsHeader/index.jsx index beac3b0d2..fa5ed71f1 100644 --- a/client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx +++ b/client/src/Pages/v1/StatusPage/Status/Components/ControlsHeader/index.jsx @@ -1,8 +1,8 @@ // Components import { Box, Stack, Typography, Button } from "@mui/material"; -import Image from "../../../../../Components/Image"; -import SettingsIcon from "../../../../../assets/icons/settings-bold.svg?react"; -import ThemeSwitch from "../../../../../Components/ThemeSwitch"; +import Image from "../../../../../../Components/Image/index.jsx"; +import SettingsIcon from "../../../../../../assets/icons/settings-bold.svg?react"; +import ThemeSwitch from "../../../../../../Components/ThemeSwitch/index.jsx"; import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward"; //Utils import { useTheme } from "@mui/material/styles"; diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx b/client/src/Pages/v1/StatusPage/Status/Components/MonitorsList/index.jsx similarity index 82% rename from client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx rename to client/src/Pages/v1/StatusPage/Status/Components/MonitorsList/index.jsx index 545974c57..fd04dae66 100644 --- a/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx +++ b/client/src/Pages/v1/StatusPage/Status/Components/MonitorsList/index.jsx @@ -1,12 +1,12 @@ // Components import { Stack, Box } from "@mui/material"; -import Host from "../../../../../Components/Host"; -import StatusPageBarChart from "../../../../../Components/Charts/StatusPageBarChart"; -import { StatusLabel } from "../../../../../Components/Label"; +import Host from "../../../../../../Components/Host/index.jsx"; +import StatusPageBarChart from "../../../../../../Components/Charts/StatusPageBarChart/index.jsx"; +import { StatusLabel } from "../../../../../../Components/Label/index.jsx"; //Utils import { useTheme } from "@mui/material/styles"; -import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils"; +import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js"; import PropTypes from "prop-types"; import { useSelector } from "react-redux"; diff --git a/client/src/Pages/StatusPage/Status/Components/Skeleton/index.jsx b/client/src/Pages/v1/StatusPage/Status/Components/Skeleton/index.jsx similarity index 100% rename from client/src/Pages/StatusPage/Status/Components/Skeleton/index.jsx rename to client/src/Pages/v1/StatusPage/Status/Components/Skeleton/index.jsx diff --git a/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx b/client/src/Pages/v1/StatusPage/Status/Components/StatusBar/index.jsx similarity index 100% rename from client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx rename to client/src/Pages/v1/StatusPage/Status/Components/StatusBar/index.jsx diff --git a/client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx b/client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageDelete.jsx similarity index 89% rename from client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx rename to client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageDelete.jsx index 2c39c70b3..038a771ba 100644 --- a/client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx +++ b/client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageDelete.jsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { networkService } from "../../../../Utils/NetworkService"; -import { createToast } from "../../../../Utils/toastUtils"; +import { networkService } from "../../../../../Utils/NetworkService.js"; +import { createToast } from "../../../../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; /** diff --git a/client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx b/client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx similarity index 87% rename from client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx rename to client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx index ae9c7b579..506082887 100644 --- a/client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx +++ b/client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx @@ -1,9 +1,9 @@ import { useEffect, useState, useCallback } from "react"; -import { networkService } from "../../../../main"; +import { networkService } from "../../../../../main.jsx"; import { useSelector } from "react-redux"; -import { createToast } from "../../../../Utils/toastUtils"; +import { createToast } from "../../../../../Utils/toastUtils.jsx"; import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils"; +import { useMonitorUtils } from "../../../../../Hooks/v1/useMonitorUtils.js"; const useStatusPageFetch = (isCreate = false, url) => { const [isLoading, setIsLoading] = useState(true); diff --git a/client/src/Pages/StatusPage/Status/index.jsx b/client/src/Pages/v1/StatusPage/Status/index.jsx similarity index 85% rename from client/src/Pages/StatusPage/Status/index.jsx rename to client/src/Pages/v1/StatusPage/Status/index.jsx index a77f2a380..48fc44fc9 100644 --- a/client/src/Pages/StatusPage/Status/index.jsx +++ b/client/src/Pages/v1/StatusPage/Status/index.jsx @@ -1,18 +1,18 @@ // Components import { Typography, Stack } from "@mui/material"; -import GenericFallback from "../../../Components/GenericFallback"; -import AdminLink from "./Components/AdminLink"; -import ControlsHeader from "./Components/ControlsHeader"; -import SkeletonLayout from "./Components/Skeleton"; -import StatusBar from "./Components/StatusBar"; -import MonitorsList from "./Components/MonitorsList"; -import Breadcrumbs from "../../../Components/Breadcrumbs/index.jsx"; -import TextLink from "../../../Components/TextLink"; +import GenericFallback from "../../../../Components/GenericFallback/index.jsx"; +import AdminLink from "./Components/AdminLink/index.jsx"; +import ControlsHeader from "./Components/ControlsHeader/index.jsx"; +import SkeletonLayout from "./Components/Skeleton/index.jsx"; +import StatusBar from "./Components/StatusBar/index.jsx"; +import MonitorsList from "./Components/MonitorsList/index.jsx"; +import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; +import TextLink from "../../../../Components/TextLink/index.jsx"; // Utils -import { useStatusPageFetch } from "./Hooks/useStatusPageFetch"; +import { useStatusPageFetch } from "./Hooks/useStatusPageFetch.jsx"; import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "../../../Hooks/useIsAdmin"; +import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; import { useLocation } from "react-router-dom"; import { useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx b/client/src/Pages/v1/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx similarity index 93% rename from client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx rename to client/src/Pages/v1/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx index 04d7a6cfb..d8d68fb2e 100644 --- a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx +++ b/client/src/Pages/v1/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx @@ -1,7 +1,7 @@ -import DataTable from "../../../../../Components/Table"; +import DataTable from "../../../../../../Components/Table/index.jsx"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; -import { StatusLabel } from "../../../../../Components/Label"; +import { StatusLabel } from "../../../../../../Components/Label/index.jsx"; import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward"; import { Stack, Typography } from "@mui/material"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx b/client/src/Pages/v1/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx similarity index 86% rename from client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx rename to client/src/Pages/v1/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx index b236368c8..89df1b84d 100644 --- a/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx +++ b/client/src/Pages/v1/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; -import { networkService } from "../../../../main"; +import { networkService } from "../../../../../main.jsx"; import { useSelector } from "react-redux"; -import { createToast } from "../../../../Utils/toastUtils"; +import { createToast } from "../../../../../Utils/toastUtils.jsx"; const useStatusPagesFetch = () => { const { user } = useSelector((state) => state.auth); diff --git a/client/src/Pages/StatusPage/StatusPages/index.jsx b/client/src/Pages/v1/StatusPage/StatusPages/index.jsx similarity index 70% rename from client/src/Pages/StatusPage/StatusPages/index.jsx rename to client/src/Pages/v1/StatusPage/StatusPages/index.jsx index fa26c2b81..8e0c89ce3 100644 --- a/client/src/Pages/StatusPage/StatusPages/index.jsx +++ b/client/src/Pages/v1/StatusPage/StatusPages/index.jsx @@ -1,13 +1,13 @@ // Components import { Stack } from "@mui/material"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import MonitorCreateHeader from "../../../Components/MonitorCreateHeader"; -import StatusPagesTable from "./Components/StatusPagesTable"; -import PageStateWrapper from "../../../Components/PageStateWrapper"; +import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; +import MonitorCreateHeader from "../../../../Components/MonitorCreateHeader/index.jsx"; +import StatusPagesTable from "./Components/StatusPagesTable/index.jsx"; +import PageStateWrapper from "../../../../Components/PageStateWrapper/index.jsx"; // Utils import { useTheme } from "@emotion/react"; -import { useStatusPagesFetch } from "./Hooks/useStatusPagesFetch"; -import { useIsAdmin } from "../../../Hooks/useIsAdmin"; +import { useStatusPagesFetch } from "./Hooks/useStatusPagesFetch.jsx"; +import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; const BREADCRUMBS = [{ name: `Status Pages`, path: "" }]; const StatusPages = () => { diff --git a/client/src/Pages/Uptime/BulkImport/Upload.jsx b/client/src/Pages/v1/Uptime/BulkImport/Upload.jsx similarity index 100% rename from client/src/Pages/Uptime/BulkImport/Upload.jsx rename to client/src/Pages/v1/Uptime/BulkImport/Upload.jsx diff --git a/client/src/Pages/Uptime/BulkImport/index.jsx b/client/src/Pages/v1/Uptime/BulkImport/index.jsx similarity index 89% rename from client/src/Pages/Uptime/BulkImport/index.jsx rename to client/src/Pages/v1/Uptime/BulkImport/index.jsx index 9490b4eab..412ba0f97 100644 --- a/client/src/Pages/Uptime/BulkImport/index.jsx +++ b/client/src/Pages/v1/Uptime/BulkImport/index.jsx @@ -5,14 +5,14 @@ import { useState } from "react"; import { Box, Stack, Typography, Button, Link } from "@mui/material"; //Components -import { createToast } from "../../../Utils/toastUtils"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import ConfigBox from "../../../Components/ConfigBox"; -import UploadFile from "./Upload"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; +import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; +import ConfigBox from "../../../../Components/ConfigBox/index.jsx"; +import UploadFile from "./Upload.jsx"; import { useSelector } from "react-redux"; import { useNavigate } from "react-router-dom"; import { Trans, useTranslation } from "react-i18next"; -import { useCreateBulkMonitors } from "../../../Hooks/monitorHooks"; +import { useCreateBulkMonitors } from "../../../../Hooks/v1/monitorHooks.js"; const BulkImport = () => { const theme = useTheme(); diff --git a/client/src/Pages/Uptime/Create/index.css b/client/src/Pages/v1/Uptime/Create/index.css similarity index 100% rename from client/src/Pages/Uptime/Create/index.css rename to client/src/Pages/v1/Uptime/Create/index.css diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/v1/Uptime/Create/index.jsx similarity index 94% rename from client/src/Pages/Uptime/Create/index.jsx rename to client/src/Pages/v1/Uptime/Create/index.jsx index 3f905fa11..6bb2a3b33 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/v1/Uptime/Create/index.jsx @@ -9,31 +9,31 @@ import { Tooltip, Typography, } from "@mui/material"; -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import TextInput from "../../../Components/Inputs/TextInput"; -import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; -import Radio from "../../../Components/Inputs/Radio"; -import Select from "../../../Components/Inputs/Select"; -import ConfigBox from "../../../Components/ConfigBox"; -import NotificationsConfig from "../../../Components/NotificationConfig"; -import Checkbox from "../../../Components/Inputs/Checkbox"; -import Dialog from "../../../Components/Dialog"; -import PulseDot from "../../../Components/Animated/PulseDot"; -import SkeletonLayout from "./skeleton"; +import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; +import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; +import { HttpAdornment } from "../../../../Components/Inputs/TextInput/Adornments/index.jsx"; +import Radio from "../../../../Components/Inputs/Radio/index.jsx"; +import Select from "../../../../Components/Inputs/Select/index.jsx"; +import ConfigBox from "../../../../Components/ConfigBox/index.jsx"; +import NotificationsConfig from "../../../../Components/NotificationConfig/index.jsx"; +import Checkbox from "../../../../Components/Inputs/Checkbox/index.jsx"; +import Dialog from "../../../../Components/Dialog/index.jsx"; +import PulseDot from "../../../../Components/Animated/PulseDot.jsx"; +import SkeletonLayout from "./skeleton.jsx"; // Utils import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { monitorValidation } from "../../../Validation/validation"; -import { createToast } from "../../../Utils/toastUtils"; +import { monitorValidation } from "../../../../Validation/validation.js"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; import { PauseOutlined as PauseOutlinedIcon, PlayArrowOutlined as PlayArrowOutlinedIcon, } from "@mui/icons-material"; -import { useMonitorUtils } from "../../../Hooks/useMonitorUtils"; -import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications"; +import { useMonitorUtils } from "../../../../Hooks/v1/useMonitorUtils.js"; +import { useGetNotificationsByTeamId } from "../../../../Hooks/v1/useNotifications.js"; import { useParams } from "react-router-dom"; import { useCreateMonitor, @@ -42,7 +42,7 @@ import { usePauseMonitor, useFetchMonitorById, useFetchMonitorGames, -} from "../../../Hooks/monitorHooks"; +} from "../../../../Hooks/v1/monitorHooks.js"; /** * Create page renders monitor creation or configuration views. diff --git a/client/src/Pages/Uptime/Create/skeleton.jsx b/client/src/Pages/v1/Uptime/Create/skeleton.jsx similarity index 100% rename from client/src/Pages/Uptime/Create/skeleton.jsx rename to client/src/Pages/v1/Uptime/Create/skeleton.jsx diff --git a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx b/client/src/Pages/v1/Uptime/Details/Components/ChartBoxes/index.jsx similarity index 86% rename from client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx rename to client/src/Pages/v1/Uptime/Details/Components/ChartBoxes/index.jsx index b60a4dd04..c6b76c289 100644 --- a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx +++ b/client/src/Pages/v1/Uptime/Details/Components/ChartBoxes/index.jsx @@ -1,15 +1,15 @@ // Components import { Stack, Typography, Box } from "@mui/material"; -import ChartBox from "../../../../../Components/Charts/ChartBox"; -import UptimeIcon from "../../../../../assets/icons/uptime-icon.svg?react"; -import IncidentsIcon from "../../../../../assets/icons/incidents.svg?react"; -import AverageResponseIcon from "../../../../../assets/icons/average-response-icon.svg?react"; -import UpBarChart from "../Charts/UpBarChart"; -import DownBarChart from "../Charts/DownBarChart"; -import ResponseGaugeChart from "../Charts/ResponseGaugeChart"; -import SkeletonLayout from "./skeleton"; +import ChartBox from "../../../../../../Components/Charts/ChartBox/index.jsx"; +import UptimeIcon from "../../../../../../assets/icons/uptime-icon.svg?react"; +import IncidentsIcon from "../../../../../../assets/icons/incidents.svg?react"; +import AverageResponseIcon from "../../../../../../assets/icons/average-response-icon.svg?react"; +import UpBarChart from "../Charts/UpBarChart.jsx"; +import DownBarChart from "../Charts/DownBarChart.jsx"; +import ResponseGaugeChart from "../Charts/ResponseGaugeChart.jsx"; +import SkeletonLayout from "./skeleton.jsx"; // Utils -import { formatDateWithTz } from "../../../../../Utils/timeUtils"; +import { formatDateWithTz } from "../../../../../../Utils/timeUtils.js"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { useState } from "react"; diff --git a/client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx b/client/src/Pages/v1/Uptime/Details/Components/ChartBoxes/skeleton.jsx similarity index 100% rename from client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx rename to client/src/Pages/v1/Uptime/Details/Components/ChartBoxes/skeleton.jsx diff --git a/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx b/client/src/Pages/v1/Uptime/Details/Components/Charts/CustomLabels.jsx similarity index 93% rename from client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx rename to client/src/Pages/v1/Uptime/Details/Components/Charts/CustomLabels.jsx index 8df33953f..c9c1ff58d 100644 --- a/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx +++ b/client/src/Pages/v1/Uptime/Details/Components/Charts/CustomLabels.jsx @@ -1,6 +1,6 @@ import PropTypes from "prop-types"; import { useSelector } from "react-redux"; -import { formatDateWithTz } from "../../../../../Utils/timeUtils"; +import { formatDateWithTz } from "../../../../../../Utils/timeUtils.js"; const CustomLabels = ({ x, width, height, firstDataPoint, lastDataPoint, type }) => { const uiTimezone = useSelector((state) => state.ui.timezone); diff --git a/client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx b/client/src/Pages/v1/Uptime/Details/Components/Charts/DownBarChart.jsx similarity index 97% rename from client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx rename to client/src/Pages/v1/Uptime/Details/Components/Charts/DownBarChart.jsx index 662abc831..c5e08ecca 100644 --- a/client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx +++ b/client/src/Pages/v1/Uptime/Details/Components/Charts/DownBarChart.jsx @@ -2,7 +2,7 @@ import { memo, useState } from "react"; import { useTheme } from "@mui/material"; import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts"; import PropTypes from "prop-types"; -import CustomLabels from "./CustomLabels"; +import CustomLabels from "./CustomLabels.jsx"; const DownBarChart = memo(({ groupedDownChecks = [], type, onBarHover }) => { const theme = useTheme(); diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx b/client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx similarity index 100% rename from client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx rename to client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx b/client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChart.jsx similarity index 60% rename from client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx rename to client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChart.jsx index e2d8dea80..f306dfd76 100644 --- a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx +++ b/client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChart.jsx @@ -1,7 +1,7 @@ -import ChartBox from "../../../../../Components/Charts/ChartBox"; -import MonitorDetailsAreaChart from "../../../../../Components/Charts/MonitorDetailsAreaChart"; -import ResponseTimeIcon from "../../../../../assets/icons/response-time-icon.svg?react"; -import SkeletonLayout from "./ResponseTimeChartSkeleton"; +import ChartBox from "../../../../../../Components/Charts/ChartBox/index.jsx"; +import MonitorDetailsAreaChart from "../../../../../../Components/Charts/MonitorDetailsAreaChart/index.jsx"; +import ResponseTimeIcon from "../../../../../../assets/icons/response-time-icon.svg?react"; +import SkeletonLayout from "./ResponseTimeChartSkeleton.jsx"; import PropTypes from "prop-types"; const ResponseTImeChart = ({ isLoading = false, groupedChecks = [], dateRange }) => { diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx b/client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx similarity index 100% rename from client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx rename to client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx diff --git a/client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx b/client/src/Pages/v1/Uptime/Details/Components/Charts/UpBarChart.jsx similarity index 98% rename from client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx rename to client/src/Pages/v1/Uptime/Details/Components/Charts/UpBarChart.jsx index 317147b74..8f1af8912 100644 --- a/client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx +++ b/client/src/Pages/v1/Uptime/Details/Components/Charts/UpBarChart.jsx @@ -2,7 +2,7 @@ import { memo, useState } from "react"; import { useTheme } from "@mui/material"; import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts"; import PropTypes from "prop-types"; -import CustomLabels from "./CustomLabels"; +import CustomLabels from "./CustomLabels.jsx"; const getThemeColor = (responseTime) => { if (responseTime < 200) { diff --git a/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx b/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx similarity index 76% rename from client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx rename to client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx index 4d482aa07..cc5745a26 100644 --- a/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx +++ b/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx @@ -1,12 +1,12 @@ -import ChartBox from "../../../../../Components/Charts/ChartBox"; +import ChartBox from "../../../../../../Components/Charts/ChartBox/index.jsx"; import PropTypes from "prop-types"; -import HistoryIcon from "../../../../../assets/icons/history-icon.svg?react"; -import Table from "../../../../../Components/Table"; -import TablePagination from "../../../../../Components/Table/TablePagination"; -import { StatusLabel } from "../../../../../Components/Label"; +import HistoryIcon from "../../../../../../assets/icons/history-icon.svg?react"; +import Table from "../../../../../../Components/Table/index.jsx"; +import TablePagination from "../../../../../../Components/Table/TablePagination/index.jsx"; +import { StatusLabel } from "../../../../../../Components/Label/index.jsx"; import { useTranslation } from "react-i18next"; -import { formatDateWithTz } from "../../../../../Utils/timeUtils"; -import SkeletonLayout from "./skeleton"; +import { formatDateWithTz } from "../../../../../../Utils/timeUtils.js"; +import SkeletonLayout from "./skeleton.jsx"; const ResponseTable = ({ isLoading = false, checks = [], diff --git a/client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx b/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/skeleton.jsx similarity index 100% rename from client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx rename to client/src/Pages/v1/Uptime/Details/Components/ResponseTable/skeleton.jsx diff --git a/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx b/client/src/Pages/v1/Uptime/Details/Components/UptimeStatusBoxes/index.jsx similarity index 85% rename from client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx rename to client/src/Pages/v1/Uptime/Details/Components/UptimeStatusBoxes/index.jsx index abeb251fe..f0988466c 100644 --- a/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx +++ b/client/src/Pages/v1/Uptime/Details/Components/UptimeStatusBoxes/index.jsx @@ -1,11 +1,11 @@ -import StatusBoxes from "../../../../../Components/StatusBoxes"; -import StatBox from "../../../../../Components/StatBox"; +import StatusBoxes from "../../../../../../Components/StatusBoxes/index.jsx"; +import StatBox from "../../../../../../Components/StatBox/index.jsx"; import PropTypes from "prop-types"; -import { getHumanReadableDuration } from "../../../../../Utils/timeUtils"; +import { getHumanReadableDuration } from "../../../../../../Utils/timeUtils.js"; import { useTheme } from "@mui/material/styles"; import { Typography } from "@mui/material"; -import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils"; +import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js"; const UptimeStatusBoxes = ({ isLoading = false, diff --git a/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx b/client/src/Pages/v1/Uptime/Details/Hooks/useCertificateFetch.jsx similarity index 84% rename from client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx rename to client/src/Pages/v1/Uptime/Details/Hooks/useCertificateFetch.jsx index ad0db6f70..f85111f97 100644 --- a/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx +++ b/client/src/Pages/v1/Uptime/Details/Hooks/useCertificateFetch.jsx @@ -1,7 +1,7 @@ -import { logger } from "../../../../Utils/Logger"; +import { logger } from "../../../../../Utils/Logger.js"; import { useEffect, useState } from "react"; -import { networkService } from "../../../../main"; -import { formatDateWithTz } from "../../../../Utils/timeUtils"; +import { networkService } from "../../../../../main.jsx"; +import { formatDateWithTz } from "../../../../../Utils/timeUtils.js"; const useCertificateFetch = ({ monitor, diff --git a/client/src/Pages/Uptime/Details/index.jsx b/client/src/Pages/v1/Uptime/Details/index.jsx similarity index 83% rename from client/src/Pages/Uptime/Details/index.jsx rename to client/src/Pages/v1/Uptime/Details/index.jsx index c72a5756f..4e773dc2b 100644 --- a/client/src/Pages/Uptime/Details/index.jsx +++ b/client/src/Pages/v1/Uptime/Details/index.jsx @@ -1,12 +1,12 @@ // Components -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import MonitorDetailsControlHeader from "../../../Components/MonitorDetailsControlHeader"; -import MonitorTimeFrameHeader from "../../../Components/MonitorTimeFrameHeader"; -import ChartBoxes from "./Components/ChartBoxes"; -import ResponseTimeChart from "./Components/Charts/ResponseTimeChart"; -import ResponseTable from "./Components/ResponseTable"; -import UptimeStatusBoxes from "./Components/UptimeStatusBoxes"; -import GenericFallback from "../../../Components/GenericFallback"; +import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; +import MonitorDetailsControlHeader from "../../../../Components/MonitorDetailsControlHeader/index.jsx"; +import MonitorTimeFrameHeader from "../../../../Components/MonitorTimeFrameHeader/index.jsx"; +import ChartBoxes from "./Components/ChartBoxes/index.jsx"; +import ResponseTimeChart from "./Components/Charts/ResponseTimeChart.jsx"; +import ResponseTable from "./Components/ResponseTable/index.jsx"; +import UptimeStatusBoxes from "./Components/UptimeStatusBoxes/index.jsx"; +import GenericFallback from "../../../../Components/GenericFallback/index.jsx"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; @@ -15,10 +15,10 @@ import { useState } from "react"; import { useParams } from "react-router-dom"; import { useSelector } from "react-redux"; import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "../../../Hooks/useIsAdmin"; -import { useFetchUptimeMonitorById } from "../../../Hooks/monitorHooks"; -import useCertificateFetch from "./Hooks/useCertificateFetch"; -import { useFetchChecksByMonitor } from "../../../Hooks/checkHooks"; +import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; +import { useFetchUptimeMonitorById } from "../../../../Hooks/v1/monitorHooks.js"; +import useCertificateFetch from "./Hooks/useCertificateFetch.jsx"; +import { useFetchChecksByMonitor } from "../../../../Hooks/v1/checkHooks.js"; import { useTranslation } from "react-i18next"; // Constants diff --git a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx b/client/src/Pages/v1/Uptime/Monitors/Components/Filter/index.jsx similarity index 98% rename from client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx rename to client/src/Pages/v1/Uptime/Monitors/Components/Filter/index.jsx index 0efb06b6c..85c4a8540 100644 --- a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx +++ b/client/src/Pages/v1/Uptime/Monitors/Components/Filter/index.jsx @@ -1,6 +1,6 @@ import { useTheme } from "@emotion/react"; import PropTypes from "prop-types"; -import FilterHeader from "../../../../../Components/FilterHeader"; +import FilterHeader from "../../../../../../Components/FilterHeader/index.jsx"; import { useMemo } from "react"; import { Box, Button } from "@mui/material"; import ClearIcon from "@mui/icons-material/Clear"; diff --git a/client/src/Pages/Uptime/Monitors/Components/LoadingSpinner/index.jsx b/client/src/Pages/v1/Uptime/Monitors/Components/LoadingSpinner/index.jsx similarity index 100% rename from client/src/Pages/Uptime/Monitors/Components/LoadingSpinner/index.jsx rename to client/src/Pages/v1/Uptime/Monitors/Components/LoadingSpinner/index.jsx diff --git a/client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx b/client/src/Pages/v1/Uptime/Monitors/Components/SearchComponent/index.jsx similarity index 89% rename from client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx rename to client/src/Pages/v1/Uptime/Monitors/Components/SearchComponent/index.jsx index 0c6eec854..3339102a2 100644 --- a/client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx +++ b/client/src/Pages/v1/Uptime/Monitors/Components/SearchComponent/index.jsx @@ -1,7 +1,7 @@ import { useState } from "react"; -import Search from "../../../../../Components/Inputs/Search"; +import Search from "../../../../../../Components/Inputs/Search/index.jsx"; import { Box } from "@mui/material"; -import useDebounce from "../../Hooks/useDebounce"; +import useDebounce from "../../Hooks/useDebounce.jsx"; import { useEffect, useRef } from "react"; import PropTypes from "prop-types"; diff --git a/client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx b/client/src/Pages/v1/Uptime/Monitors/Components/Skeleton/index.jsx similarity index 100% rename from client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx rename to client/src/Pages/v1/Uptime/Monitors/Components/Skeleton/index.jsx diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx b/client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/index.jsx similarity index 91% rename from client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx rename to client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/index.jsx index 9cd29c4ca..5cba9e3b1 100644 --- a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx +++ b/client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/index.jsx @@ -1,9 +1,9 @@ import PropTypes from "prop-types"; import { Stack } from "@mui/material"; -import StatusBox from "./statusBox"; +import StatusBox from "./statusBox.jsx"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import SkeletonLayout from "./skeleton"; +import SkeletonLayout from "./skeleton.jsx"; const StatusBoxes = ({ shouldRender, monitorsSummary }) => { const theme = useTheme(); diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx b/client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx similarity index 100% rename from client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx rename to client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx b/client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx similarity index 90% rename from client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx rename to client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx index 78d555409..c745e7935 100644 --- a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx +++ b/client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx @@ -1,9 +1,9 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { Box, Stack, Typography } from "@mui/material"; -import Arrow from "../../../../../assets/icons/top-right-arrow.svg?react"; -import Background from "../../../../../assets/Images/background-grid.svg?react"; -import ClockSnooze from "../../../../../assets/icons/clock-snooze.svg?react"; +import Arrow from "../../../../../../assets/icons/top-right-arrow.svg?react"; +import Background from "../../../../../../assets/Images/background-grid.svg?react"; +import ClockSnooze from "../../../../../../assets/icons/clock-snooze.svg?react"; const StatusBox = ({ title, value, status }) => { const theme = useTheme(); diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx b/client/src/Pages/v1/Uptime/Monitors/Components/UptimeDataTable/index.jsx similarity index 90% rename from client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx rename to client/src/Pages/v1/Uptime/Monitors/Components/UptimeDataTable/index.jsx index 63a40f85a..575647eab 100644 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx +++ b/client/src/Pages/v1/Uptime/Monitors/Components/UptimeDataTable/index.jsx @@ -1,19 +1,19 @@ // Components import { Box, Stack } from "@mui/material"; -import DataTable from "../../../../../Components/Table"; +import DataTable from "../../../../../../Components/Table/index.jsx"; import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; -import Host from "../../../../../Components/Host"; -import { StatusLabel } from "../../../../../Components/Label"; -import BarChart from "../../../../../Components/Charts/BarChart"; -import ActionsMenu from "../../../../../Components/ActionsMenu"; +import Host from "../../../../../../Components/Host/index.jsx"; +import { StatusLabel } from "../../../../../../Components/Label/index.jsx"; +import BarChart from "../../../../../../Components/Charts/BarChart/index.jsx"; +import ActionsMenu from "../../../../../../Components/ActionsMenu/index.jsx"; -import LoadingSpinner from "../LoadingSpinner"; -import TableSkeleton from "../../../../../Components/Table/skeleton"; +import LoadingSpinner from "../LoadingSpinner/index.jsx"; +import TableSkeleton from "../../../../../../Components/Table/skeleton.jsx"; // Utils import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils"; +import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js"; import { useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/Uptime/Monitors/Hooks/useDebounce.jsx b/client/src/Pages/v1/Uptime/Monitors/Hooks/useDebounce.jsx similarity index 100% rename from client/src/Pages/Uptime/Monitors/Hooks/useDebounce.jsx rename to client/src/Pages/v1/Uptime/Monitors/Hooks/useDebounce.jsx diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/v1/Uptime/Monitors/index.jsx similarity index 86% rename from client/src/Pages/Uptime/Monitors/index.jsx rename to client/src/Pages/v1/Uptime/Monitors/index.jsx index 7f28c1cd7..dae0bc2d5 100644 --- a/client/src/Pages/Uptime/Monitors/index.jsx +++ b/client/src/Pages/v1/Uptime/Monitors/index.jsx @@ -4,32 +4,32 @@ // 2a.List of monitors must have the total number of monitors that match. // Components -import Breadcrumbs from "../../../Components/Breadcrumbs"; -import Greeting from "../../../Utils/greeting"; -import StatusBoxes from "./Components/StatusBoxes"; -import UptimeDataTable from "./Components/UptimeDataTable"; -import Pagination from "../../../Components/Table/TablePagination"; -import CreateMonitorHeader from "../../../Components/MonitorCreateHeader"; -import SearchComponent from "./Components/SearchComponent"; -import Filter from "./Components/Filter"; -import PageStateWrapper from "../../../Components/PageStateWrapper"; +import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; +import Greeting from "../../../../Utils/greeting.jsx"; +import StatusBoxes from "./Components/StatusBoxes/index.jsx"; +import UptimeDataTable from "./Components/UptimeDataTable/index.jsx"; +import Pagination from "../../../../Components/Table/TablePagination/index.jsx"; +import CreateMonitorHeader from "../../../../Components/MonitorCreateHeader/index.jsx"; +import SearchComponent from "./Components/SearchComponent/index.jsx"; +import Filter from "./Components/Filter/index.jsx"; +import PageStateWrapper from "../../../../Components/PageStateWrapper/index.jsx"; -import MonitorCountHeader from "../../../Components/MonitorCountHeader"; +import MonitorCountHeader from "../../../../Components/MonitorCountHeader/index.jsx"; // MUI Components import { Stack, Box, Button } from "@mui/material"; // Utils import { useState, useCallback, useEffect } from "react"; -import { useIsAdmin } from "../../../Hooks/useIsAdmin"; +import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; import { useSelector, useDispatch } from "react-redux"; -import { setRowsPerPage } from "../../../Features/UI/uiSlice"; +import { setRowsPerPage } from "../../../../Features/UI/uiSlice.js"; import PropTypes from "prop-types"; import { useFetchMonitorsWithSummary, useFetchMonitorsWithChecks, -} from "../../../Hooks/monitorHooks"; +} from "../../../../Hooks/v1/monitorHooks.js"; import { useTranslation } from "react-i18next"; const TYPES = ["http", "ping", "docker", "port", "game"]; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 7ddcd7e3c..0de847cb7 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -1,57 +1,57 @@ import { Navigate, Route, Routes as LibRoutes } from "react-router"; import HomeLayout from "../Components/Layouts/HomeLayout"; -import NotFound from "../Pages/NotFound"; +import NotFound from "../Pages/v1/NotFound"; // Auth -import AuthLogin from "../Pages/Auth/Login"; -import AuthRegister from "../Pages/Auth/Register/"; -import AuthForgotPassword from "../Pages/Auth/ForgotPassword"; -import AuthCheckEmail from "../Pages/Auth/CheckEmail"; -import AuthSetNewPassword from "../Pages/Auth/SetNewPassword"; -import AuthNewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed"; +import AuthLogin from "../Pages/v1/Auth/Login"; +import AuthRegister from "../Pages/v1/Auth/Register/"; +import AuthForgotPassword from "../Pages/v1/Auth/ForgotPassword"; +import AuthCheckEmail from "../Pages/v1/Auth/CheckEmail"; +import AuthSetNewPassword from "../Pages/v1/Auth/SetNewPassword"; +import AuthNewPasswordConfirmed from "../Pages/v1/Auth/NewPasswordConfirmed"; // Uptime -import Uptime from "../Pages/Uptime/Monitors"; -import UptimeDetails from "../Pages/Uptime/Details"; -import UptimeCreate from "../Pages/Uptime/Create"; +import Uptime from "../Pages/v1/Uptime/Monitors"; +import UptimeDetails from "../Pages/v1/Uptime/Details"; +import UptimeCreate from "../Pages/v1/Uptime/Create"; // PageSpeed -import PageSpeed from "../Pages/PageSpeed/Monitors"; -import PageSpeedDetails from "../Pages/PageSpeed/Details"; -import PageSpeedCreate from "../Pages/PageSpeed/Create"; +import PageSpeed from "../Pages/v1/PageSpeed/Monitors"; +import PageSpeedDetails from "../Pages/v1/PageSpeed/Details"; +import PageSpeedCreate from "../Pages/v1/PageSpeed/Create"; // Infrastructure -import Infrastructure from "../Pages/Infrastructure/Monitors"; -import InfrastructureCreate from "../Pages/Infrastructure/Create"; -import InfrastructureDetails from "../Pages/Infrastructure/Details"; +import Infrastructure from "../Pages/v1/Infrastructure/Monitors"; +import InfrastructureCreate from "../Pages/v1/Infrastructure/Create"; +import InfrastructureDetails from "../Pages/v1/Infrastructure/Details"; // Server Status -import ServerUnreachable from "../Pages/ServerUnreachable"; +import ServerUnreachable from "../Pages/v1/ServerUnreachable.jsx"; // Incidents -import Incidents from "../Pages/Incidents"; +import Incidents from "../Pages/v1/Incidents"; // Status pages -import CreateStatus from "../Pages/StatusPage/Create"; -import StatusPages from "../Pages/StatusPage/StatusPages"; -import Status from "../Pages/StatusPage/Status"; +import CreateStatus from "../Pages/v1/StatusPage/Create"; +import StatusPages from "../Pages/v1/StatusPage/StatusPages"; +import Status from "../Pages/v1/StatusPage/Status"; -import Notifications from "../Pages/Notifications"; -import CreateNotifications from "../Pages/Notifications/create"; +import Notifications from "../Pages/v1/Notifications"; +import CreateNotifications from "../Pages/v1/Notifications/create"; // Settings -import Account from "../Pages/Account"; -import EditUser from "../Pages/Account/EditUser"; -import Settings from "../Pages/Settings"; +import Account from "../Pages/v1/Account"; +import EditUser from "../Pages/v1/Account/EditUser"; +import Settings from "../Pages/v1/Settings"; -import Maintenance from "../Pages/Maintenance"; +import Maintenance from "../Pages/v1/Maintenance"; import ProtectedRoute from "../Components/ProtectedRoute"; import RoleProtectedRoute from "../Components/RoleProtectedRoute"; -import CreateNewMaintenanceWindow from "../Pages/Maintenance/CreateMaintenance"; +import CreateNewMaintenanceWindow from "../Pages/v1/Maintenance/CreateMaintenance"; import withAdminCheck from "../Components/HOC/withAdminCheck"; -import BulkImport from "../Pages/Uptime/BulkImport"; -import Logs from "../Pages/Logs"; +import BulkImport from "../Pages/v1/Uptime/BulkImport"; +import Logs from "../Pages/v1/Logs"; const Routes = () => { const AdminCheckedRegister = withAdminCheck(AuthRegister); diff --git a/client/tsconfig.app.json b/client/tsconfig.app.json new file mode 100644 index 000000000..f44839e9f --- /dev/null +++ b/client/tsconfig.app.json @@ -0,0 +1,32 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "paths": { + "@/*": ["./src/*"] + }, + "allowJs": true, + "checkJs": false + }, + "include": ["src"] +} diff --git a/client/tsconfig.json b/client/tsconfig.json new file mode 100644 index 000000000..a1a0f47d0 --- /dev/null +++ b/client/tsconfig.json @@ -0,0 +1,14 @@ +{ + "files": [], + "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }], + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "skipLibCheck": true, + "paths": { + "@/*": ["./src/*"] + }, + "allowJs": true, + "checkJs": false + } +} diff --git a/client/tsconfig.node.json b/client/tsconfig.node.json new file mode 100644 index 000000000..bbd18d2b2 --- /dev/null +++ b/client/tsconfig.node.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true, + "allowJs": true, + "checkJs": false + }, + "include": ["vite.config.ts"] +} diff --git a/client/vite.config.js b/client/vite.config.ts similarity index 63% rename from client/vite.config.js rename to client/vite.config.ts index 07b5de748..66630e88c 100644 --- a/client/vite.config.js +++ b/client/vite.config.ts @@ -1,10 +1,8 @@ -import { defineConfig, loadEnv } from "vite"; +import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import svgr from "vite-plugin-svgr"; -import { execSync } from "child_process"; -export default defineConfig(({ mode }) => { - const env = loadEnv(mode, process.cwd(), ""); +export default defineConfig(({}) => { let version = "3.2.0"; return { From 17b84cbe8ac03e26b38447098ebfbd55a2197a79 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 25 Sep 2025 10:19:08 -0700 Subject: [PATCH 022/400] format --- client/src/Pages/v1/Account/components/ProfilePanel.jsx | 6 +++++- .../Create/hooks/useInfrastructureSubmit.jsx | 5 ++++- .../Pages/v1/PageSpeed/Monitors/Components/Card/index.jsx | 5 ++++- client/src/Pages/v1/Settings/index.jsx | 7 ++++++- 4 files changed, 19 insertions(+), 4 deletions(-) diff --git a/client/src/Pages/v1/Account/components/ProfilePanel.jsx b/client/src/Pages/v1/Account/components/ProfilePanel.jsx index 8e607f4bd..1ce1d34da 100644 --- a/client/src/Pages/v1/Account/components/ProfilePanel.jsx +++ b/client/src/Pages/v1/Account/components/ProfilePanel.jsx @@ -7,7 +7,11 @@ import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; import ImageUpload from "../../../../Components/Inputs/ImageUpload/index.jsx"; import { newOrChangedCredentials } from "../../../../Validation/validation.js"; import { useDispatch, useSelector } from "react-redux"; -import { clearAuthState, deleteUser, update } from "../../../../Features/Auth/authSlice.js"; +import { + clearAuthState, + deleteUser, + update, +} from "../../../../Features/Auth/authSlice.js"; import { createToast } from "../../../../Utils/toastUtils.jsx"; import { logger } from "../../../../Utils/Logger.js"; import { GenericDialog } from "../../../../Components/Dialog/genericDialog.jsx"; diff --git a/client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx b/client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx index 0fd5c0081..8a97486c7 100644 --- a/client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx +++ b/client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx @@ -1,4 +1,7 @@ -import { useCreateMonitor, useUpdateMonitor } from "../../../../../Hooks/v1/monitorHooks.js"; +import { + useCreateMonitor, + useUpdateMonitor, +} from "../../../../../Hooks/v1/monitorHooks.js"; const useInfrastructureSubmit = () => { const [createMonitor, isCreating] = useCreateMonitor(); const [updateMonitor, isUpdating] = useUpdateMonitor(); diff --git a/client/src/Pages/v1/PageSpeed/Monitors/Components/Card/index.jsx b/client/src/Pages/v1/PageSpeed/Monitors/Components/Card/index.jsx index db1f6e712..5857a7a7e 100644 --- a/client/src/Pages/v1/PageSpeed/Monitors/Components/Card/index.jsx +++ b/client/src/Pages/v1/PageSpeed/Monitors/Components/Card/index.jsx @@ -7,7 +7,10 @@ import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts"; import { useSelector } from "react-redux"; -import { formatDateWithTz, formatDurationSplit } from "../../../../../../Utils/timeUtils.js"; +import { + formatDateWithTz, + formatDurationSplit, +} from "../../../../../../Utils/timeUtils.js"; import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js"; import { useState } from "react"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index d27685ed6..d1788eddf 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -16,7 +16,12 @@ import { useState } from "react"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; import { useSelector, useDispatch } from "react-redux"; -import { setTimezone, setMode, setLanguage, setShowURL } from "../../../Features/UI/uiSlice.js"; +import { + setTimezone, + setMode, + setLanguage, + setShowURL, +} from "../../../Features/UI/uiSlice.js"; import SettingsStats from "./SettingsStats.jsx"; import { useFetchSettings, useSaveSettings } from "../../../Hooks/v1/settingsHooks.js"; From e29f7af85894c57d2e4f34a274b69acd621ca264 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 25 Sep 2025 10:21:31 -0700 Subject: [PATCH 023/400] add client build check --- .github/workflows/check-build.yml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml index 8417b823f..5ce8c9c26 100644 --- a/.github/workflows/check-build.yml +++ b/.github/workflows/check-build.yml @@ -4,6 +4,23 @@ on: workflow_dispatch: jobs: + build-client: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22 + + - name: Install client dependencies + working-directory: client + run: npm install + + - name: Check client build + working-directory: client + run: npm run build build-server: runs-on: ubuntu-latest steps: From 28ef465b3a5bd19b5eabd72002baa02bc3f3ffcc Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 25 Sep 2025 10:22:47 -0700 Subject: [PATCH 024/400] update action name --- .github/workflows/check-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/check-build.yml b/.github/workflows/check-build.yml index 5ce8c9c26..4d2de7041 100644 --- a/.github/workflows/check-build.yml +++ b/.github/workflows/check-build.yml @@ -1,4 +1,4 @@ -name: Build Check ( Server) +name: Build Check (Client & Server) on: pull_request: workflow_dispatch: From cb1e10f0a95fd671ad8fc227dccd754adcd6d655 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 25 Sep 2025 16:06:52 -0700 Subject: [PATCH 025/400] add v2 login page --- client/package-lock.json | 65 +++++++++- client/package.json | 6 +- .../Components/Inputs/TextInput/indexV2.tsx | 6 + client/src/Hooks/v2/UseApi.tsx | 52 ++++++++ client/src/Pages/v2/Auth/Login.tsx | 113 ++++++++++++++++++ client/src/Routes/index.jsx | 15 ++- client/src/Utils/ApiClient.ts | 21 ++++ client/src/Utils/Theme/constants.js | 1 - client/src/Utils/Theme/v2/palette.ts | 82 +++++++++++++ client/src/Utils/Theme/v2/theme.ts | 52 ++++++++ client/src/types/env.d.ts | 9 ++ client/src/types/mui.d.ts | 20 ++++ client/vite.config.ts | 6 + server/src/app.js | 2 +- 14 files changed, 445 insertions(+), 5 deletions(-) create mode 100644 client/src/Components/Inputs/TextInput/indexV2.tsx create mode 100644 client/src/Hooks/v2/UseApi.tsx create mode 100644 client/src/Pages/v2/Auth/Login.tsx create mode 100644 client/src/Utils/ApiClient.ts create mode 100644 client/src/Utils/Theme/v2/palette.ts create mode 100644 client/src/Utils/Theme/v2/theme.ts create mode 100644 client/src/types/env.d.ts create mode 100644 client/src/types/mui.d.ts diff --git a/client/package-lock.json b/client/package-lock.json index ee1ab1bc1..de297943a 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -11,6 +11,7 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@hello-pangea/dnd": "^18.0.0", + "@hookform/resolvers": "5.2.2", "@mui/icons-material": "6.4.11", "@mui/lab": "6.0.0-dev.240424162023-9968b4889d", "@mui/material": "6.4.11", @@ -26,6 +27,7 @@ "mui-color-input": "^6.0.0", "react": "18.3.1", "react-dom": "^18.2.0", + "react-hook-form": "7.63.0", "react-i18next": "^15.4.0", "react-icons": "5.5.0", "react-redux": "9.2.0", @@ -34,7 +36,9 @@ "react-toastify": "^10.0.5", "recharts": "2.15.2", "redux-persist": "6.0.0", - "vite-plugin-svgr": "^4.2.0" + "swr": "2.3.6", + "vite-plugin-svgr": "^4.2.0", + "zod": "4.1.11" }, "devDependencies": { "@types/react": "^18.2.66", @@ -653,6 +657,18 @@ "react-dom": "^18.0.0 || ^19.0.0" } }, + "node_modules/@hookform/resolvers": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "license": "MIT", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -2991,6 +3007,15 @@ "node": ">=0.4.0" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/doctrine": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", @@ -5343,6 +5368,22 @@ "react": "^18.3.1" } }, + "node_modules/react-hook-form": { + "version": "7.63.0", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz", + "integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, "node_modules/react-i18next": { "version": "15.5.1", "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.1.tgz", @@ -6141,6 +6182,19 @@ "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", "license": "MIT" }, + "node_modules/swr": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/swr/-/swr-2.3.6.tgz", + "integrity": "sha512-wfHRmHWk/isGNMwlLGlZX5Gzz/uTgo0o2IRuTMcf4CPuPFJZlq0rDaKUx+ozB5nBOReNV1kiOyzMfj+MBMikLw==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3", + "use-sync-external-store": "^1.4.0" + }, + "peerDependencies": { + "react": "^16.11.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, "node_modules/text-segmentation": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz", @@ -6639,6 +6693,15 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.11.tgz", + "integrity": "sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/client/package.json b/client/package.json index 9ebc99647..03d0cf6bc 100644 --- a/client/package.json +++ b/client/package.json @@ -16,6 +16,7 @@ "@emotion/react": "^11.13.3", "@emotion/styled": "^11.13.0", "@hello-pangea/dnd": "^18.0.0", + "@hookform/resolvers": "5.2.2", "@mui/icons-material": "6.4.11", "@mui/lab": "6.0.0-dev.240424162023-9968b4889d", "@mui/material": "6.4.11", @@ -31,6 +32,7 @@ "mui-color-input": "^6.0.0", "react": "18.3.1", "react-dom": "^18.2.0", + "react-hook-form": "7.63.0", "react-i18next": "^15.4.0", "react-icons": "5.5.0", "react-redux": "9.2.0", @@ -39,7 +41,9 @@ "react-toastify": "^10.0.5", "recharts": "2.15.2", "redux-persist": "6.0.0", - "vite-plugin-svgr": "^4.2.0" + "swr": "2.3.6", + "vite-plugin-svgr": "^4.2.0", + "zod": "4.1.11" }, "unusedDepencies": { "@solana/wallet-adapter-base": "0.9.25", diff --git a/client/src/Components/Inputs/TextInput/indexV2.tsx b/client/src/Components/Inputs/TextInput/indexV2.tsx new file mode 100644 index 000000000..bc1f144b6 --- /dev/null +++ b/client/src/Components/Inputs/TextInput/indexV2.tsx @@ -0,0 +1,6 @@ +import TextField from "@mui/material/TextField"; +import Box from "@mui/material/Box"; +import type { TextFieldProps } from "@mui/material"; +export const TextInput = (props: TextFieldProps) => { + return ; +}; diff --git a/client/src/Hooks/v2/UseApi.tsx b/client/src/Hooks/v2/UseApi.tsx new file mode 100644 index 000000000..fdb4e8a24 --- /dev/null +++ b/client/src/Hooks/v2/UseApi.tsx @@ -0,0 +1,52 @@ +import { useState } from "react"; +import useSWR from "swr"; +import type { SWRConfiguration } from "swr"; +import type { AxiosRequestConfig } from "axios"; +import { get, post } from "@/Utils/ApiClient"; // your axios wrapper + +// Generic fetcher for GET requests +const fetcher = async (url: string, config?: AxiosRequestConfig) => { + const res = await get(url, config); + return res.data; +}; + +export const useGet = ( + url: string, + axiosConfig?: AxiosRequestConfig, + swrConfig?: SWRConfiguration +) => { + const { data, error, isLoading, mutate } = useSWR( + url, + (url) => fetcher(url, axiosConfig), + swrConfig + ); + + return { + response: data ?? null, + loading: isLoading, + error: error?.message ?? null, + refetch: mutate, + }; +}; + +export const usePost = (endpoint: string) => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const postFn = async (body: B, config?: AxiosRequestConfig): Promise => { + setLoading(true); + setError(null); + + try { + const res = await post(endpoint, body, config); + return res.data; + } catch (err: any) { + setError(err?.message ?? "Unknown error"); + return null; + } finally { + setLoading(false); + } + }; + + return { post: postFn, loading, error }; +}; diff --git a/client/src/Pages/v2/Auth/Login.tsx b/client/src/Pages/v2/Auth/Login.tsx new file mode 100644 index 000000000..d1d4ec014 --- /dev/null +++ b/client/src/Pages/v2/Auth/Login.tsx @@ -0,0 +1,113 @@ +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { useTranslation } from "react-i18next"; +import { useForm, Controller } from "react-hook-form"; +import { useTheme } from "@mui/material/styles"; + +import { TextInput } from "@/Components/Inputs/TextInput/indexV2.tsx"; +import Button from "@mui/material/Button"; +import Stack from "@mui/material/Stack"; +import { usePost } from "@/Hooks/v2/UseApi"; +const schema = z.object({ + email: z.email("Invalid email address"), + password: z.string().min(6, "Password must be at least 6 characters"), +}); + +type FormData = z.infer; +type LoginData = { + username: string; + password: string; +}; + +const Login = () => { + const { t } = useTranslation(); + const theme = useTheme(); + const { post, loading, error } = usePost("/auth/login"); + + const { + handleSubmit, + control, + formState: { errors }, + } = useForm({ + resolver: zodResolver(schema), // ⬅️ connect Zod + defaultValues: { + email: "", + password: "", + }, + }); + + const onSubmit = async (data: FormData) => { + const result = await post(data); + if (result) { + console.log("Login successful:", result); + } else { + console.error("Login failed:", error); + } + }; + + return ( + + + ( + + )} + /> + ( + + )} + /> + + + + ); +}; + +export default Login; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 0de847cb7..06a4b1920 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -1,9 +1,12 @@ +import { useSelector } from "react-redux"; +import { lightTheme, darkTheme } from "@/Utils/Theme/v2/theme"; import { Navigate, Route, Routes as LibRoutes } from "react-router"; +import { ThemeProvider } from "@emotion/react"; import HomeLayout from "../Components/Layouts/HomeLayout"; import NotFound from "../Pages/v1/NotFound"; - // Auth import AuthLogin from "../Pages/v1/Auth/Login"; +import AuthLoginV2 from "@/Pages/v2/Auth/Login"; import AuthRegister from "../Pages/v1/Auth/Register/"; import AuthForgotPassword from "../Pages/v1/Auth/ForgotPassword"; import AuthCheckEmail from "../Pages/v1/Auth/CheckEmail"; @@ -54,6 +57,8 @@ import BulkImport from "../Pages/v1/Uptime/BulkImport"; import Logs from "../Pages/v1/Logs"; const Routes = () => { + const mode = useSelector((state) => state.ui.mode); + const v2Theme = mode === "light" ? lightTheme : darkTheme; const AdminCheckedRegister = withAdminCheck(AuthRegister); return ( @@ -214,6 +219,14 @@ const Routes = () => { path="/login" element={} /> + + + + } + /> ( + url: string, + config: AxiosRequestConfig = {} +): Promise> => api.get(url, config); + +export const post = ( + url: string, + data: any, + config: AxiosRequestConfig = {} +): Promise> => api.post(url, data, config); + +export default api; diff --git a/client/src/Utils/Theme/constants.js b/client/src/Utils/Theme/constants.js index 9df6665d0..45217fdeb 100644 --- a/client/src/Utils/Theme/constants.js +++ b/client/src/Utils/Theme/constants.js @@ -100,7 +100,6 @@ const newColors = { blueGray900: "#515151", blueBlueWave: "#1570EF", lightBlueWave: "#CDE2FF", - /* I changed green 100 and green 700. Need to change red and warning as well, and refactor the object following the structure */ green100: "#67cd78", green200: "#4B9B77", green400: "#079455", diff --git a/client/src/Utils/Theme/v2/palette.ts b/client/src/Utils/Theme/v2/palette.ts new file mode 100644 index 000000000..2875f750d --- /dev/null +++ b/client/src/Utils/Theme/v2/palette.ts @@ -0,0 +1,82 @@ +import { lighten, darken } from "@mui/material/styles"; + +const typographyBase = 13; + +export const typographyLevels = { + base: typographyBase, + xs: `${(typographyBase - 4) / 16}rem`, + s: `${(typographyBase - 2) / 16}rem`, + m: `${typographyBase / 16}rem`, + l: `${(typographyBase + 2) / 16}rem`, + xl: `${(typographyBase + 10) / 16}rem`, +}; + +const colors = { + offWhite: "#FEFEFE", + offBlack: "#131315", + gray0: "#FDFDFD", + gray10: "#F4F4FF", + gray50: "#F9F9F9", + gray100: "#F3F3F3", + gray200: "#EFEFEF", + gray250: "#DADADA", + gray500: "#A2A3A3", + gray900: "#1c1c1c", + blueGray50: "#E8F0FE", + blueGray500: "#475467", + blueGray600: "#344054", + blueGray800: "#1C2130", + blueGray900: "#515151", + blueBlueWave: "#1570EF", + lightBlueWave: "#CDE2FF", + green100: "#67cd78", + green200: "#4B9B77", + green400: "#079455", + green700: "#026513", + orange100: "#FD8F22", + orange200: "#D69A5D", + orange600: "#9B734B", + orange700: "#884605", + red100: "#F27C7C", + red400: "#D92020", + red600: "#9B4B4B", + red700: "#980303", +}; + +export const lightPalette = { + accent: { + main: colors.blueBlueWave, + light: lighten(colors.blueBlueWave, 0.2), + dark: darken(colors.blueBlueWave, 0.2), + contrastText: colors.offWhite, + }, + primary: { + main: colors.offWhite, + contrastText: colors.blueGray800, + contrastTextSecondary: colors.blueGray600, + }, + secondary: { + main: colors.gray200, + light: colors.lightBlueWave, + contrastText: colors.blueGray600, + }, +}; + +export const darkPalette = { + accent: { + main: colors.blueBlueWave, + light: lighten(colors.blueBlueWave, 0.2), + dark: darken(colors.blueBlueWave, 0.2), + contrastText: colors.offWhite, + }, + primary: { + main: colors.offBlack, + contrastText: colors.blueGray50, + contrastTextSecondary: colors.gray200, + }, + secondary: { + main: "#313131", + light: colors.lightBlueWave, + contrastText: colors.gray200, + }, +}; diff --git a/client/src/Utils/Theme/v2/theme.ts b/client/src/Utils/Theme/v2/theme.ts new file mode 100644 index 000000000..ce81ea2fd --- /dev/null +++ b/client/src/Utils/Theme/v2/theme.ts @@ -0,0 +1,52 @@ +import { createTheme } from "@mui/material"; +import { lightPalette, darkPalette, typographyLevels } from "./palette"; +const fontFamilyPrimary = '"Inter" , sans-serif'; + +export const theme = (mode: string, palette: any) => + createTheme({ + spacing: 2, + palette: { + mode: mode, + ...palette, + }, + typography: { + fontFamily: fontFamilyPrimary, + fontSize: typographyLevels.base, + }, + + components: { + MuiFormLabel: { + styleOverrides: { + root: ({ theme }) => ({ + fontSize: typographyLevels.base, + "&.Mui-focused": { + color: theme.palette.secondary.contrastText, + }, + }), + }, + }, + MuiInputLabel: { + styleOverrides: { + root: ({ theme }) => ({ + top: `-${theme.spacing(4)}`, + "&.MuiInputLabel-shrink": { + top: 0, + }, + }), + }, + }, + MuiTextField: { + styleOverrides: { + root: ({ theme }) => ({ + "& .MuiOutlinedInput-root": { + height: 34, + fontSize: typographyLevels.base, + }, + }), + }, + }, + }, + }); + +export const lightTheme = createTheme(theme("light", lightPalette)); +export const darkTheme = createTheme(theme("dark", darkPalette)); diff --git a/client/src/types/env.d.ts b/client/src/types/env.d.ts new file mode 100644 index 000000000..e2bce23dd --- /dev/null +++ b/client/src/types/env.d.ts @@ -0,0 +1,9 @@ +/// + +interface ImportMetaEnv { + readonly VITE_APP_API_V2_BASE_URL?: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +} diff --git a/client/src/types/mui.d.ts b/client/src/types/mui.d.ts new file mode 100644 index 000000000..851ffea70 --- /dev/null +++ b/client/src/types/mui.d.ts @@ -0,0 +1,20 @@ +import "@mui/material/Button"; + +declare module "@mui/material/styles" { + interface Palette { + accent: Palette["primary"]; + } + interface PaletteOptions { + accent?: PaletteOptions["primary"]; + } + + interface PaletteColor { + contrastTextSecondary?: string; + } +} + +declare module "@mui/material/Button" { + interface ButtonPropsColorOverrides { + accent: true; + } +} diff --git a/client/vite.config.ts b/client/vite.config.ts index 66630e88c..a99c1a254 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,6 +1,7 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import svgr from "vite-plugin-svgr"; +import path from "path"; export default defineConfig(({}) => { let version = "3.2.0"; @@ -8,6 +9,11 @@ export default defineConfig(({}) => { return { base: "/", plugins: [svgr(), react()], + resolve: { + alias: { + "@": path.resolve(__dirname, "src"), + }, + }, optimizeDeps: { include: ["@mui/material/Tooltip", "@emotion/styled"], }, diff --git a/server/src/app.js b/server/src/app.js index 253baee88..4578de896 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -27,7 +27,7 @@ export const createApp = ({ services, controllers, envSettings, frontendPath, op cors({ origin: allowedOrigin, methods: "GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS", - allowedHeaders: "*", + allowedHeaders: ["Content-Type", "Authorization"], credentials: true, }) ); From 0c5b9a863b5531417c2fce6b25df4ac9ea400b77 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 26 Sep 2025 14:16:59 -0700 Subject: [PATCH 026/400] add loading --- client/src/Pages/v2/Auth/Login.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/Pages/v2/Auth/Login.tsx b/client/src/Pages/v2/Auth/Login.tsx index d1d4ec014..6a76fdf76 100644 --- a/client/src/Pages/v2/Auth/Login.tsx +++ b/client/src/Pages/v2/Auth/Login.tsx @@ -99,6 +99,7 @@ const Login = () => { /> + {error && {error}} + + + ); +}; + +export default Register; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 06a4b1920..39959bc1c 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -8,6 +8,7 @@ import NotFound from "../Pages/v1/NotFound"; import AuthLogin from "../Pages/v1/Auth/Login"; import AuthLoginV2 from "@/Pages/v2/Auth/Login"; import AuthRegister from "../Pages/v1/Auth/Register/"; +import AuthRegisterV2 from "@/Pages/v2/Auth/Register"; import AuthForgotPassword from "../Pages/v1/Auth/ForgotPassword"; import AuthCheckEmail from "../Pages/v1/Auth/CheckEmail"; import AuthSetNewPassword from "../Pages/v1/Auth/SetNewPassword"; @@ -232,6 +233,14 @@ const Routes = () => { path="/register" element={} /> + + + + } + /> Date: Fri, 26 Sep 2025 15:31:55 -0700 Subject: [PATCH 033/400] move v2 routes to v2 router --- client/src/Routes/index.jsx | 27 +++++++-------------------- client/src/Routes/v2router.tsx | 29 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 20 deletions(-) create mode 100644 client/src/Routes/v2router.tsx diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 39959bc1c..2e4fd9c50 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -1,14 +1,11 @@ import { useSelector } from "react-redux"; import { lightTheme, darkTheme } from "@/Utils/Theme/v2/theme"; import { Navigate, Route, Routes as LibRoutes } from "react-router"; -import { ThemeProvider } from "@emotion/react"; import HomeLayout from "../Components/Layouts/HomeLayout"; import NotFound from "../Pages/v1/NotFound"; // Auth import AuthLogin from "../Pages/v1/Auth/Login"; -import AuthLoginV2 from "@/Pages/v2/Auth/Login"; import AuthRegister from "../Pages/v1/Auth/Register/"; -import AuthRegisterV2 from "@/Pages/v2/Auth/Register"; import AuthForgotPassword from "../Pages/v1/Auth/ForgotPassword"; import AuthCheckEmail from "../Pages/v1/Auth/CheckEmail"; import AuthSetNewPassword from "../Pages/v1/Auth/SetNewPassword"; @@ -57,12 +54,18 @@ import withAdminCheck from "../Components/HOC/withAdminCheck"; import BulkImport from "../Pages/v1/Uptime/BulkImport"; import Logs from "../Pages/v1/Logs"; +import V2Routes from "@/Routes/v2router"; + const Routes = () => { const mode = useSelector((state) => state.ui.mode); - const v2Theme = mode === "light" ? lightTheme : darkTheme; const AdminCheckedRegister = withAdminCheck(AuthRegister); return ( + } + /> + { path="/login" element={} /> - - - - } - /> } /> - - - - } - /> { + const v2Theme = mode === "light" ? lightTheme : darkTheme; + + return ( + + + } + /> + } + /> + + + ); +}; + +export default V2Routes; From 1d5f2427ed692658b9d86111eec95334fc087fbf Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 26 Sep 2025 16:01:05 -0700 Subject: [PATCH 034/400] add root layout --- .../src/Components/v2/Layouts/RootLayout.tsx | 50 +++++++++++++++++++ client/src/Routes/index.jsx | 27 +++------- client/src/Routes/v2router.tsx | 39 +++++++++++++++ 3 files changed, 96 insertions(+), 20 deletions(-) create mode 100644 client/src/Components/v2/Layouts/RootLayout.tsx create mode 100644 client/src/Routes/v2router.tsx diff --git a/client/src/Components/v2/Layouts/RootLayout.tsx b/client/src/Components/v2/Layouts/RootLayout.tsx new file mode 100644 index 000000000..5f78600f4 --- /dev/null +++ b/client/src/Components/v2/Layouts/RootLayout.tsx @@ -0,0 +1,50 @@ +import { useState, useEffect } from "react"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useTheme } from "@emotion/react"; + +import { Outlet } from "react-router"; +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; + +const COLLAPSED_WIDTH = 50; +const EXPANDED_WIDTH = 250; + +const SideBar = () => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + + const [collapsed, setCollapsed] = useState(false); + useEffect(() => { + setCollapsed(isSmall); + }, [isSmall]); + return ( + + setCollapsed(!collapsed)} + > + Sidebar Content + + + ); +}; + +const RootLayout = ({ mode = "light" }) => { + return ( + + + + + ); +}; + +export default RootLayout; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 39959bc1c..2e4fd9c50 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -1,14 +1,11 @@ import { useSelector } from "react-redux"; import { lightTheme, darkTheme } from "@/Utils/Theme/v2/theme"; import { Navigate, Route, Routes as LibRoutes } from "react-router"; -import { ThemeProvider } from "@emotion/react"; import HomeLayout from "../Components/Layouts/HomeLayout"; import NotFound from "../Pages/v1/NotFound"; // Auth import AuthLogin from "../Pages/v1/Auth/Login"; -import AuthLoginV2 from "@/Pages/v2/Auth/Login"; import AuthRegister from "../Pages/v1/Auth/Register/"; -import AuthRegisterV2 from "@/Pages/v2/Auth/Register"; import AuthForgotPassword from "../Pages/v1/Auth/ForgotPassword"; import AuthCheckEmail from "../Pages/v1/Auth/CheckEmail"; import AuthSetNewPassword from "../Pages/v1/Auth/SetNewPassword"; @@ -57,12 +54,18 @@ import withAdminCheck from "../Components/HOC/withAdminCheck"; import BulkImport from "../Pages/v1/Uptime/BulkImport"; import Logs from "../Pages/v1/Logs"; +import V2Routes from "@/Routes/v2router"; + const Routes = () => { const mode = useSelector((state) => state.ui.mode); - const v2Theme = mode === "light" ? lightTheme : darkTheme; const AdminCheckedRegister = withAdminCheck(AuthRegister); return ( + } + /> + { path="/login" element={} /> - - - - } - /> } /> - - - - } - /> { + const v2Theme = mode === "light" ? lightTheme : darkTheme; + + return ( + + + } + /> + } + /> + } + > + Test Page} + /> + + + + ); +}; + +export default V2Routes; From 4d254b004dd8440ec08bbfc01f04ffcbd229e190 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sat, 27 Sep 2025 14:38:14 -0700 Subject: [PATCH 035/400] add gitignore --- .gitignore | 1 + client/src/Components/v2/Layouts/RootLayout.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 172702b40..997dff9c5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ +.idea .vscode .VSCodeCounter \ No newline at end of file diff --git a/client/src/Components/v2/Layouts/RootLayout.tsx b/client/src/Components/v2/Layouts/RootLayout.tsx index 5f78600f4..4082242ef 100644 --- a/client/src/Components/v2/Layouts/RootLayout.tsx +++ b/client/src/Components/v2/Layouts/RootLayout.tsx @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import useMediaQuery from "@mui/material/useMediaQuery"; -import { useTheme } from "@emotion/react"; +import { useTheme } from "@mui/material/styles"; import { Outlet } from "react-router"; import Stack from "@mui/material/Stack"; From 09fbc6b0b267406ffa6cf256cc2a09fdbe6b4294 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sat, 27 Sep 2025 14:48:05 -0700 Subject: [PATCH 036/400] remove unused mode --- client/src/Components/v2/Layouts/RootLayout.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/Components/v2/Layouts/RootLayout.tsx b/client/src/Components/v2/Layouts/RootLayout.tsx index 4082242ef..be2f42e30 100644 --- a/client/src/Components/v2/Layouts/RootLayout.tsx +++ b/client/src/Components/v2/Layouts/RootLayout.tsx @@ -35,7 +35,7 @@ const SideBar = () => { ); }; -const RootLayout = ({ mode = "light" }) => { +const RootLayout = () => { return ( Date: Sat, 27 Sep 2025 14:54:55 -0700 Subject: [PATCH 037/400] move components to v1 and v2 dirs --- client/src/App.jsx | 2 +- .../Components/{ => v1}/ActionsMenu/index.jsx | 8 ++-- .../src/Components/{ => v1}/Alert/index.css | 0 .../src/Components/{ => v1}/Alert/index.jsx | 0 .../Components/{ => v1}/Animated/PulseDot.jsx | 0 .../Components/{ => v1}/ArrowLeft/index.jsx | 6 +-- .../Components/{ => v1}/ArrowRight/index.jsx | 4 +- .../src/Components/{ => v1}/Avatar/index.css | 0 .../src/Components/{ => v1}/Avatar/index.jsx | 0 .../Components/{ => v1}/Breadcrumbs/index.css | 0 .../Components/{ => v1}/Breadcrumbs/index.jsx | 2 +- .../{ => v1}/Buttons/RoundGradientButton.jsx | 0 .../{ => v1}/Charts/AreaChart/index.jsx | 2 +- .../{ => v1}/Charts/BarChart/index.css | 0 .../{ => v1}/Charts/BarChart/index.jsx | 2 +- .../{ => v1}/Charts/ChartBox/EmptyView.jsx | 2 +- .../{ => v1}/Charts/ChartBox/index.jsx | 4 +- .../{ => v1}/Charts/CustomGauge/index.css | 0 .../{ => v1}/Charts/CustomGauge/index.jsx | 0 .../Charts/DePINStatusPageBarChart/index.jsx | 2 +- .../{ => v1}/Charts/LegendBox/index.jsx | 2 +- .../Charts/MonitorDetailsAreaChart/index.css | 0 .../Charts/MonitorDetailsAreaChart/index.jsx | 4 +- .../Charts/StatusPageBarChart/index.jsx | 2 +- .../Charts/Utils/chartUtilFunctions.js | 0 .../{ => v1}/Charts/Utils/chartUtils.jsx | 4 +- .../{ => v1}/Charts/Utils/gradientUtils.jsx | 0 .../src/Components/{ => v1}/Check/Check.jsx | 4 +- .../src/Components/{ => v1}/Check/check.css | 0 .../{ => v1}/CircularCount/index.jsx | 0 .../src/Components/{ => v1}/Common/AppBar.jsx | 2 +- .../src/Components/{ => v1}/Common/Footer.jsx | 0 .../Components/{ => v1}/ConfigBox/index.jsx | 0 .../Components/{ => v1}/ConfigRow/index.jsx | 2 +- .../{ => v1}/Dialog/genericDialog.jsx | 0 .../src/Components/{ => v1}/Dialog/index.jsx | 2 +- client/src/Components/{ => v1}/Dot/index.jsx | 0 .../Fallback/FallBackActionButtons.jsx | 0 .../{ => v1}/Fallback/FallbackBackground.jsx | 6 +-- .../{ => v1}/Fallback/FallbackCheckList.jsx | 2 +- .../{ => v1}/Fallback/FallbackContainer.jsx | 0 .../Fallback/FallbackPageSpeedWarning.jsx | 2 +- .../{ => v1}/Fallback/FallbackTitle.jsx | 0 .../Components/{ => v1}/Fallback/index.css | 0 .../Components/{ => v1}/Fallback/index.jsx | 10 ++--- .../{ => v1}/FilterHeader/index.jsx | 0 .../{ => v1}/GenericFallback/NetworkError.jsx | 0 .../{ => v1}/GenericFallback/index.jsx | 6 +-- .../{ => v1}/HOC/withAdminCheck.jsx | 4 +- .../src/Components/{ => v1}/Heading/index.jsx | 0 client/src/Components/{ => v1}/Host/index.jsx | 2 +- .../{ => v1}/HttpStatusLabel/index.jsx | 2 +- .../Components/{ => v1}/I18nLoader/index.jsx | 2 +- .../src/Components/{ => v1}/IconBox/index.jsx | 0 .../src/Components/{ => v1}/Image/index.jsx | 0 .../src/Components/{ => v1}/InfoBox/index.jsx | 4 +- .../{ => v1}/Inputs/Checkbox/index.css | 0 .../{ => v1}/Inputs/Checkbox/index.jsx | 4 +- .../{ => v1}/Inputs/ColorPicker/index.jsx | 0 .../{ => v1}/Inputs/FieldWrapper/index.jsx | 0 .../{ => v1}/Inputs/ImageUpload/index.jsx | 4 +- .../{ => v1}/Inputs/Radio/index.css | 0 .../{ => v1}/Inputs/Radio/index.jsx | 2 +- .../{ => v1}/Inputs/Search/index.jsx | 4 +- .../{ => v1}/Inputs/Select/index.css | 0 .../{ => v1}/Inputs/Select/index.jsx | 2 +- .../Inputs/TextInput/Adornments/index.jsx | 2 +- .../{ => v1}/Inputs/TextInput/index.jsx | 2 +- .../src/Components/{ => v1}/Label/index.css | 0 .../src/Components/{ => v1}/Label/index.jsx | 0 .../{ => v1}/Layouts/AppLayout/index.jsx | 2 +- .../{ => v1}/Layouts/HomeLayout/index.css | 0 .../{ => v1}/Layouts/HomeLayout/index.jsx | 2 +- client/src/Components/{ => v1}/Link/index.jsx | 0 client/src/Components/{ => v1}/Link/link.css | 0 .../{ => v1}/MonitorActions/index.jsx | 4 +- .../{ => v1}/MonitorCountHeader/index.jsx | 2 +- .../{ => v1}/MonitorCountHeader/skeleton.jsx | 0 .../{ => v1}/MonitorCreateHeader/index.jsx | 2 +- .../{ => v1}/MonitorCreateHeader/skeleton.jsx | 0 .../MonitorDetailsControlHeader/index.jsx | 10 ++--- .../MonitorDetailsControlHeader/skeleton.jsx | 0 .../MonitorDetailsControlHeader/status.jsx | 10 ++--- .../{ => v1}/MonitorTimeFrameHeader/index.jsx | 2 +- .../MonitorTimeFrameHeader/skeleton.jsx | 0 .../{ => v1}/NotificationConfig/index.jsx | 2 +- .../NotificationIntegrationModal.jsx | 6 +-- .../Components/TabComponent.jsx | 4 +- .../Components/TabPanel.jsx | 0 .../Hooks/useNotification.js | 4 +- .../{ => v1}/PageStateWrapper/index.jsx | 6 +-- .../{ => v1}/ProgressBars/index.css | 0 .../{ => v1}/ProgressBars/index.jsx | 0 .../{ => v1}/ProgressStepper/index.css | 0 .../{ => v1}/ProgressStepper/index.jsx | 0 .../{ => v1}/ProtectedRoute/index.jsx | 0 .../{ => v1}/RoleProtectedRoute/index.jsx | 0 .../{ => v1}/ShareComponent/index.jsx | 0 .../Sidebar/components/authFooter.jsx | 8 ++-- .../Sidebar/components/collapseButton.jsx | 6 +-- .../{ => v1}/Sidebar/components/logo.jsx | 0 .../{ => v1}/Sidebar/components/navItem.jsx | 0 .../src/Components/{ => v1}/Sidebar/index.jsx | 42 +++++++++---------- .../{ => v1}/Skeletons/FullPage/index.jsx | 0 .../{ => v1}/StandardContainer/index.jsx | 0 .../Components/{ => v1}/StarPrompt/index.jsx | 2 +- .../src/Components/{ => v1}/StatBox/index.jsx | 4 +- .../Components/{ => v1}/StatusBoxes/index.jsx | 2 +- .../{ => v1}/StatusBoxes/skeleton.jsx | 0 .../Components/{ => v1}/Subheader/index.jsx | 0 client/src/Components/{ => v1}/Tab/index.jsx | 0 .../Table/TablePagination/Actions/index.jsx | 4 +- .../{ => v1}/Table/TablePagination/index.jsx | 4 +- .../Components/{ => v1}/Table/TableUtils.js | 0 .../src/Components/{ => v1}/Table/index.jsx | 2 +- .../Components/{ => v1}/Table/skeleton.jsx | 0 .../Components/{ => v1}/TextLink/index.jsx | 0 .../{ => v1}/ThemeSwitch/SunAndMoonIcon.jsx | 0 .../Components/{ => v1}/ThemeSwitch/index.css | 0 .../Components/{ => v1}/ThemeSwitch/index.jsx | 4 +- client/src/Components/{ => v1}/Toast/body.jsx | 0 .../src/Components/{ => v1}/Toast/index.jsx | 2 +- .../indexV2.tsx => v2/Inputs/TextInput.tsx} | 0 client/src/Pages/v1/About/index.jsx | 4 +- .../src/Pages/v1/Account/EditUser/index.jsx | 6 +-- .../components/AddTeamMember/index.jsx | 8 ++-- .../v1/Account/components/PasswordPanel.jsx | 6 +-- .../v1/Account/components/ProfilePanel.jsx | 10 ++--- .../v1/Account/components/RoleTable/index.jsx | 2 +- .../Pages/v1/Account/components/TeamPanel.jsx | 8 ++-- client/src/Pages/v1/Account/index.jsx | 2 +- client/src/Pages/v1/Auth/CheckEmail.jsx | 2 +- client/src/Pages/v1/Auth/ForgotPassword.jsx | 4 +- client/src/Pages/v1/Auth/Login/index.jsx | 6 +-- .../Pages/v1/Auth/NewPasswordConfirmed.jsx | 2 +- client/src/Pages/v1/Auth/Register/index.jsx | 2 +- client/src/Pages/v1/Auth/SetNewPassword.jsx | 8 ++-- .../Pages/v1/Auth/components/AuthHeader.jsx | 2 +- .../v1/Auth/components/PasswordTooltip.jsx | 2 +- .../Components/IncidentTable/index.jsx | 14 +++---- .../Components/OptionsHeader/index.jsx | 2 +- client/src/Pages/v1/Incidents/index.jsx | 6 +-- .../Create/Components/CustomAlertsSection.jsx | 2 +- .../Components/CustomThreshold/index.jsx | 4 +- .../Components/MonitorActionButtons.jsx | 2 +- .../Create/Components/MonitorStatusHeader.jsx | 2 +- .../Pages/v1/Infrastructure/Create/index.jsx | 16 +++---- .../AreaChartBoxes/InfraAreaChart.jsx | 2 +- .../Components/AreaChartBoxes/index.jsx | 2 +- .../Details/Components/GaugeBoxes/Gauge.jsx | 2 +- .../Components/NetworkStats/NetworkCharts.jsx | 2 +- .../NetworkStats/NetworkStatBoxes.jsx | 4 +- .../Details/Components/NetworkStats/index.jsx | 4 +- .../Details/Components/StatusBoxes/index.jsx | 4 +- .../Pages/v1/Infrastructure/Details/index.jsx | 10 ++--- .../Monitors/Components/Filters/index.jsx | 2 +- .../Components/MonitorsTable/index.jsx | 8 ++-- .../Components/MonitorsTableMenu/index.jsx | 2 +- .../v1/Infrastructure/Monitors/index.jsx | 10 ++--- .../Diagnostics/components/gauges/index.jsx | 2 +- .../src/Pages/v1/Logs/Diagnostics/index.jsx | 4 +- client/src/Pages/v1/Logs/Logs/index.jsx | 2 +- .../Queue/components/FailedJobTable/index.jsx | 2 +- .../Logs/Queue/components/JobTable/index.jsx | 4 +- .../Logs/Queue/components/Metrics/index.jsx | 4 +- .../Queue/components/MetricsTable/index.jsx | 2 +- client/src/Pages/v1/Logs/index.jsx | 2 +- .../Components/ConfigSelect/index.jsx | 2 +- .../Components/MonitorsConfig/index.jsx | 4 +- .../Maintenance/CreateMaintenance/index.jsx | 8 ++-- .../MaintenanceTable/ActionsMenu/index.jsx | 2 +- .../v1/Maintenance/MaintenanceTable/index.jsx | 6 +-- client/src/Pages/v1/Maintenance/index.jsx | 4 +- .../Pages/v1/Notifications/create/index.jsx | 10 ++--- client/src/Pages/v1/Notifications/index.jsx | 6 +-- .../src/Pages/v1/PageSpeed/Create/index.jsx | 18 ++++---- .../Components/Charts/AreaChartLegend.jsx | 4 +- .../Components/Charts/PieChartLegend.jsx | 2 +- .../Components/PageSpeedAreaChart/index.jsx | 2 +- .../Components/PageSpeedStatusBoxes/index.jsx | 4 +- .../Components/PerformanceReport/index.jsx | 2 +- .../src/Pages/v1/PageSpeed/Details/index.jsx | 8 ++-- .../Monitors/Components/Card/index.jsx | 4 +- .../src/Pages/v1/PageSpeed/Monitors/index.jsx | 10 ++--- client/src/Pages/v1/ServerUnreachable.jsx | 4 +- .../src/Pages/v1/Settings/SettingsAbout.jsx | 4 +- .../v1/Settings/SettingsDemoMonitors.jsx | 4 +- .../src/Pages/v1/Settings/SettingsEmail.jsx | 8 ++-- .../v1/Settings/SettingsGlobalThresholds.jsx | 4 +- .../Pages/v1/Settings/SettingsPagespeed.jsx | 6 +-- .../src/Pages/v1/Settings/SettingsStats.jsx | 6 +-- .../Pages/v1/Settings/SettingsTimeZone.jsx | 4 +- client/src/Pages/v1/Settings/SettingsUI.jsx | 4 +- client/src/Pages/v1/Settings/SettingsURL.jsx | 4 +- client/src/Pages/v1/Settings/index.jsx | 2 +- .../Create/Components/Progress/index.jsx | 2 +- .../Create/Components/Tabs/ConfigStack.jsx | 2 +- .../Create/Components/Tabs/Content.jsx | 4 +- .../Create/Components/Tabs/Settings.jsx | 12 +++--- .../Create/Components/Tabs/index.jsx | 2 +- .../src/Pages/v1/StatusPage/Create/index.jsx | 6 +-- .../Components/ControlsHeader/index.jsx | 4 +- .../Status/Components/MonitorsList/index.jsx | 6 +-- .../src/Pages/v1/StatusPage/Status/index.jsx | 6 +-- .../Components/StatusPagesTable/index.jsx | 4 +- .../Pages/v1/StatusPage/StatusPages/index.jsx | 6 +-- .../src/Pages/v1/Uptime/BulkImport/index.jsx | 4 +- client/src/Pages/v1/Uptime/Create/index.jsx | 20 ++++----- .../Details/Components/ChartBoxes/index.jsx | 2 +- .../Components/Charts/ResponseTimeChart.jsx | 4 +- .../Components/ResponseTable/index.jsx | 8 ++-- .../Components/UptimeStatusBoxes/index.jsx | 4 +- client/src/Pages/v1/Uptime/Details/index.jsx | 8 ++-- .../Monitors/Components/Filter/index.jsx | 2 +- .../Components/SearchComponent/index.jsx | 2 +- .../Components/UptimeDataTable/index.jsx | 12 +++--- client/src/Pages/v1/Uptime/Monitors/index.jsx | 10 ++--- client/src/Pages/v2/Auth/Login.tsx | 2 +- client/src/Pages/v2/Auth/Register.tsx | 2 +- client/src/Routes/index.jsx | 8 ++-- client/src/Utils/toastUtils.jsx | 2 +- client/src/main.jsx | 2 +- 222 files changed, 370 insertions(+), 370 deletions(-) rename client/src/Components/{ => v1}/ActionsMenu/index.jsx (94%) rename client/src/Components/{ => v1}/Alert/index.css (100%) rename client/src/Components/{ => v1}/Alert/index.jsx (100%) rename client/src/Components/{ => v1}/Animated/PulseDot.jsx (100%) rename client/src/Components/{ => v1}/ArrowLeft/index.jsx (70%) rename client/src/Components/{ => v1}/ArrowRight/index.jsx (73%) rename client/src/Components/{ => v1}/Avatar/index.css (100%) rename client/src/Components/{ => v1}/Avatar/index.jsx (100%) rename client/src/Components/{ => v1}/Breadcrumbs/index.css (100%) rename client/src/Components/{ => v1}/Breadcrumbs/index.jsx (97%) rename client/src/Components/{ => v1}/Buttons/RoundGradientButton.jsx (100%) rename client/src/Components/{ => v1}/Charts/AreaChart/index.jsx (98%) rename client/src/Components/{ => v1}/Charts/BarChart/index.css (100%) rename client/src/Components/{ => v1}/Charts/BarChart/index.jsx (98%) rename client/src/Components/{ => v1}/Charts/ChartBox/EmptyView.jsx (98%) rename client/src/Components/{ => v1}/Charts/ChartBox/index.jsx (95%) rename client/src/Components/{ => v1}/Charts/CustomGauge/index.css (100%) rename client/src/Components/{ => v1}/Charts/CustomGauge/index.jsx (100%) rename client/src/Components/{ => v1}/Charts/DePINStatusPageBarChart/index.jsx (98%) rename client/src/Components/{ => v1}/Charts/LegendBox/index.jsx (95%) rename client/src/Components/{ => v1}/Charts/MonitorDetailsAreaChart/index.css (100%) rename client/src/Components/{ => v1}/Charts/MonitorDetailsAreaChart/index.jsx (97%) rename client/src/Components/{ => v1}/Charts/StatusPageBarChart/index.jsx (98%) rename client/src/Components/{ => v1}/Charts/Utils/chartUtilFunctions.js (100%) rename client/src/Components/{ => v1}/Charts/Utils/chartUtils.jsx (98%) rename client/src/Components/{ => v1}/Charts/Utils/gradientUtils.jsx (100%) rename client/src/Components/{ => v1}/Check/Check.jsx (93%) rename client/src/Components/{ => v1}/Check/check.css (100%) rename client/src/Components/{ => v1}/CircularCount/index.jsx (100%) rename client/src/Components/{ => v1}/Common/AppBar.jsx (99%) rename client/src/Components/{ => v1}/Common/Footer.jsx (100%) rename client/src/Components/{ => v1}/ConfigBox/index.jsx (100%) rename client/src/Components/{ => v1}/ConfigRow/index.jsx (96%) rename client/src/Components/{ => v1}/Dialog/genericDialog.jsx (100%) rename client/src/Components/{ => v1}/Dialog/index.jsx (95%) rename client/src/Components/{ => v1}/Dot/index.jsx (100%) rename client/src/Components/{ => v1}/Fallback/FallBackActionButtons.jsx (100%) rename client/src/Components/{ => v1}/Fallback/FallbackBackground.jsx (78%) rename client/src/Components/{ => v1}/Fallback/FallbackCheckList.jsx (95%) rename client/src/Components/{ => v1}/Fallback/FallbackContainer.jsx (100%) rename client/src/Components/{ => v1}/Fallback/FallbackPageSpeedWarning.jsx (97%) rename client/src/Components/{ => v1}/Fallback/FallbackTitle.jsx (100%) rename client/src/Components/{ => v1}/Fallback/index.css (100%) rename client/src/Components/{ => v1}/Fallback/index.jsx (87%) rename client/src/Components/{ => v1}/FilterHeader/index.jsx (100%) rename client/src/Components/{ => v1}/GenericFallback/NetworkError.jsx (100%) rename client/src/Components/{ => v1}/GenericFallback/index.jsx (89%) rename client/src/Components/{ => v1}/HOC/withAdminCheck.jsx (91%) rename client/src/Components/{ => v1}/Heading/index.jsx (100%) rename client/src/Components/{ => v1}/Host/index.jsx (97%) rename client/src/Components/{ => v1}/HttpStatusLabel/index.jsx (96%) rename client/src/Components/{ => v1}/I18nLoader/index.jsx (88%) rename client/src/Components/{ => v1}/IconBox/index.jsx (100%) rename client/src/Components/{ => v1}/Image/index.jsx (100%) rename client/src/Components/{ => v1}/InfoBox/index.jsx (88%) rename client/src/Components/{ => v1}/Inputs/Checkbox/index.css (100%) rename client/src/Components/{ => v1}/Inputs/Checkbox/index.jsx (95%) rename client/src/Components/{ => v1}/Inputs/ColorPicker/index.jsx (100%) rename client/src/Components/{ => v1}/Inputs/FieldWrapper/index.jsx (100%) rename client/src/Components/{ => v1}/Inputs/ImageUpload/index.jsx (98%) rename client/src/Components/{ => v1}/Inputs/Radio/index.css (100%) rename client/src/Components/{ => v1}/Inputs/Radio/index.jsx (96%) rename client/src/Components/{ => v1}/Inputs/Search/index.jsx (98%) rename client/src/Components/{ => v1}/Inputs/Select/index.css (100%) rename client/src/Components/{ => v1}/Inputs/Select/index.jsx (98%) rename client/src/Components/{ => v1}/Inputs/TextInput/Adornments/index.jsx (96%) rename client/src/Components/{ => v1}/Inputs/TextInput/index.jsx (98%) rename client/src/Components/{ => v1}/Label/index.css (100%) rename client/src/Components/{ => v1}/Label/index.jsx (100%) rename client/src/Components/{ => v1}/Layouts/AppLayout/index.jsx (91%) rename client/src/Components/{ => v1}/Layouts/HomeLayout/index.css (100%) rename client/src/Components/{ => v1}/Layouts/HomeLayout/index.jsx (88%) rename client/src/Components/{ => v1}/Link/index.jsx (100%) rename client/src/Components/{ => v1}/Link/link.css (100%) rename client/src/Components/{ => v1}/MonitorActions/index.jsx (95%) rename client/src/Components/{ => v1}/MonitorCountHeader/index.jsx (95%) rename client/src/Components/{ => v1}/MonitorCountHeader/skeleton.jsx (100%) rename client/src/Components/{ => v1}/MonitorCreateHeader/index.jsx (94%) rename client/src/Components/{ => v1}/MonitorCreateHeader/skeleton.jsx (100%) rename client/src/Components/{ => v1}/MonitorDetailsControlHeader/index.jsx (92%) rename client/src/Components/{ => v1}/MonitorDetailsControlHeader/skeleton.jsx (100%) rename client/src/Components/{ => v1}/MonitorDetailsControlHeader/status.jsx (83%) rename client/src/Components/{ => v1}/MonitorTimeFrameHeader/index.jsx (97%) rename client/src/Components/{ => v1}/MonitorTimeFrameHeader/skeleton.jsx (100%) rename client/src/Components/{ => v1}/NotificationConfig/index.jsx (98%) rename client/src/Components/{ => v1}/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx (98%) rename client/src/Components/{ => v1}/NotificationIntegrationModal/Components/TabComponent.jsx (95%) rename client/src/Components/{ => v1}/NotificationIntegrationModal/Components/TabPanel.jsx (100%) rename client/src/Components/{ => v1}/NotificationIntegrationModal/Hooks/useNotification.js (96%) rename client/src/Components/{ => v1}/PageStateWrapper/index.jsx (90%) rename client/src/Components/{ => v1}/ProgressBars/index.css (100%) rename client/src/Components/{ => v1}/ProgressBars/index.jsx (100%) rename client/src/Components/{ => v1}/ProgressStepper/index.css (100%) rename client/src/Components/{ => v1}/ProgressStepper/index.jsx (100%) rename client/src/Components/{ => v1}/ProtectedRoute/index.jsx (100%) rename client/src/Components/{ => v1}/RoleProtectedRoute/index.jsx (100%) rename client/src/Components/{ => v1}/ShareComponent/index.jsx (100%) rename client/src/Components/{ => v1}/Sidebar/components/authFooter.jsx (96%) rename client/src/Components/{ => v1}/Sidebar/components/collapseButton.jsx (88%) rename client/src/Components/{ => v1}/Sidebar/components/logo.jsx (100%) rename client/src/Components/{ => v1}/Sidebar/components/navItem.jsx (100%) rename client/src/Components/{ => v1}/Sidebar/index.jsx (75%) rename client/src/Components/{ => v1}/Skeletons/FullPage/index.jsx (100%) rename client/src/Components/{ => v1}/StandardContainer/index.jsx (100%) rename client/src/Components/{ => v1}/StarPrompt/index.jsx (97%) rename client/src/Components/{ => v1}/StatBox/index.jsx (97%) rename client/src/Components/{ => v1}/StatusBoxes/index.jsx (93%) rename client/src/Components/{ => v1}/StatusBoxes/skeleton.jsx (100%) rename client/src/Components/{ => v1}/Subheader/index.jsx (100%) rename client/src/Components/{ => v1}/Tab/index.jsx (100%) rename client/src/Components/{ => v1}/Table/TablePagination/Actions/index.jsx (95%) rename client/src/Components/{ => v1}/Table/TablePagination/index.jsx (96%) rename client/src/Components/{ => v1}/Table/TableUtils.js (100%) rename client/src/Components/{ => v1}/Table/index.jsx (99%) rename client/src/Components/{ => v1}/Table/skeleton.jsx (100%) rename client/src/Components/{ => v1}/TextLink/index.jsx (100%) rename client/src/Components/{ => v1}/ThemeSwitch/SunAndMoonIcon.jsx (100%) rename client/src/Components/{ => v1}/ThemeSwitch/index.css (100%) rename client/src/Components/{ => v1}/ThemeSwitch/index.jsx (91%) rename client/src/Components/{ => v1}/Toast/body.jsx (100%) rename client/src/Components/{ => v1}/Toast/index.jsx (98%) rename client/src/Components/{Inputs/TextInput/indexV2.tsx => v2/Inputs/TextInput.tsx} (100%) diff --git a/client/src/App.jsx b/client/src/App.jsx index b599890f1..04f0d5b65 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -10,7 +10,7 @@ import { logger } from "./Utils/Logger"; // Import the logger import { networkService } from "./main"; import { Routes } from "./Routes"; import WalletProvider from "./Components/WalletProvider"; -import AppLayout from "./Components/Layouts/AppLayout"; +import AppLayout from "@/Components/v1/Layouts/AppLayout"; function App() { const mode = useSelector((state) => state.ui.mode); diff --git a/client/src/Components/ActionsMenu/index.jsx b/client/src/Components/v1/ActionsMenu/index.jsx similarity index 94% rename from client/src/Components/ActionsMenu/index.jsx rename to client/src/Components/v1/ActionsMenu/index.jsx index 78d86e1b2..229c7c875 100644 --- a/client/src/Components/ActionsMenu/index.jsx +++ b/client/src/Components/v1/ActionsMenu/index.jsx @@ -2,17 +2,17 @@ import IconButton from "@mui/material/IconButton"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; -import Settings from "../../assets/icons/settings-bold.svg?react"; -import Dialog from "../../Components/Dialog"; +import Settings from "../../../assets/icons/settings-bold.svg?react"; +import Dialog from "../Dialog/index.jsx"; // Utils import { useState } from "react"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; -import { createToast } from "../../Utils/toastUtils"; +import { createToast } from "../../../Utils/toastUtils.jsx"; import PropTypes from "prop-types"; -import { usePauseMonitor, useDeleteMonitor } from "../../Hooks/v1/monitorHooks.js"; +import { usePauseMonitor, useDeleteMonitor } from "../../../Hooks/v1/monitorHooks.js"; const ActionsMenu = ({ monitor, diff --git a/client/src/Components/Alert/index.css b/client/src/Components/v1/Alert/index.css similarity index 100% rename from client/src/Components/Alert/index.css rename to client/src/Components/v1/Alert/index.css diff --git a/client/src/Components/Alert/index.jsx b/client/src/Components/v1/Alert/index.jsx similarity index 100% rename from client/src/Components/Alert/index.jsx rename to client/src/Components/v1/Alert/index.jsx diff --git a/client/src/Components/Animated/PulseDot.jsx b/client/src/Components/v1/Animated/PulseDot.jsx similarity index 100% rename from client/src/Components/Animated/PulseDot.jsx rename to client/src/Components/v1/Animated/PulseDot.jsx diff --git a/client/src/Components/ArrowLeft/index.jsx b/client/src/Components/v1/ArrowLeft/index.jsx similarity index 70% rename from client/src/Components/ArrowLeft/index.jsx rename to client/src/Components/v1/ArrowLeft/index.jsx index 7909441f3..a327d6bb7 100644 --- a/client/src/Components/ArrowLeft/index.jsx +++ b/client/src/Components/v1/ArrowLeft/index.jsx @@ -1,6 +1,6 @@ -import LeftArrow from "../../assets/icons/left-arrow.svg?react"; -import LeftArrowDouble from "../../assets/icons/left-arrow-double.svg?react"; -import LeftArrowLong from "../../assets/icons/left-arrow-long.svg?react"; +import LeftArrow from "../../../assets/icons/left-arrow.svg?react"; +import LeftArrowDouble from "../../../assets/icons/left-arrow-double.svg?react"; +import LeftArrowLong from "../../../assets/icons/left-arrow-long.svg?react"; import PropTypes from "prop-types"; const ArrowLeft = ({ type, color = "#667085", ...props }) => { diff --git a/client/src/Components/ArrowRight/index.jsx b/client/src/Components/v1/ArrowRight/index.jsx similarity index 73% rename from client/src/Components/ArrowRight/index.jsx rename to client/src/Components/v1/ArrowRight/index.jsx index 335bc19f2..08a2639b0 100644 --- a/client/src/Components/ArrowRight/index.jsx +++ b/client/src/Components/v1/ArrowRight/index.jsx @@ -1,5 +1,5 @@ -import RightArrow from "../../assets/icons/right-arrow.svg?react"; -import RightArrowDouble from "../../assets/icons/right-arrow-double.svg?react"; +import RightArrow from "../../../assets/icons/right-arrow.svg?react"; +import RightArrowDouble from "../../../assets/icons/right-arrow-double.svg?react"; import PropTypes from "prop-types"; const ArrowRight = ({ type, color = "#667085", ...props }) => { diff --git a/client/src/Components/Avatar/index.css b/client/src/Components/v1/Avatar/index.css similarity index 100% rename from client/src/Components/Avatar/index.css rename to client/src/Components/v1/Avatar/index.css diff --git a/client/src/Components/Avatar/index.jsx b/client/src/Components/v1/Avatar/index.jsx similarity index 100% rename from client/src/Components/Avatar/index.jsx rename to client/src/Components/v1/Avatar/index.jsx diff --git a/client/src/Components/Breadcrumbs/index.css b/client/src/Components/v1/Breadcrumbs/index.css similarity index 100% rename from client/src/Components/Breadcrumbs/index.css rename to client/src/Components/v1/Breadcrumbs/index.css diff --git a/client/src/Components/Breadcrumbs/index.jsx b/client/src/Components/v1/Breadcrumbs/index.jsx similarity index 97% rename from client/src/Components/Breadcrumbs/index.jsx rename to client/src/Components/v1/Breadcrumbs/index.jsx index f1af89790..34e5d5e95 100644 --- a/client/src/Components/Breadcrumbs/index.jsx +++ b/client/src/Components/v1/Breadcrumbs/index.jsx @@ -2,7 +2,7 @@ import PropTypes from "prop-types"; import { Box, Breadcrumbs as MUIBreadcrumbs } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; -import ArrowRight from "../ArrowRight"; +import ArrowRight from "../ArrowRight/index.jsx"; import "./index.css"; /** diff --git a/client/src/Components/Buttons/RoundGradientButton.jsx b/client/src/Components/v1/Buttons/RoundGradientButton.jsx similarity index 100% rename from client/src/Components/Buttons/RoundGradientButton.jsx rename to client/src/Components/v1/Buttons/RoundGradientButton.jsx diff --git a/client/src/Components/Charts/AreaChart/index.jsx b/client/src/Components/v1/Charts/AreaChart/index.jsx similarity index 98% rename from client/src/Components/Charts/AreaChart/index.jsx rename to client/src/Components/v1/Charts/AreaChart/index.jsx index e1f1832e5..dd5a900d6 100644 --- a/client/src/Components/Charts/AreaChart/index.jsx +++ b/client/src/Components/v1/Charts/AreaChart/index.jsx @@ -61,7 +61,7 @@ import { Tooltip, ResponsiveContainer, } from "recharts"; -import { createGradient } from "../Utils/gradientUtils"; +import { createGradient } from "../Utils/gradientUtils.jsx"; import PropTypes from "prop-types"; import { useTheme } from "@mui/material"; import { useId } from "react"; diff --git a/client/src/Components/Charts/BarChart/index.css b/client/src/Components/v1/Charts/BarChart/index.css similarity index 100% rename from client/src/Components/Charts/BarChart/index.css rename to client/src/Components/v1/Charts/BarChart/index.css diff --git a/client/src/Components/Charts/BarChart/index.jsx b/client/src/Components/v1/Charts/BarChart/index.jsx similarity index 98% rename from client/src/Components/Charts/BarChart/index.jsx rename to client/src/Components/v1/Charts/BarChart/index.jsx index 375773b94..2bf8e1798 100644 --- a/client/src/Components/Charts/BarChart/index.jsx +++ b/client/src/Components/v1/Charts/BarChart/index.jsx @@ -1,6 +1,6 @@ import { useTheme } from "@emotion/react"; import { Box, Stack, Tooltip, Typography } from "@mui/material"; -import { formatDateWithTz } from "../../../Utils/timeUtils"; +import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; import { useEffect, useState } from "react"; import "./index.css"; import { useSelector } from "react-redux"; diff --git a/client/src/Components/Charts/ChartBox/EmptyView.jsx b/client/src/Components/v1/Charts/ChartBox/EmptyView.jsx similarity index 98% rename from client/src/Components/Charts/ChartBox/EmptyView.jsx rename to client/src/Components/v1/Charts/ChartBox/EmptyView.jsx index 296197a8c..c1b60632d 100644 --- a/client/src/Components/Charts/ChartBox/EmptyView.jsx +++ b/client/src/Components/v1/Charts/ChartBox/EmptyView.jsx @@ -2,7 +2,7 @@ import { Typography, Stack } from "@mui/material"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; -import IconBox from "../../IconBox"; +import IconBox from "../../IconBox/index.jsx"; /** * `EmptyView` is a functional React component that displays an empty state view with an optional icon, header, and message. diff --git a/client/src/Components/Charts/ChartBox/index.jsx b/client/src/Components/v1/Charts/ChartBox/index.jsx similarity index 95% rename from client/src/Components/Charts/ChartBox/index.jsx rename to client/src/Components/v1/Charts/ChartBox/index.jsx index 2c13ece87..be2faf8ed 100644 --- a/client/src/Components/Charts/ChartBox/index.jsx +++ b/client/src/Components/v1/Charts/ChartBox/index.jsx @@ -1,7 +1,7 @@ import { Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; -import IconBox from "../../IconBox"; -import EmptyView from "./EmptyView"; +import IconBox from "../../IconBox/index.jsx"; +import EmptyView from "./EmptyView.jsx"; import PropTypes from "prop-types"; const ChartBox = ({ diff --git a/client/src/Components/Charts/CustomGauge/index.css b/client/src/Components/v1/Charts/CustomGauge/index.css similarity index 100% rename from client/src/Components/Charts/CustomGauge/index.css rename to client/src/Components/v1/Charts/CustomGauge/index.css diff --git a/client/src/Components/Charts/CustomGauge/index.jsx b/client/src/Components/v1/Charts/CustomGauge/index.jsx similarity index 100% rename from client/src/Components/Charts/CustomGauge/index.jsx rename to client/src/Components/v1/Charts/CustomGauge/index.jsx diff --git a/client/src/Components/Charts/DePINStatusPageBarChart/index.jsx b/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx similarity index 98% rename from client/src/Components/Charts/DePINStatusPageBarChart/index.jsx rename to client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx index 65c6278f1..b37990e46 100644 --- a/client/src/Components/Charts/DePINStatusPageBarChart/index.jsx +++ b/client/src/Components/v1/Charts/DePINStatusPageBarChart/index.jsx @@ -5,7 +5,7 @@ import { Stack, Box, Tooltip, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useState, useEffect } from "react"; import PropTypes from "prop-types"; -import { formatDateWithTz } from "../../../Utils/timeUtils"; +import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; import { useSelector } from "react-redux"; const PlaceholderCheck = ({ daysToShow }) => { diff --git a/client/src/Components/Charts/LegendBox/index.jsx b/client/src/Components/v1/Charts/LegendBox/index.jsx similarity index 95% rename from client/src/Components/Charts/LegendBox/index.jsx rename to client/src/Components/v1/Charts/LegendBox/index.jsx index ac14669e3..f19e09105 100644 --- a/client/src/Components/Charts/LegendBox/index.jsx +++ b/client/src/Components/v1/Charts/LegendBox/index.jsx @@ -1,6 +1,6 @@ import { Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; -import IconBox from "../../IconBox"; +import IconBox from "../../IconBox/index.jsx"; import PropTypes from "prop-types"; const LegendBox = ({ children, icon, header, sx }) => { diff --git a/client/src/Components/Charts/MonitorDetailsAreaChart/index.css b/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.css similarity index 100% rename from client/src/Components/Charts/MonitorDetailsAreaChart/index.css rename to client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.css diff --git a/client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx b/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx similarity index 97% rename from client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx rename to client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx index a1ead3cf9..a9ef04a53 100644 --- a/client/src/Components/Charts/MonitorDetailsAreaChart/index.jsx +++ b/client/src/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx @@ -12,11 +12,11 @@ import { Box, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useMemo, useState } from "react"; import { useSelector } from "react-redux"; -import { formatDateWithTz } from "../../../Utils/timeUtils"; +import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; import { tooltipDateFormatLookup, tickDateFormatLookup, -} from "../Utils/chartUtilFunctions"; +} from "../Utils/chartUtilFunctions.js"; import "./index.css"; const CustomToolTip = ({ active, payload, label, dateRange }) => { diff --git a/client/src/Components/Charts/StatusPageBarChart/index.jsx b/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx similarity index 98% rename from client/src/Components/Charts/StatusPageBarChart/index.jsx rename to client/src/Components/v1/Charts/StatusPageBarChart/index.jsx index eab0c7d1d..7da4d58ba 100644 --- a/client/src/Components/Charts/StatusPageBarChart/index.jsx +++ b/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx @@ -1,6 +1,6 @@ import { useTheme } from "@emotion/react"; import { Box, Stack, Tooltip, Typography } from "@mui/material"; -import { formatDateWithTz } from "../../../Utils/timeUtils"; +import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; import { useEffect, useState, forwardRef } from "react"; import { useSelector } from "react-redux"; import PropTypes from "prop-types"; diff --git a/client/src/Components/Charts/Utils/chartUtilFunctions.js b/client/src/Components/v1/Charts/Utils/chartUtilFunctions.js similarity index 100% rename from client/src/Components/Charts/Utils/chartUtilFunctions.js rename to client/src/Components/v1/Charts/Utils/chartUtilFunctions.js diff --git a/client/src/Components/Charts/Utils/chartUtils.jsx b/client/src/Components/v1/Charts/Utils/chartUtils.jsx similarity index 98% rename from client/src/Components/Charts/Utils/chartUtils.jsx rename to client/src/Components/v1/Charts/Utils/chartUtils.jsx index e24881c1c..1bab80595 100644 --- a/client/src/Components/Charts/Utils/chartUtils.jsx +++ b/client/src/Components/v1/Charts/Utils/chartUtils.jsx @@ -2,9 +2,9 @@ import PropTypes from "prop-types"; import { useSelector } from "react-redux"; import { useTheme } from "@mui/material"; import { Text } from "recharts"; -import { formatDateWithTz } from "../../../Utils/timeUtils"; +import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; import { Box, Stack, Typography } from "@mui/material"; -import { tickDateFormatLookup, tooltipDateFormatLookup } from "./chartUtilFunctions"; +import { tickDateFormatLookup, tooltipDateFormatLookup } from "./chartUtilFunctions.js"; /** * Custom tick component for rendering time with timezone. * diff --git a/client/src/Components/Charts/Utils/gradientUtils.jsx b/client/src/Components/v1/Charts/Utils/gradientUtils.jsx similarity index 100% rename from client/src/Components/Charts/Utils/gradientUtils.jsx rename to client/src/Components/v1/Charts/Utils/gradientUtils.jsx diff --git a/client/src/Components/Check/Check.jsx b/client/src/Components/v1/Check/Check.jsx similarity index 93% rename from client/src/Components/Check/Check.jsx rename to client/src/Components/v1/Check/Check.jsx index 02b9fd78a..53971316b 100644 --- a/client/src/Components/Check/Check.jsx +++ b/client/src/Components/v1/Check/Check.jsx @@ -1,7 +1,7 @@ import "./check.css"; import PropTypes from "prop-types"; -import CheckGrey from "../../assets/icons/check.svg?react"; -import CheckOutlined from "../../assets/icons/check-outlined.svg?react"; +import CheckGrey from "../../../assets/icons/check.svg?react"; +import CheckOutlined from "../../../assets/icons/check-outlined.svg?react"; import { Box, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; diff --git a/client/src/Components/Check/check.css b/client/src/Components/v1/Check/check.css similarity index 100% rename from client/src/Components/Check/check.css rename to client/src/Components/v1/Check/check.css diff --git a/client/src/Components/CircularCount/index.jsx b/client/src/Components/v1/CircularCount/index.jsx similarity index 100% rename from client/src/Components/CircularCount/index.jsx rename to client/src/Components/v1/CircularCount/index.jsx diff --git a/client/src/Components/Common/AppBar.jsx b/client/src/Components/v1/Common/AppBar.jsx similarity index 99% rename from client/src/Components/Common/AppBar.jsx rename to client/src/Components/v1/Common/AppBar.jsx index de5f0992e..1d5a38ddd 100644 --- a/client/src/Components/Common/AppBar.jsx +++ b/client/src/Components/v1/Common/AppBar.jsx @@ -10,7 +10,7 @@ import MenuItem from "@mui/material/MenuItem"; import Drawer from "@mui/material/Drawer"; import MenuIcon from "@mui/icons-material/Menu"; import CloseRoundedIcon from "@mui/icons-material/CloseRounded"; -import ThemeSwitch from "../ThemeSwitch"; +import ThemeSwitch from "../ThemeSwitch/index.jsx"; import { useTheme } from "@mui/material/styles"; import { useSelector } from "react-redux"; import { useLocation, useNavigate } from "react-router-dom"; diff --git a/client/src/Components/Common/Footer.jsx b/client/src/Components/v1/Common/Footer.jsx similarity index 100% rename from client/src/Components/Common/Footer.jsx rename to client/src/Components/v1/Common/Footer.jsx diff --git a/client/src/Components/ConfigBox/index.jsx b/client/src/Components/v1/ConfigBox/index.jsx similarity index 100% rename from client/src/Components/ConfigBox/index.jsx rename to client/src/Components/v1/ConfigBox/index.jsx diff --git a/client/src/Components/ConfigRow/index.jsx b/client/src/Components/v1/ConfigRow/index.jsx similarity index 96% rename from client/src/Components/ConfigRow/index.jsx rename to client/src/Components/v1/ConfigRow/index.jsx index 217085b6d..12fea923e 100644 --- a/client/src/Components/ConfigRow/index.jsx +++ b/client/src/Components/v1/ConfigRow/index.jsx @@ -1,7 +1,7 @@ import { Box, Stack, Typography } from "@mui/material"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; -import ConfigBox from "../ConfigBox"; +import ConfigBox from "../ConfigBox/index.jsx"; /** * ConfigRow is a styled container used to layout content in a row format with specific padding, border, and spacing. diff --git a/client/src/Components/Dialog/genericDialog.jsx b/client/src/Components/v1/Dialog/genericDialog.jsx similarity index 100% rename from client/src/Components/Dialog/genericDialog.jsx rename to client/src/Components/v1/Dialog/genericDialog.jsx diff --git a/client/src/Components/Dialog/index.jsx b/client/src/Components/v1/Dialog/index.jsx similarity index 95% rename from client/src/Components/Dialog/index.jsx rename to client/src/Components/v1/Dialog/index.jsx index 678eb8721..b8cf07439 100644 --- a/client/src/Components/Dialog/index.jsx +++ b/client/src/Components/v1/Dialog/index.jsx @@ -1,6 +1,6 @@ import PropTypes from "prop-types"; import { Button, Stack } from "@mui/material"; -import { GenericDialog } from "./genericDialog"; +import { GenericDialog } from "./genericDialog.jsx"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Components/Dot/index.jsx b/client/src/Components/v1/Dot/index.jsx similarity index 100% rename from client/src/Components/Dot/index.jsx rename to client/src/Components/v1/Dot/index.jsx diff --git a/client/src/Components/Fallback/FallBackActionButtons.jsx b/client/src/Components/v1/Fallback/FallBackActionButtons.jsx similarity index 100% rename from client/src/Components/Fallback/FallBackActionButtons.jsx rename to client/src/Components/v1/Fallback/FallBackActionButtons.jsx diff --git a/client/src/Components/Fallback/FallbackBackground.jsx b/client/src/Components/v1/Fallback/FallbackBackground.jsx similarity index 78% rename from client/src/Components/Fallback/FallbackBackground.jsx rename to client/src/Components/v1/Fallback/FallbackBackground.jsx index fe46f4064..2182082fd 100644 --- a/client/src/Components/Fallback/FallbackBackground.jsx +++ b/client/src/Components/v1/Fallback/FallbackBackground.jsx @@ -1,8 +1,8 @@ import { useTheme } from "@emotion/react"; import Box from "@mui/material/Box"; -import Background from "../../assets/Images/background-grid.svg?react"; -import OutputAnimation from "../../assets/Animations/output.gif"; -import DarkmodeOutput from "../../assets/Animations/darkmodeOutput.gif"; +import Background from "../../../assets/Images/background-grid.svg?react"; +import OutputAnimation from "../../../assets/Animations/output.gif"; +import DarkmodeOutput from "../../../assets/Animations/darkmodeOutput.gif"; import { useSelector } from "react-redux"; const FallbackBackground = () => { const theme = useTheme(); diff --git a/client/src/Components/Fallback/FallbackCheckList.jsx b/client/src/Components/v1/Fallback/FallbackCheckList.jsx similarity index 95% rename from client/src/Components/Fallback/FallbackCheckList.jsx rename to client/src/Components/v1/Fallback/FallbackCheckList.jsx index 4d13dead4..befb95185 100644 --- a/client/src/Components/Fallback/FallbackCheckList.jsx +++ b/client/src/Components/v1/Fallback/FallbackCheckList.jsx @@ -2,7 +2,7 @@ import PropTypes from "prop-types"; import { useTheme } from "@mui/material/styles"; import Box from "@mui/material/Box"; import Stack from "@mui/material/Stack"; -import Check from "../Check/Check"; +import Check from "../Check/Check.jsx"; const FallbackCheckList = ({ checks, type }) => { const theme = useTheme(); return ( diff --git a/client/src/Components/Fallback/FallbackContainer.jsx b/client/src/Components/v1/Fallback/FallbackContainer.jsx similarity index 100% rename from client/src/Components/Fallback/FallbackContainer.jsx rename to client/src/Components/v1/Fallback/FallbackContainer.jsx diff --git a/client/src/Components/Fallback/FallbackPageSpeedWarning.jsx b/client/src/Components/v1/Fallback/FallbackPageSpeedWarning.jsx similarity index 97% rename from client/src/Components/Fallback/FallbackPageSpeedWarning.jsx rename to client/src/Components/v1/Fallback/FallbackPageSpeedWarning.jsx index bcec7300d..2cf52bd88 100644 --- a/client/src/Components/Fallback/FallbackPageSpeedWarning.jsx +++ b/client/src/Components/v1/Fallback/FallbackPageSpeedWarning.jsx @@ -3,7 +3,7 @@ import { useTheme } from "@mui/material/styles"; import Link from "@mui/material/Link"; import { Link as RouterLink } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import Alert from "../Alert"; +import Alert from "../Alert/index.jsx"; import PropTypes from "prop-types"; const renderWarningMessage = (t) => { diff --git a/client/src/Components/Fallback/FallbackTitle.jsx b/client/src/Components/v1/Fallback/FallbackTitle.jsx similarity index 100% rename from client/src/Components/Fallback/FallbackTitle.jsx rename to client/src/Components/v1/Fallback/FallbackTitle.jsx diff --git a/client/src/Components/Fallback/index.css b/client/src/Components/v1/Fallback/index.css similarity index 100% rename from client/src/Components/Fallback/index.css rename to client/src/Components/v1/Fallback/index.css diff --git a/client/src/Components/Fallback/index.jsx b/client/src/Components/v1/Fallback/index.jsx similarity index 87% rename from client/src/Components/Fallback/index.jsx rename to client/src/Components/v1/Fallback/index.jsx index 5fbb958e7..40113f882 100644 --- a/client/src/Components/Fallback/index.jsx +++ b/client/src/Components/v1/Fallback/index.jsx @@ -2,11 +2,11 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { Box, Stack } from "@mui/material"; import "./index.css"; -import FallbackTitle from "./FallbackTitle"; -import FallbackCheckList from "./FallbackCheckList"; -import FallbackActionButtons from "./FallBackActionButtons"; -import FallbackBackground from "./FallbackBackground"; -import FallbackContainer from "./FallbackContainer"; +import FallbackTitle from "./FallbackTitle.jsx"; +import FallbackCheckList from "./FallbackCheckList.jsx"; +import FallbackActionButtons from "./FallBackActionButtons.jsx"; +import FallbackBackground from "./FallbackBackground.jsx"; +import FallbackContainer from "./FallbackContainer.jsx"; /** * Fallback component to display a fallback UI with a title, a list of checks, and a navigation button. * diff --git a/client/src/Components/FilterHeader/index.jsx b/client/src/Components/v1/FilterHeader/index.jsx similarity index 100% rename from client/src/Components/FilterHeader/index.jsx rename to client/src/Components/v1/FilterHeader/index.jsx diff --git a/client/src/Components/GenericFallback/NetworkError.jsx b/client/src/Components/v1/GenericFallback/NetworkError.jsx similarity index 100% rename from client/src/Components/GenericFallback/NetworkError.jsx rename to client/src/Components/v1/GenericFallback/NetworkError.jsx diff --git a/client/src/Components/GenericFallback/index.jsx b/client/src/Components/v1/GenericFallback/index.jsx similarity index 89% rename from client/src/Components/GenericFallback/index.jsx rename to client/src/Components/v1/GenericFallback/index.jsx index 7f6d258ea..137cdd1da 100644 --- a/client/src/Components/GenericFallback/index.jsx +++ b/client/src/Components/v1/GenericFallback/index.jsx @@ -1,8 +1,8 @@ import { useTheme } from "@emotion/react"; import { Box, Stack } from "@mui/material"; -import OutputAnimation from "../../assets/Animations/output.gif"; -import DarkmodeOutput from "../../assets/Animations/darkmodeOutput.gif"; -import Background from "../../assets/Images/background-grid.svg?react"; +import OutputAnimation from "../../../assets/Animations/output.gif"; +import DarkmodeOutput from "../../../assets/Animations/darkmodeOutput.gif"; +import Background from "../../../assets/Images/background-grid.svg?react"; import { useSelector } from "react-redux"; /** diff --git a/client/src/Components/HOC/withAdminCheck.jsx b/client/src/Components/v1/HOC/withAdminCheck.jsx similarity index 91% rename from client/src/Components/HOC/withAdminCheck.jsx rename to client/src/Components/v1/HOC/withAdminCheck.jsx index 458330a1f..8646c99b6 100644 --- a/client/src/Components/HOC/withAdminCheck.jsx +++ b/client/src/Components/v1/HOC/withAdminCheck.jsx @@ -1,8 +1,8 @@ import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; -import { logger } from "../../Utils/Logger"; -import { networkService } from "../../main"; +import { logger } from "../../../Utils/Logger.js"; +import { networkService } from "../../../main.jsx"; const withAdminCheck = (WrappedComponent) => { const WithAdminCheck = (props) => { diff --git a/client/src/Components/Heading/index.jsx b/client/src/Components/v1/Heading/index.jsx similarity index 100% rename from client/src/Components/Heading/index.jsx rename to client/src/Components/v1/Heading/index.jsx diff --git a/client/src/Components/Host/index.jsx b/client/src/Components/v1/Host/index.jsx similarity index 97% rename from client/src/Components/Host/index.jsx rename to client/src/Components/v1/Host/index.jsx index d1cf382b2..772f5b61c 100644 --- a/client/src/Components/Host/index.jsx +++ b/client/src/Components/v1/Host/index.jsx @@ -1,7 +1,7 @@ import { Stack, Typography } from "@mui/material"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; -import Dot from "../Dot"; +import Dot from "../Dot/index.jsx"; /** * Host component. * This subcomponent receives a params object and displays the host details. diff --git a/client/src/Components/HttpStatusLabel/index.jsx b/client/src/Components/v1/HttpStatusLabel/index.jsx similarity index 96% rename from client/src/Components/HttpStatusLabel/index.jsx rename to client/src/Components/v1/HttpStatusLabel/index.jsx index 52676ae7c..2253e323e 100644 --- a/client/src/Components/HttpStatusLabel/index.jsx +++ b/client/src/Components/v1/HttpStatusLabel/index.jsx @@ -1,6 +1,6 @@ import PropTypes from "prop-types"; import { useTheme } from "@mui/material"; -import { BaseLabel } from "../Label"; +import { BaseLabel } from "../Label/index.jsx"; /** * @component diff --git a/client/src/Components/I18nLoader/index.jsx b/client/src/Components/v1/I18nLoader/index.jsx similarity index 88% rename from client/src/Components/I18nLoader/index.jsx rename to client/src/Components/v1/I18nLoader/index.jsx index 7c3cbfb85..2a6a4966d 100644 --- a/client/src/Components/I18nLoader/index.jsx +++ b/client/src/Components/v1/I18nLoader/index.jsx @@ -1,4 +1,4 @@ -import i18n from "../../Utils/i18n"; +import i18n from "../../../Utils/i18n.js"; import { useSelector } from "react-redux"; import { useEffect } from "react"; const I18nLoader = () => { diff --git a/client/src/Components/IconBox/index.jsx b/client/src/Components/v1/IconBox/index.jsx similarity index 100% rename from client/src/Components/IconBox/index.jsx rename to client/src/Components/v1/IconBox/index.jsx diff --git a/client/src/Components/Image/index.jsx b/client/src/Components/v1/Image/index.jsx similarity index 100% rename from client/src/Components/Image/index.jsx rename to client/src/Components/v1/Image/index.jsx diff --git a/client/src/Components/InfoBox/index.jsx b/client/src/Components/v1/InfoBox/index.jsx similarity index 88% rename from client/src/Components/InfoBox/index.jsx rename to client/src/Components/v1/InfoBox/index.jsx index 6797d6c9c..96e67975d 100644 --- a/client/src/Components/InfoBox/index.jsx +++ b/client/src/Components/v1/InfoBox/index.jsx @@ -1,6 +1,6 @@ -import { RowContainer } from "../StandardContainer"; +import { RowContainer } from "../StandardContainer/index.jsx"; import { Stack, Typography } from "@mui/material"; -import Image from "../Image"; +import Image from "../Image/index.jsx"; import { useTheme } from "@mui/material/styles"; diff --git a/client/src/Components/Inputs/Checkbox/index.css b/client/src/Components/v1/Inputs/Checkbox/index.css similarity index 100% rename from client/src/Components/Inputs/Checkbox/index.css rename to client/src/Components/v1/Inputs/Checkbox/index.css diff --git a/client/src/Components/Inputs/Checkbox/index.jsx b/client/src/Components/v1/Inputs/Checkbox/index.jsx similarity index 95% rename from client/src/Components/Inputs/Checkbox/index.jsx rename to client/src/Components/v1/Inputs/Checkbox/index.jsx index 1497c44fb..08af49d84 100644 --- a/client/src/Components/Inputs/Checkbox/index.jsx +++ b/client/src/Components/v1/Inputs/Checkbox/index.jsx @@ -1,8 +1,8 @@ import PropTypes from "prop-types"; import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material"; import { useTheme } from "@emotion/react"; -import CheckboxOutline from "../../../assets/icons/checkbox-outline.svg?react"; -import CheckboxFilled from "../../../assets/icons/checkbox-filled.svg?react"; +import CheckboxOutline from "../../../../assets/icons/checkbox-outline.svg?react"; +import CheckboxFilled from "../../../../assets/icons/checkbox-filled.svg?react"; import "./index.css"; /** diff --git a/client/src/Components/Inputs/ColorPicker/index.jsx b/client/src/Components/v1/Inputs/ColorPicker/index.jsx similarity index 100% rename from client/src/Components/Inputs/ColorPicker/index.jsx rename to client/src/Components/v1/Inputs/ColorPicker/index.jsx diff --git a/client/src/Components/Inputs/FieldWrapper/index.jsx b/client/src/Components/v1/Inputs/FieldWrapper/index.jsx similarity index 100% rename from client/src/Components/Inputs/FieldWrapper/index.jsx rename to client/src/Components/v1/Inputs/FieldWrapper/index.jsx diff --git a/client/src/Components/Inputs/ImageUpload/index.jsx b/client/src/Components/v1/Inputs/ImageUpload/index.jsx similarity index 98% rename from client/src/Components/Inputs/ImageUpload/index.jsx rename to client/src/Components/v1/Inputs/ImageUpload/index.jsx index a81c89393..e7415bf8f 100644 --- a/client/src/Components/Inputs/ImageUpload/index.jsx +++ b/client/src/Components/v1/Inputs/ImageUpload/index.jsx @@ -1,10 +1,10 @@ // Components import { Box, Stack, Typography } from "@mui/material"; import CloudUploadIcon from "@mui/icons-material/CloudUpload"; -import Image from "../../../Components/Image"; +import Image from "../../Image/index.jsx"; import TextField from "@mui/material/TextField"; import IconButton from "@mui/material/IconButton"; -import ProgressUpload from "../../ProgressBars"; +import ProgressUpload from "../../ProgressBars/index.jsx"; import ImageIcon from "@mui/icons-material/Image"; // Utils diff --git a/client/src/Components/Inputs/Radio/index.css b/client/src/Components/v1/Inputs/Radio/index.css similarity index 100% rename from client/src/Components/Inputs/Radio/index.css rename to client/src/Components/v1/Inputs/Radio/index.css diff --git a/client/src/Components/Inputs/Radio/index.jsx b/client/src/Components/v1/Inputs/Radio/index.jsx similarity index 96% rename from client/src/Components/Inputs/Radio/index.jsx rename to client/src/Components/v1/Inputs/Radio/index.jsx index 4c687de1e..575c1af74 100644 --- a/client/src/Components/Inputs/Radio/index.jsx +++ b/client/src/Components/v1/Inputs/Radio/index.jsx @@ -1,7 +1,7 @@ import PropTypes from "prop-types"; import { FormControlLabel, Radio as MUIRadio, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; -import RadioChecked from "../../../assets/icons/radio-checked.svg?react"; +import RadioChecked from "../../../../assets/icons/radio-checked.svg?react"; import "./index.css"; /** diff --git a/client/src/Components/Inputs/Search/index.jsx b/client/src/Components/v1/Inputs/Search/index.jsx similarity index 98% rename from client/src/Components/Inputs/Search/index.jsx rename to client/src/Components/v1/Inputs/Search/index.jsx index 7202cc2eb..7a3771d47 100644 --- a/client/src/Components/Inputs/Search/index.jsx +++ b/client/src/Components/v1/Inputs/Search/index.jsx @@ -8,10 +8,10 @@ import { Checkbox, } from "@mui/material"; import { useTheme } from "@emotion/react"; -import SearchIcon from "../../../assets/icons/search.svg?react"; +import SearchIcon from "../../../../assets/icons/search.svg?react"; import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import FieldWrapper from "../FieldWrapper"; +import FieldWrapper from "../FieldWrapper/index.jsx"; /** * Search component using Material UI's Autocomplete. diff --git a/client/src/Components/Inputs/Select/index.css b/client/src/Components/v1/Inputs/Select/index.css similarity index 100% rename from client/src/Components/Inputs/Select/index.css rename to client/src/Components/v1/Inputs/Select/index.css diff --git a/client/src/Components/Inputs/Select/index.jsx b/client/src/Components/v1/Inputs/Select/index.jsx similarity index 98% rename from client/src/Components/Inputs/Select/index.jsx rename to client/src/Components/v1/Inputs/Select/index.jsx index c5c488f91..37ce30dc1 100644 --- a/client/src/Components/Inputs/Select/index.jsx +++ b/client/src/Components/v1/Inputs/Select/index.jsx @@ -2,7 +2,7 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { MenuItem, Select as MuiSelect, Stack, Typography } from "@mui/material"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; -import FieldWrapper from "../FieldWrapper"; +import FieldWrapper from "../FieldWrapper/index.jsx"; import "./index.css"; diff --git a/client/src/Components/Inputs/TextInput/Adornments/index.jsx b/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx similarity index 96% rename from client/src/Components/Inputs/TextInput/Adornments/index.jsx rename to client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx index d6af2a89e..4e4bbd2af 100644 --- a/client/src/Components/Inputs/TextInput/Adornments/index.jsx +++ b/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx @@ -4,7 +4,7 @@ import PropTypes from "prop-types"; import VisibilityOff from "@mui/icons-material/VisibilityOff"; import Visibility from "@mui/icons-material/Visibility"; import ReorderRoundedIcon from "@mui/icons-material/ReorderRounded"; -import DeleteIcon from "../../../../assets/icons/trash-bin.svg?react"; +import DeleteIcon from "../../../../../assets/icons/trash-bin.svg?react"; export const HttpAdornment = ({ https }) => { const theme = useTheme(); diff --git a/client/src/Components/Inputs/TextInput/index.jsx b/client/src/Components/v1/Inputs/TextInput/index.jsx similarity index 98% rename from client/src/Components/Inputs/TextInput/index.jsx rename to client/src/Components/v1/Inputs/TextInput/index.jsx index 469791704..7203e5f50 100644 --- a/client/src/Components/Inputs/TextInput/index.jsx +++ b/client/src/Components/v1/Inputs/TextInput/index.jsx @@ -2,7 +2,7 @@ import { Stack, TextField, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { forwardRef, useState, cloneElement } from "react"; import PropTypes from "prop-types"; -import FieldWrapper from "../FieldWrapper"; +import FieldWrapper from "../FieldWrapper/index.jsx"; const getSx = (theme, type, maxWidth) => { const sx = { diff --git a/client/src/Components/Label/index.css b/client/src/Components/v1/Label/index.css similarity index 100% rename from client/src/Components/Label/index.css rename to client/src/Components/v1/Label/index.css diff --git a/client/src/Components/Label/index.jsx b/client/src/Components/v1/Label/index.jsx similarity index 100% rename from client/src/Components/Label/index.jsx rename to client/src/Components/v1/Label/index.jsx diff --git a/client/src/Components/Layouts/AppLayout/index.jsx b/client/src/Components/v1/Layouts/AppLayout/index.jsx similarity index 91% rename from client/src/Components/Layouts/AppLayout/index.jsx rename to client/src/Components/v1/Layouts/AppLayout/index.jsx index d13f6d879..0dbfb1e7a 100644 --- a/client/src/Components/Layouts/AppLayout/index.jsx +++ b/client/src/Components/v1/Layouts/AppLayout/index.jsx @@ -1,7 +1,7 @@ import Box from "@mui/material/Box"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; -import BackgroundSVG from "../../../assets/Images/background.svg"; +import BackgroundSVG from "../../../../assets/Images/background.svg"; import { useSelector } from "react-redux"; const AppLayout = ({ children }) => { diff --git a/client/src/Components/Layouts/HomeLayout/index.css b/client/src/Components/v1/Layouts/HomeLayout/index.css similarity index 100% rename from client/src/Components/Layouts/HomeLayout/index.css rename to client/src/Components/v1/Layouts/HomeLayout/index.css diff --git a/client/src/Components/Layouts/HomeLayout/index.jsx b/client/src/Components/v1/Layouts/HomeLayout/index.jsx similarity index 88% rename from client/src/Components/Layouts/HomeLayout/index.jsx rename to client/src/Components/v1/Layouts/HomeLayout/index.jsx index 55b612522..3e63aff37 100644 --- a/client/src/Components/Layouts/HomeLayout/index.jsx +++ b/client/src/Components/v1/Layouts/HomeLayout/index.jsx @@ -1,4 +1,4 @@ -import Sidebar from "../../Sidebar"; +import Sidebar from "../../Sidebar/index.jsx"; import { Outlet } from "react-router"; import { Stack } from "@mui/material"; diff --git a/client/src/Components/Link/index.jsx b/client/src/Components/v1/Link/index.jsx similarity index 100% rename from client/src/Components/Link/index.jsx rename to client/src/Components/v1/Link/index.jsx diff --git a/client/src/Components/Link/link.css b/client/src/Components/v1/Link/link.css similarity index 100% rename from client/src/Components/Link/link.css rename to client/src/Components/v1/Link/link.css diff --git a/client/src/Components/MonitorActions/index.jsx b/client/src/Components/v1/MonitorActions/index.jsx similarity index 95% rename from client/src/Components/MonitorActions/index.jsx rename to client/src/Components/v1/MonitorActions/index.jsx index 9e88e26c5..23cebc972 100644 --- a/client/src/Components/MonitorActions/index.jsx +++ b/client/src/Components/v1/MonitorActions/index.jsx @@ -10,8 +10,8 @@ import MenuItem from "@mui/material/MenuItem"; import MenuList from "@mui/material/MenuList"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { createToast } from "../../Utils/toastUtils"; -import { useExportMonitors } from "../../Hooks/v1/monitorHooks.js"; +import { createToast } from "../../../Utils/toastUtils.jsx"; +import { useExportMonitors } from "../../../Hooks/v1/monitorHooks.js"; const MonitorActions = ({ isLoading }) => { const [open, setOpen] = React.useState(false); diff --git a/client/src/Components/MonitorCountHeader/index.jsx b/client/src/Components/v1/MonitorCountHeader/index.jsx similarity index 95% rename from client/src/Components/MonitorCountHeader/index.jsx rename to client/src/Components/v1/MonitorCountHeader/index.jsx index 7610a9d5c..eba644d26 100644 --- a/client/src/Components/MonitorCountHeader/index.jsx +++ b/client/src/Components/v1/MonitorCountHeader/index.jsx @@ -1,7 +1,7 @@ import { Stack, Typography } from "@mui/material"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; -import SkeletonLayout from "./skeleton"; +import SkeletonLayout from "./skeleton.jsx"; const MonitorCountHeader = ({ isLoading = false, diff --git a/client/src/Components/MonitorCountHeader/skeleton.jsx b/client/src/Components/v1/MonitorCountHeader/skeleton.jsx similarity index 100% rename from client/src/Components/MonitorCountHeader/skeleton.jsx rename to client/src/Components/v1/MonitorCountHeader/skeleton.jsx diff --git a/client/src/Components/MonitorCreateHeader/index.jsx b/client/src/Components/v1/MonitorCreateHeader/index.jsx similarity index 94% rename from client/src/Components/MonitorCreateHeader/index.jsx rename to client/src/Components/v1/MonitorCreateHeader/index.jsx index b3b50681c..90c12f5ec 100644 --- a/client/src/Components/MonitorCreateHeader/index.jsx +++ b/client/src/Components/v1/MonitorCreateHeader/index.jsx @@ -3,7 +3,7 @@ import { useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; import { useTheme } from "@emotion/react"; -import MonitorActions from "../MonitorActions"; +import MonitorActions from "../MonitorActions/index.jsx"; const CreateMonitorHeader = ({ isAdmin, label, isLoading = true, path, bulkPath }) => { const navigate = useNavigate(); diff --git a/client/src/Components/MonitorCreateHeader/skeleton.jsx b/client/src/Components/v1/MonitorCreateHeader/skeleton.jsx similarity index 100% rename from client/src/Components/MonitorCreateHeader/skeleton.jsx rename to client/src/Components/v1/MonitorCreateHeader/skeleton.jsx diff --git a/client/src/Components/MonitorDetailsControlHeader/index.jsx b/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx similarity index 92% rename from client/src/Components/MonitorDetailsControlHeader/index.jsx rename to client/src/Components/v1/MonitorDetailsControlHeader/index.jsx index cd56394f4..ae565f3bb 100644 --- a/client/src/Components/MonitorDetailsControlHeader/index.jsx +++ b/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx @@ -1,6 +1,6 @@ import Stack from "@mui/material/Stack"; -import Status from "./status"; -import Skeleton from "./skeleton"; +import Status from "./status.jsx"; +import Skeleton from "./skeleton.jsx"; import Button from "@mui/material/Button"; import { Tooltip } from "@mui/material"; import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined"; @@ -12,10 +12,10 @@ import EmailIcon from "@mui/icons-material/Email"; import PropTypes from "prop-types"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@mui/material/styles"; -import { usePauseMonitor } from "../../Hooks/v1/monitorHooks.js"; -import { useSendTestEmail } from "../../Hooks/v1/useSendTestEmail.js"; +import { usePauseMonitor } from "../../../Hooks/v1/monitorHooks.js"; +import { useSendTestEmail } from "../../../Hooks/v1/useSendTestEmail.js"; import { useTranslation } from "react-i18next"; -import { useTestAllNotifications } from "../../Hooks/v1/useNotifications.js"; +import { useTestAllNotifications } from "../../../Hooks/v1/useNotifications.js"; /** * MonitorDetailsControlHeader component displays the control header for monitor details. * It includes status display, pause/resume button, and a configure button for admins. diff --git a/client/src/Components/MonitorDetailsControlHeader/skeleton.jsx b/client/src/Components/v1/MonitorDetailsControlHeader/skeleton.jsx similarity index 100% rename from client/src/Components/MonitorDetailsControlHeader/skeleton.jsx rename to client/src/Components/v1/MonitorDetailsControlHeader/skeleton.jsx diff --git a/client/src/Components/MonitorDetailsControlHeader/status.jsx b/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx similarity index 83% rename from client/src/Components/MonitorDetailsControlHeader/status.jsx rename to client/src/Components/v1/MonitorDetailsControlHeader/status.jsx index 2cf749861..d9b7afc40 100644 --- a/client/src/Components/MonitorDetailsControlHeader/status.jsx +++ b/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx @@ -1,14 +1,14 @@ // Components import Stack from "@mui/material/Stack"; -import PulseDot from "../../Components/Animated/PulseDot"; +import PulseDot from "../Animated/PulseDot.jsx"; import Typography from "@mui/material/Typography"; -import Dot from "../../Components/Dot"; +import Dot from "../Dot/index.jsx"; // Utils -import { formatDurationRounded } from "../../Utils/timeUtils"; +import { formatDurationRounded } from "../../../Utils/timeUtils.js"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "../../Hooks/v1/useMonitorUtils.js"; -import { formatMonitorUrl } from "../../Utils/utils"; +import { useMonitorUtils } from "../../../Hooks/v1/useMonitorUtils.js"; +import { formatMonitorUrl } from "../../../Utils/utils.js"; /** * Status component displays the status information of a monitor. * It includes the monitor's name, URL, and check interval. diff --git a/client/src/Components/MonitorTimeFrameHeader/index.jsx b/client/src/Components/v1/MonitorTimeFrameHeader/index.jsx similarity index 97% rename from client/src/Components/MonitorTimeFrameHeader/index.jsx rename to client/src/Components/v1/MonitorTimeFrameHeader/index.jsx index 3958d507c..9d57edc41 100644 --- a/client/src/Components/MonitorTimeFrameHeader/index.jsx +++ b/client/src/Components/v1/MonitorTimeFrameHeader/index.jsx @@ -1,6 +1,6 @@ import { Stack, Typography, Button, ButtonGroup } from "@mui/material"; import { useTheme } from "@emotion/react"; -import SkeletonLayout from "./skeleton"; +import SkeletonLayout from "./skeleton.jsx"; import PropTypes from "prop-types"; const MonitorTimeFrameHeader = ({ diff --git a/client/src/Components/MonitorTimeFrameHeader/skeleton.jsx b/client/src/Components/v1/MonitorTimeFrameHeader/skeleton.jsx similarity index 100% rename from client/src/Components/MonitorTimeFrameHeader/skeleton.jsx rename to client/src/Components/v1/MonitorTimeFrameHeader/skeleton.jsx diff --git a/client/src/Components/NotificationConfig/index.jsx b/client/src/Components/v1/NotificationConfig/index.jsx similarity index 98% rename from client/src/Components/NotificationConfig/index.jsx rename to client/src/Components/v1/NotificationConfig/index.jsx index c37889eff..58096881e 100644 --- a/client/src/Components/NotificationConfig/index.jsx +++ b/client/src/Components/v1/NotificationConfig/index.jsx @@ -3,7 +3,7 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import Divider from "@mui/material/Divider"; import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded"; -import Search from "../Inputs/Search"; +import Search from "../Inputs/Search/index.jsx"; // Utils import { useState, useEffect } from "react"; diff --git a/client/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx b/client/src/Components/v1/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx similarity index 98% rename from client/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx rename to client/src/Components/v1/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx index dc94ee731..2aae996f6 100644 --- a/client/src/Components/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx +++ b/client/src/Components/v1/NotificationIntegrationModal/Components/NotificationIntegrationModal.jsx @@ -14,9 +14,9 @@ import { Stack, } from "@mui/material"; import { useTheme } from "@emotion/react"; -import TabPanel from "./TabPanel"; -import TabComponent from "./TabComponent"; -import useNotifications from "../Hooks/useNotification"; +import TabPanel from "./TabPanel.jsx"; +import TabComponent from "./TabComponent.jsx"; +import useNotifications from "../Hooks/useNotification.js"; // Define constants for notification types to avoid magic values const NOTIFICATION_TYPES = { diff --git a/client/src/Components/NotificationIntegrationModal/Components/TabComponent.jsx b/client/src/Components/v1/NotificationIntegrationModal/Components/TabComponent.jsx similarity index 95% rename from client/src/Components/NotificationIntegrationModal/Components/TabComponent.jsx rename to client/src/Components/v1/NotificationIntegrationModal/Components/TabComponent.jsx index 571379c7e..87fb2c301 100644 --- a/client/src/Components/NotificationIntegrationModal/Components/TabComponent.jsx +++ b/client/src/Components/v1/NotificationIntegrationModal/Components/TabComponent.jsx @@ -2,8 +2,8 @@ import React from "react"; import { Typography, Box, Button, CircularProgress } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useTheme } from "@emotion/react"; -import TextInput from "../../../Components/Inputs/TextInput"; -import Checkbox from "../../../Components/Inputs/Checkbox"; +import TextInput from "../../Inputs/TextInput/index.jsx"; +import Checkbox from "../../Inputs/Checkbox/index.jsx"; const TabComponent = ({ type, diff --git a/client/src/Components/NotificationIntegrationModal/Components/TabPanel.jsx b/client/src/Components/v1/NotificationIntegrationModal/Components/TabPanel.jsx similarity index 100% rename from client/src/Components/NotificationIntegrationModal/Components/TabPanel.jsx rename to client/src/Components/v1/NotificationIntegrationModal/Components/TabPanel.jsx diff --git a/client/src/Components/NotificationIntegrationModal/Hooks/useNotification.js b/client/src/Components/v1/NotificationIntegrationModal/Hooks/useNotification.js similarity index 96% rename from client/src/Components/NotificationIntegrationModal/Hooks/useNotification.js rename to client/src/Components/v1/NotificationIntegrationModal/Hooks/useNotification.js index cdbc11598..c0712fa5f 100644 --- a/client/src/Components/NotificationIntegrationModal/Hooks/useNotification.js +++ b/client/src/Components/v1/NotificationIntegrationModal/Hooks/useNotification.js @@ -1,7 +1,7 @@ import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { networkService } from "../../../Utils/NetworkService"; -import { createToast } from "../../../Utils/toastUtils"; +import { networkService } from "../../../../Utils/NetworkService.js"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; // Define constants for notification types to avoid magic values const NOTIFICATION_TYPES = { diff --git a/client/src/Components/PageStateWrapper/index.jsx b/client/src/Components/v1/PageStateWrapper/index.jsx similarity index 90% rename from client/src/Components/PageStateWrapper/index.jsx rename to client/src/Components/v1/PageStateWrapper/index.jsx index c335fcbb3..ac97704df 100644 --- a/client/src/Components/PageStateWrapper/index.jsx +++ b/client/src/Components/v1/PageStateWrapper/index.jsx @@ -1,9 +1,9 @@ -import GenericFallback from "../GenericFallback"; -import Fallback from "../Fallback"; +import GenericFallback from "../GenericFallback/index.jsx"; +import Fallback from "../Fallback/index.jsx"; import { Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import { useIsAdmin } from "../../Hooks/v1/useIsAdmin.js"; +import { useIsAdmin } from "../../../Hooks/v1/useIsAdmin.js"; import PropTypes from "prop-types"; const PageStateWrapper = ({ networkError, diff --git a/client/src/Components/ProgressBars/index.css b/client/src/Components/v1/ProgressBars/index.css similarity index 100% rename from client/src/Components/ProgressBars/index.css rename to client/src/Components/v1/ProgressBars/index.css diff --git a/client/src/Components/ProgressBars/index.jsx b/client/src/Components/v1/ProgressBars/index.jsx similarity index 100% rename from client/src/Components/ProgressBars/index.jsx rename to client/src/Components/v1/ProgressBars/index.jsx diff --git a/client/src/Components/ProgressStepper/index.css b/client/src/Components/v1/ProgressStepper/index.css similarity index 100% rename from client/src/Components/ProgressStepper/index.css rename to client/src/Components/v1/ProgressStepper/index.css diff --git a/client/src/Components/ProgressStepper/index.jsx b/client/src/Components/v1/ProgressStepper/index.jsx similarity index 100% rename from client/src/Components/ProgressStepper/index.jsx rename to client/src/Components/v1/ProgressStepper/index.jsx diff --git a/client/src/Components/ProtectedRoute/index.jsx b/client/src/Components/v1/ProtectedRoute/index.jsx similarity index 100% rename from client/src/Components/ProtectedRoute/index.jsx rename to client/src/Components/v1/ProtectedRoute/index.jsx diff --git a/client/src/Components/RoleProtectedRoute/index.jsx b/client/src/Components/v1/RoleProtectedRoute/index.jsx similarity index 100% rename from client/src/Components/RoleProtectedRoute/index.jsx rename to client/src/Components/v1/RoleProtectedRoute/index.jsx diff --git a/client/src/Components/ShareComponent/index.jsx b/client/src/Components/v1/ShareComponent/index.jsx similarity index 100% rename from client/src/Components/ShareComponent/index.jsx rename to client/src/Components/v1/ShareComponent/index.jsx diff --git a/client/src/Components/Sidebar/components/authFooter.jsx b/client/src/Components/v1/Sidebar/components/authFooter.jsx similarity index 96% rename from client/src/Components/Sidebar/components/authFooter.jsx rename to client/src/Components/v1/Sidebar/components/authFooter.jsx index e94aeddee..2c33a2402 100644 --- a/client/src/Components/Sidebar/components/authFooter.jsx +++ b/client/src/Components/v1/Sidebar/components/authFooter.jsx @@ -3,19 +3,19 @@ import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import Tooltip from "@mui/material/Tooltip"; import IconButton from "@mui/material/IconButton"; -import Avatar from "../../Avatar"; +import Avatar from "../../Avatar/index.jsx"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; import Divider from "@mui/material/Divider"; -import DotsVertical from "../../../assets/icons/dots-vertical.svg?react"; -import LogoutSvg from "../../../assets/icons/logout.svg?react"; +import DotsVertical from "../../../../assets/icons/dots-vertical.svg?react"; +import LogoutSvg from "../../../../assets/icons/logout.svg?react"; import { useTheme } from "@emotion/react"; import { useSelector } from "react-redux"; import { useTranslation } from "react-i18next"; import { useState } from "react"; import { useNavigate } from "react-router"; -import { clearAuthState } from "../../../Features/Auth/authSlice"; +import { clearAuthState } from "../../../../Features/Auth/authSlice.js"; import { useDispatch } from "react-redux"; import PropTypes from "prop-types"; diff --git a/client/src/Components/Sidebar/components/collapseButton.jsx b/client/src/Components/v1/Sidebar/components/collapseButton.jsx similarity index 88% rename from client/src/Components/Sidebar/components/collapseButton.jsx rename to client/src/Components/v1/Sidebar/components/collapseButton.jsx index e7c382e5a..e3a9c91f5 100644 --- a/client/src/Components/Sidebar/components/collapseButton.jsx +++ b/client/src/Components/v1/Sidebar/components/collapseButton.jsx @@ -1,9 +1,9 @@ import IconButton from "@mui/material/IconButton"; -import ArrowRight from "../../ArrowRight"; -import ArrowLeft from "../../ArrowLeft"; +import ArrowRight from "../../ArrowRight/index.jsx"; +import ArrowLeft from "../../ArrowLeft/index.jsx"; import { useTheme } from "@mui/material/styles"; import { useDispatch } from "react-redux"; -import { toggleSidebar } from "../../../Features/UI/uiSlice"; +import { toggleSidebar } from "../../../../Features/UI/uiSlice.js"; import PropTypes from "prop-types"; const CollapseButton = ({ collapsed }) => { diff --git a/client/src/Components/Sidebar/components/logo.jsx b/client/src/Components/v1/Sidebar/components/logo.jsx similarity index 100% rename from client/src/Components/Sidebar/components/logo.jsx rename to client/src/Components/v1/Sidebar/components/logo.jsx diff --git a/client/src/Components/Sidebar/components/navItem.jsx b/client/src/Components/v1/Sidebar/components/navItem.jsx similarity index 100% rename from client/src/Components/Sidebar/components/navItem.jsx rename to client/src/Components/v1/Sidebar/components/navItem.jsx diff --git a/client/src/Components/Sidebar/index.jsx b/client/src/Components/v1/Sidebar/index.jsx similarity index 75% rename from client/src/Components/Sidebar/index.jsx rename to client/src/Components/v1/Sidebar/index.jsx index 6b7b6577c..bd7400fa6 100644 --- a/client/src/Components/Sidebar/index.jsx +++ b/client/src/Components/v1/Sidebar/index.jsx @@ -1,29 +1,29 @@ import Stack from "@mui/material/Stack"; import List from "@mui/material/List"; -import Logo from "./components/logo"; -import CollapseButton from "./components/collapseButton"; +import Logo from "./components/logo.jsx"; +import CollapseButton from "./components/collapseButton.jsx"; import Divider from "@mui/material/Divider"; -import NavItem from "./components/navItem"; -import AuthFooter from "./components/authFooter"; +import NavItem from "./components/navItem.jsx"; +import AuthFooter from "./components/authFooter.jsx"; -import StarPrompt from "../StarPrompt"; -import LockSvg from "../../assets/icons/lock.svg?react"; -import UserSvg from "../../assets/icons/user.svg?react"; -import TeamSvg from "../../assets/icons/user-two.svg?react"; -import Support from "../../assets/icons/support.svg?react"; -import Maintenance from "../../assets/icons/maintenance.svg?react"; -import Monitors from "../../assets/icons/monitors.svg?react"; -import Incidents from "../../assets/icons/incidents.svg?react"; -import Integrations from "../../assets/icons/integrations.svg?react"; -import PageSpeed from "../../assets/icons/page-speed.svg?react"; -import Settings from "../../assets/icons/settings.svg?react"; -import ChangeLog from "../../assets/icons/changeLog.svg?react"; -import Docs from "../../assets/icons/docs.svg?react"; -import StatusPages from "../../assets/icons/status-pages.svg?react"; -import Discussions from "../../assets/icons/discussions.svg?react"; -import Notifications from "../../assets/icons/notifications.svg?react"; -import Logs from "../../assets/icons/logs.svg?react"; +import StarPrompt from "../StarPrompt/index.jsx"; +import LockSvg from "../../../assets/icons/lock.svg?react"; +import UserSvg from "../../../assets/icons/user.svg?react"; +import TeamSvg from "../../../assets/icons/user-two.svg?react"; +import Support from "../../../assets/icons/support.svg?react"; +import Maintenance from "../../../assets/icons/maintenance.svg?react"; +import Monitors from "../../../assets/icons/monitors.svg?react"; +import Incidents from "../../../assets/icons/incidents.svg?react"; +import Integrations from "../../../assets/icons/integrations.svg?react"; +import PageSpeed from "../../../assets/icons/page-speed.svg?react"; +import Settings from "../../../assets/icons/settings.svg?react"; +import ChangeLog from "../../../assets/icons/changeLog.svg?react"; +import Docs from "../../../assets/icons/docs.svg?react"; +import StatusPages from "../../../assets/icons/status-pages.svg?react"; +import Discussions from "../../../assets/icons/discussions.svg?react"; +import Notifications from "../../../assets/icons/notifications.svg?react"; +import Logs from "../../../assets/icons/logs.svg?react"; // Utils import { useTheme } from "@mui/material/styles"; diff --git a/client/src/Components/Skeletons/FullPage/index.jsx b/client/src/Components/v1/Skeletons/FullPage/index.jsx similarity index 100% rename from client/src/Components/Skeletons/FullPage/index.jsx rename to client/src/Components/v1/Skeletons/FullPage/index.jsx diff --git a/client/src/Components/StandardContainer/index.jsx b/client/src/Components/v1/StandardContainer/index.jsx similarity index 100% rename from client/src/Components/StandardContainer/index.jsx rename to client/src/Components/v1/StandardContainer/index.jsx diff --git a/client/src/Components/StarPrompt/index.jsx b/client/src/Components/v1/StarPrompt/index.jsx similarity index 97% rename from client/src/Components/StarPrompt/index.jsx rename to client/src/Components/v1/StarPrompt/index.jsx index f5f130387..6151544c1 100644 --- a/client/src/Components/StarPrompt/index.jsx +++ b/client/src/Components/v1/StarPrompt/index.jsx @@ -4,7 +4,7 @@ import CloseIcon from "@mui/icons-material/Close"; import { useTheme } from "@emotion/react"; import { useSelector, useDispatch } from "react-redux"; import { useTranslation } from "react-i18next"; -import { setStarPromptOpen } from "../../Features/UI/uiSlice"; +import { setStarPromptOpen } from "../../../Features/UI/uiSlice.js"; const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" }) => { const theme = useTheme(); diff --git a/client/src/Components/StatBox/index.jsx b/client/src/Components/v1/StatBox/index.jsx similarity index 97% rename from client/src/Components/StatBox/index.jsx rename to client/src/Components/v1/StatBox/index.jsx index 15eb61f74..1b5f1cf6f 100644 --- a/client/src/Components/StatBox/index.jsx +++ b/client/src/Components/v1/StatBox/index.jsx @@ -1,8 +1,8 @@ import { Stack, Typography } from "@mui/material"; -import Image from "../Image"; +import Image from "../Image/index.jsx"; import { useTheme } from "@mui/material/styles"; import PropTypes from "prop-types"; -import { useMonitorUtils } from "../../Hooks/v1/useMonitorUtils.js"; +import { useMonitorUtils } from "../../../Hooks/v1/useMonitorUtils.js"; /** * StatBox Component diff --git a/client/src/Components/StatusBoxes/index.jsx b/client/src/Components/v1/StatusBoxes/index.jsx similarity index 93% rename from client/src/Components/StatusBoxes/index.jsx rename to client/src/Components/v1/StatusBoxes/index.jsx index d97bb6211..a78337ba7 100644 --- a/client/src/Components/StatusBoxes/index.jsx +++ b/client/src/Components/v1/StatusBoxes/index.jsx @@ -1,6 +1,6 @@ // Components import { Stack } from "@mui/material"; -import SkeletonLayout from "./skeleton"; +import SkeletonLayout from "./skeleton.jsx"; // Utils import { useTheme } from "@mui/material/styles"; import PropTypes from "prop-types"; diff --git a/client/src/Components/StatusBoxes/skeleton.jsx b/client/src/Components/v1/StatusBoxes/skeleton.jsx similarity index 100% rename from client/src/Components/StatusBoxes/skeleton.jsx rename to client/src/Components/v1/StatusBoxes/skeleton.jsx diff --git a/client/src/Components/Subheader/index.jsx b/client/src/Components/v1/Subheader/index.jsx similarity index 100% rename from client/src/Components/Subheader/index.jsx rename to client/src/Components/v1/Subheader/index.jsx diff --git a/client/src/Components/Tab/index.jsx b/client/src/Components/v1/Tab/index.jsx similarity index 100% rename from client/src/Components/Tab/index.jsx rename to client/src/Components/v1/Tab/index.jsx diff --git a/client/src/Components/Table/TablePagination/Actions/index.jsx b/client/src/Components/v1/Table/TablePagination/Actions/index.jsx similarity index 95% rename from client/src/Components/Table/TablePagination/Actions/index.jsx rename to client/src/Components/v1/Table/TablePagination/Actions/index.jsx index dec72446b..0df62878e 100644 --- a/client/src/Components/Table/TablePagination/Actions/index.jsx +++ b/client/src/Components/v1/Table/TablePagination/Actions/index.jsx @@ -1,7 +1,7 @@ import PropTypes from "prop-types"; import { Box, Button } from "@mui/material"; -import LeftArrow from "../../../ArrowLeft"; -import RightArrow from "../../../ArrowRight"; +import LeftArrow from "../../../ArrowLeft/index.jsx"; +import RightArrow from "../../../ArrowRight/index.jsx"; import { useTheme } from "@emotion/react"; TablePaginationActions.propTypes = { diff --git a/client/src/Components/Table/TablePagination/index.jsx b/client/src/Components/v1/Table/TablePagination/index.jsx similarity index 96% rename from client/src/Components/Table/TablePagination/index.jsx rename to client/src/Components/v1/Table/TablePagination/index.jsx index 44b127d45..8a5af0cd5 100644 --- a/client/src/Components/Table/TablePagination/index.jsx +++ b/client/src/Components/v1/Table/TablePagination/index.jsx @@ -1,8 +1,8 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { Stack, TablePagination, Typography } from "@mui/material"; -import { TablePaginationActions } from "./Actions"; -import SelectorVertical from "../../../assets/icons/selector-vertical.svg?react"; +import { TablePaginationActions } from "./Actions/index.jsx"; +import SelectorVertical from "../../../../assets/icons/selector-vertical.svg?react"; Pagination.propTypes = { paginationLabel: PropTypes.string, // Label for the pagination. diff --git a/client/src/Components/Table/TableUtils.js b/client/src/Components/v1/Table/TableUtils.js similarity index 100% rename from client/src/Components/Table/TableUtils.js rename to client/src/Components/v1/Table/TableUtils.js diff --git a/client/src/Components/Table/index.jsx b/client/src/Components/v1/Table/index.jsx similarity index 99% rename from client/src/Components/Table/index.jsx rename to client/src/Components/v1/Table/index.jsx index bfafb5387..2113a2215 100644 --- a/client/src/Components/Table/index.jsx +++ b/client/src/Components/v1/Table/index.jsx @@ -8,7 +8,7 @@ import { TableRow, } from "@mui/material"; import Tooltip from "@mui/material/Tooltip"; -import SkeletonLayout from "./skeleton"; +import SkeletonLayout from "./skeleton.jsx"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; diff --git a/client/src/Components/Table/skeleton.jsx b/client/src/Components/v1/Table/skeleton.jsx similarity index 100% rename from client/src/Components/Table/skeleton.jsx rename to client/src/Components/v1/Table/skeleton.jsx diff --git a/client/src/Components/TextLink/index.jsx b/client/src/Components/v1/TextLink/index.jsx similarity index 100% rename from client/src/Components/TextLink/index.jsx rename to client/src/Components/v1/TextLink/index.jsx diff --git a/client/src/Components/ThemeSwitch/SunAndMoonIcon.jsx b/client/src/Components/v1/ThemeSwitch/SunAndMoonIcon.jsx similarity index 100% rename from client/src/Components/ThemeSwitch/SunAndMoonIcon.jsx rename to client/src/Components/v1/ThemeSwitch/SunAndMoonIcon.jsx diff --git a/client/src/Components/ThemeSwitch/index.css b/client/src/Components/v1/ThemeSwitch/index.css similarity index 100% rename from client/src/Components/ThemeSwitch/index.css rename to client/src/Components/v1/ThemeSwitch/index.css diff --git a/client/src/Components/ThemeSwitch/index.jsx b/client/src/Components/v1/ThemeSwitch/index.jsx similarity index 91% rename from client/src/Components/ThemeSwitch/index.jsx rename to client/src/Components/v1/ThemeSwitch/index.jsx index 5efb17419..60763da90 100644 --- a/client/src/Components/ThemeSwitch/index.jsx +++ b/client/src/Components/v1/ThemeSwitch/index.jsx @@ -10,9 +10,9 @@ */ import { IconButton } from "@mui/material"; -import SunAndMoonIcon from "./SunAndMoonIcon"; +import SunAndMoonIcon from "./SunAndMoonIcon.jsx"; import { useDispatch, useSelector } from "react-redux"; -import { setMode } from "../../Features/UI/uiSlice"; +import { setMode } from "../../../Features/UI/uiSlice.js"; import "./index.css"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Components/Toast/body.jsx b/client/src/Components/v1/Toast/body.jsx similarity index 100% rename from client/src/Components/Toast/body.jsx rename to client/src/Components/v1/Toast/body.jsx diff --git a/client/src/Components/Toast/index.jsx b/client/src/Components/v1/Toast/index.jsx similarity index 98% rename from client/src/Components/Toast/index.jsx rename to client/src/Components/v1/Toast/index.jsx index 36df0e631..5d66fca3f 100644 --- a/client/src/Components/Toast/index.jsx +++ b/client/src/Components/v1/Toast/index.jsx @@ -2,7 +2,7 @@ import Stack from "@mui/material/Stack"; import IconButton from "@mui/material/IconButton"; import Typography from "@mui/material/Typography"; import Button from "@mui/material/Button"; -import ToastBody from "./body"; +import ToastBody from "./body.jsx"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined"; import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined"; diff --git a/client/src/Components/Inputs/TextInput/indexV2.tsx b/client/src/Components/v2/Inputs/TextInput.tsx similarity index 100% rename from client/src/Components/Inputs/TextInput/indexV2.tsx rename to client/src/Components/v2/Inputs/TextInput.tsx diff --git a/client/src/Pages/v1/About/index.jsx b/client/src/Pages/v1/About/index.jsx index 0e68089d3..ff7bb6c6b 100644 --- a/client/src/Pages/v1/About/index.jsx +++ b/client/src/Pages/v1/About/index.jsx @@ -1,6 +1,6 @@ import React from "react"; -import AppBar from "../../../Components/Common/AppBar.jsx"; -import Footer from "../../../Components/Common/Footer.jsx"; +import AppBar from "@/Components/v1/Common/AppBar.jsx"; +import Footer from "@/Components/v1/Common/Footer.jsx"; import { useTranslation } from "react-i18next"; const AboutUs = () => { diff --git a/client/src/Pages/v1/Account/EditUser/index.jsx b/client/src/Pages/v1/Account/EditUser/index.jsx index 658de5144..e2e976916 100644 --- a/client/src/Pages/v1/Account/EditUser/index.jsx +++ b/client/src/Pages/v1/Account/EditUser/index.jsx @@ -2,9 +2,9 @@ import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; -import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; -import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; -import Search from "../../../../Components/Inputs/Search/index.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; +import Search from "@/Components/v1/Inputs/Search/index.jsx"; import Button from "@mui/material/Button"; import RoleTable from "../components/RoleTable/index.jsx"; diff --git a/client/src/Pages/v1/Account/components/AddTeamMember/index.jsx b/client/src/Pages/v1/Account/components/AddTeamMember/index.jsx index e17ed433b..219658b63 100644 --- a/client/src/Pages/v1/Account/components/AddTeamMember/index.jsx +++ b/client/src/Pages/v1/Account/components/AddTeamMember/index.jsx @@ -1,7 +1,7 @@ import { Button, Stack } from "@mui/material"; -import { GenericDialog } from "../../../../../Components/Dialog/genericDialog.jsx"; -import TextInput from "../../../../../Components/Inputs/TextInput/index.jsx"; -import Select from "../../../../../Components/Inputs/Select/index.jsx"; +import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; +import Select from "@/Components/v1/Inputs/Select/index.jsx"; import { useGetInviteToken } from "../../../../../Hooks/v1/inviteHooks.js"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; @@ -10,7 +10,7 @@ import { useState } from "react"; import PasswordTooltip from "../../../Auth/components/PasswordTooltip.jsx"; import useAddTeamMember from "./hooks/useAddTeamMember.jsx"; import usePasswordFeedback from "../../../Auth/hooks/usePasswordFeedback.jsx"; -import { PasswordEndAdornment } from "../../../../../Components/Inputs/TextInput/Adornments/index.jsx"; +import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; import PropTypes from "prop-types"; const INITIAL_FORM_STATE = { diff --git a/client/src/Pages/v1/Account/components/PasswordPanel.jsx b/client/src/Pages/v1/Account/components/PasswordPanel.jsx index 11961bf58..ca2af9205 100644 --- a/client/src/Pages/v1/Account/components/PasswordPanel.jsx +++ b/client/src/Pages/v1/Account/components/PasswordPanel.jsx @@ -2,10 +2,10 @@ import TabPanel from "@mui/lab/TabPanel"; import { useState } from "react"; import { useTheme } from "@emotion/react"; import { Box, Stack, Typography, Button } from "@mui/material"; -import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments/index.jsx"; -import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; +import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; import { newOrChangedCredentials } from "../../../../Validation/validation.js"; -import Alert from "../../../../Components/Alert/index.jsx"; +import Alert from "@/Components/v1/Alert/index.jsx"; import { update } from "../../../../Features/Auth/authSlice.js"; import { useDispatch, useSelector } from "react-redux"; import { createToast } from "../../../../Utils/toastUtils.jsx"; diff --git a/client/src/Pages/v1/Account/components/ProfilePanel.jsx b/client/src/Pages/v1/Account/components/ProfilePanel.jsx index 1ce1d34da..2035861a2 100644 --- a/client/src/Pages/v1/Account/components/ProfilePanel.jsx +++ b/client/src/Pages/v1/Account/components/ProfilePanel.jsx @@ -2,9 +2,9 @@ import { useTheme } from "@emotion/react"; import { useState } from "react"; import TabPanel from "@mui/lab/TabPanel"; import { Box, Button, Divider, Stack, Typography } from "@mui/material"; -import Avatar from "../../../../Components/Avatar/index.jsx"; -import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; -import ImageUpload from "../../../../Components/Inputs/ImageUpload/index.jsx"; +import Avatar from "@/Components/v1/Avatar/index.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; +import ImageUpload from "@/Components/v1/Inputs/ImageUpload/index.jsx"; import { newOrChangedCredentials } from "../../../../Validation/validation.js"; import { useDispatch, useSelector } from "react-redux"; import { @@ -14,8 +14,8 @@ import { } from "../../../../Features/Auth/authSlice.js"; import { createToast } from "../../../../Utils/toastUtils.jsx"; import { logger } from "../../../../Utils/Logger.js"; -import { GenericDialog } from "../../../../Components/Dialog/genericDialog.jsx"; -import Dialog from "../../../../Components/Dialog/index.jsx"; +import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; +import Dialog from "@/Components/v1/Dialog/index.jsx"; import { useTranslation } from "react-i18next"; /** diff --git a/client/src/Pages/v1/Account/components/RoleTable/index.jsx b/client/src/Pages/v1/Account/components/RoleTable/index.jsx index c42be7bcd..02dcaaf0d 100644 --- a/client/src/Pages/v1/Account/components/RoleTable/index.jsx +++ b/client/src/Pages/v1/Account/components/RoleTable/index.jsx @@ -1,5 +1,5 @@ import Typography from "@mui/material/Typography"; -import DataTable from "../../../../../Components/Table/index.jsx"; +import DataTable from "@/Components/v1/Table/index.jsx"; import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/v1/Account/components/TeamPanel.jsx b/client/src/Pages/v1/Account/components/TeamPanel.jsx index 8964cd277..02918eb30 100644 --- a/client/src/Pages/v1/Account/components/TeamPanel.jsx +++ b/client/src/Pages/v1/Account/components/TeamPanel.jsx @@ -3,14 +3,14 @@ import TabPanel from "@mui/lab/TabPanel"; import { Button, ButtonGroup, Stack, Typography } from "@mui/material"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; import { newOrChangedCredentials } from "../../../../Validation/validation.js"; import { networkService } from "../../../../main.jsx"; import { createToast } from "../../../../Utils/toastUtils.jsx"; -import Select from "../../../../Components/Inputs/Select/index.jsx"; -import { GenericDialog } from "../../../../Components/Dialog/genericDialog.jsx"; +import Select from "@/Components/v1/Inputs/Select/index.jsx"; +import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; import AddTeamMember from "./AddTeamMember/index.jsx"; -import DataTable from "../../../../Components/Table/index.jsx"; +import DataTable from "@/Components/v1/Table/index.jsx"; import { useGetInviteToken } from "../../../../Hooks/v1/inviteHooks.js"; import { useNavigate } from "react-router-dom"; import { useIsSuperAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; diff --git a/client/src/Pages/v1/Account/index.jsx b/client/src/Pages/v1/Account/index.jsx index a496858b7..ae16ff9d7 100644 --- a/client/src/Pages/v1/Account/index.jsx +++ b/client/src/Pages/v1/Account/index.jsx @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import { useNavigate } from "react-router-dom"; import { useSelector } from "react-redux"; import { Box, Tab, useTheme } from "@mui/material"; -import CustomTabList from "../../../Components/Tab/index.jsx"; +import CustomTabList from "@/Components/v1/Tab/index.jsx"; import TabContext from "@mui/lab/TabContext"; import ProfilePanel from "./components/ProfilePanel.jsx"; import PasswordPanel from "./components/PasswordPanel.jsx"; diff --git a/client/src/Pages/v1/Auth/CheckEmail.jsx b/client/src/Pages/v1/Auth/CheckEmail.jsx index 95217bcb5..c12a84804 100644 --- a/client/src/Pages/v1/Auth/CheckEmail.jsx +++ b/client/src/Pages/v1/Auth/CheckEmail.jsx @@ -9,7 +9,7 @@ import { Trans, useTranslation } from "react-i18next"; import Background from "../../../assets/Images/background-grid.svg?react"; import EmailIcon from "../../../assets/icons/email.svg?react"; import Logo from "../../../assets/icons/checkmate-icon.svg?react"; -import IconBox from "../../../Components/IconBox/index.jsx"; +import IconBox from "@/Components/v1/IconBox/index.jsx"; import "./index.css"; const CheckEmail = () => { diff --git a/client/src/Pages/v1/Auth/ForgotPassword.jsx b/client/src/Pages/v1/Auth/ForgotPassword.jsx index 9836f5d77..8cbd85d8a 100644 --- a/client/src/Pages/v1/Auth/ForgotPassword.jsx +++ b/client/src/Pages/v1/Auth/ForgotPassword.jsx @@ -6,11 +6,11 @@ import { forgotPassword } from "../../../Features/Auth/authSlice.js"; import { useEffect, useState } from "react"; import { newOrChangedCredentials } from "../../../Validation/validation.js"; import { useNavigate } from "react-router-dom"; -import TextInput from "../../../Components/Inputs/TextInput/index.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; import Logo from "../../../assets/icons/checkmate-icon.svg?react"; import Key from "../../../assets/icons/key.svg?react"; import Background from "../../../assets/Images/background-grid.svg?react"; -import IconBox from "../../../Components/IconBox/index.jsx"; +import IconBox from "@/Components/v1/IconBox/index.jsx"; import { Trans, useTranslation } from "react-i18next"; import "./index.css"; diff --git a/client/src/Pages/v1/Auth/Login/index.jsx b/client/src/Pages/v1/Auth/Login/index.jsx index 901edcd8b..7f5dcb5e4 100644 --- a/client/src/Pages/v1/Auth/Login/index.jsx +++ b/client/src/Pages/v1/Auth/Login/index.jsx @@ -1,9 +1,9 @@ // Components import Stack from "@mui/material/Stack"; import Button from "@mui/material/Button"; -import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; -import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments/index.jsx"; -import TextLink from "../../../../Components/TextLink/index.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; +import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; +import TextLink from "@/Components/v1/TextLink/index.jsx"; import AuthPageWrapper from "../components/AuthPageWrapper.jsx"; // Utils import { useTheme } from "@mui/material/styles"; diff --git a/client/src/Pages/v1/Auth/NewPasswordConfirmed.jsx b/client/src/Pages/v1/Auth/NewPasswordConfirmed.jsx index 2bede1ae4..629494098 100644 --- a/client/src/Pages/v1/Auth/NewPasswordConfirmed.jsx +++ b/client/src/Pages/v1/Auth/NewPasswordConfirmed.jsx @@ -6,7 +6,7 @@ import { clearAuthState } from "../../../Features/Auth/authSlice.js"; import Background from "../../../assets/Images/background-grid.svg?react"; import ConfirmIcon from "../../../assets/icons/check-outlined.svg?react"; import Logo from "../../../assets/icons/checkmate-icon.svg?react"; -import IconBox from "../../../Components/IconBox/index.jsx"; +import IconBox from "@/Components/v1/IconBox/index.jsx"; import { Trans, useTranslation } from "react-i18next"; import "./index.css"; diff --git a/client/src/Pages/v1/Auth/Register/index.jsx b/client/src/Pages/v1/Auth/Register/index.jsx index a903602d4..d6515b85c 100644 --- a/client/src/Pages/v1/Auth/Register/index.jsx +++ b/client/src/Pages/v1/Auth/Register/index.jsx @@ -1,7 +1,7 @@ // Components import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; import Button from "@mui/material/Button"; import Box from "@mui/material/Box"; import PasswordTooltip from "../components/PasswordTooltip.jsx"; diff --git a/client/src/Pages/v1/Auth/SetNewPassword.jsx b/client/src/Pages/v1/Auth/SetNewPassword.jsx index e9df89d88..45f3351f3 100644 --- a/client/src/Pages/v1/Auth/SetNewPassword.jsx +++ b/client/src/Pages/v1/Auth/SetNewPassword.jsx @@ -7,10 +7,10 @@ import { Box, Stack, Typography, Button } from "@mui/material"; import { setNewPassword } from "../../../Features/Auth/authSlice.js"; import { createToast } from "../../../Utils/toastUtils.jsx"; import { newOrChangedCredentials } from "../../../Validation/validation.js"; -import Check from "../../../Components/Check/Check.jsx"; -import TextInput from "../../../Components/Inputs/TextInput/index.jsx"; -import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments/index.jsx"; -import IconBox from "../../../Components/IconBox/index.jsx"; +import Check from "@/Components/v1/Check/Check.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; +import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; +import IconBox from "@/Components/v1/IconBox/index.jsx"; import LockIcon from "../../../assets/icons/lock.svg?react"; import Logo from "../../../assets/icons/checkmate-icon.svg?react"; import Background from "../../../assets/Images/background-grid.svg?react"; diff --git a/client/src/Pages/v1/Auth/components/AuthHeader.jsx b/client/src/Pages/v1/Auth/components/AuthHeader.jsx index 067c47d92..a34e83d6a 100644 --- a/client/src/Pages/v1/Auth/components/AuthHeader.jsx +++ b/client/src/Pages/v1/Auth/components/AuthHeader.jsx @@ -3,7 +3,7 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import Logo from "../../../../assets/icons/checkmate-icon.svg?react"; import LanguageSelector from "../../../../Components/LanguageSelector.jsx"; -import ThemeSwitch from "../../../../Components/ThemeSwitch/index.jsx"; +import ThemeSwitch from "@/Components/v1/ThemeSwitch/index.jsx"; // Utils import { useTheme } from "@mui/material/styles"; diff --git a/client/src/Pages/v1/Auth/components/PasswordTooltip.jsx b/client/src/Pages/v1/Auth/components/PasswordTooltip.jsx index ff3c2f385..2881eb001 100644 --- a/client/src/Pages/v1/Auth/components/PasswordTooltip.jsx +++ b/client/src/Pages/v1/Auth/components/PasswordTooltip.jsx @@ -1,4 +1,4 @@ -import Check from "../../../../Components/Check/Check.jsx"; +import Check from "@/Components/v1/Check/Check.jsx"; import Stack from "@mui/material/Stack"; import { Tooltip, useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/v1/Incidents/Components/IncidentTable/index.jsx b/client/src/Pages/v1/Incidents/Components/IncidentTable/index.jsx index e204d7167..df457c0e2 100644 --- a/client/src/Pages/v1/Incidents/Components/IncidentTable/index.jsx +++ b/client/src/Pages/v1/Incidents/Components/IncidentTable/index.jsx @@ -1,16 +1,16 @@ //Components import Stack from "@mui/material/Stack"; -import DataTable from "../../../../../Components/Table/index.jsx"; +import DataTable from "@/Components/v1/Table/index.jsx"; import Table from "@mui/material/Table"; import TableBody from "@mui/material/TableBody"; import TableRow from "@mui/material/TableRow"; import TableCell from "@mui/material/TableCell"; -import TableSkeleton from "../../../../../Components/Table/skeleton.jsx"; -import Pagination from "../../../../../Components/Table/TablePagination/index.jsx"; -import { StatusLabel } from "../../../../../Components/Label/index.jsx"; -import { HttpStatusLabel } from "../../../../../Components/HttpStatusLabel/index.jsx"; -import GenericFallback from "../../../../../Components/GenericFallback/index.jsx"; -import NetworkError from "../../../../../Components/GenericFallback/NetworkError.jsx"; +import TableSkeleton from "@/Components/v1/Table/skeleton.jsx"; +import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; +import { StatusLabel } from "@/Components/v1/Label/index.jsx"; +import { HttpStatusLabel } from "@/Components/v1/HttpStatusLabel/index.jsx"; +import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; +import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; //Utils import { formatDateWithTz } from "../../../../../Utils/timeUtils.js"; diff --git a/client/src/Pages/v1/Incidents/Components/OptionsHeader/index.jsx b/client/src/Pages/v1/Incidents/Components/OptionsHeader/index.jsx index b1bde1175..c16f54f18 100644 --- a/client/src/Pages/v1/Incidents/Components/OptionsHeader/index.jsx +++ b/client/src/Pages/v1/Incidents/Components/OptionsHeader/index.jsx @@ -1,6 +1,6 @@ // Components import { Stack, Typography, Button, ButtonGroup } from "@mui/material"; -import Select from "../../../../../Components/Inputs/Select/index.jsx"; +import Select from "@/Components/v1/Inputs/Select/index.jsx"; import PropTypes from "prop-types"; //Utils diff --git a/client/src/Pages/v1/Incidents/index.jsx b/client/src/Pages/v1/Incidents/index.jsx index b9281f97c..3c0d7f60f 100644 --- a/client/src/Pages/v1/Incidents/index.jsx +++ b/client/src/Pages/v1/Incidents/index.jsx @@ -1,7 +1,7 @@ // Components import { Stack } from "@mui/material"; -import Breadcrumbs from "../../../Components/Breadcrumbs/index.jsx"; -import GenericFallback from "../../../Components/GenericFallback/index.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; +import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; import IncidentTable from "./Components/IncidentTable/index.jsx"; import OptionsHeader from "./Components/OptionsHeader/index.jsx"; import StatusBoxes from "./Components/StatusBoxes/index.jsx"; @@ -13,7 +13,7 @@ import { useFetchMonitorsByTeamId } from "../../../Hooks/v1/monitorHooks.js"; import { useFetchChecksSummaryByTeamId } from "../../../Hooks/v1/checkHooks.js"; import { useAcknowledgeChecks } from "../../../Hooks/v1/checkHooks.js"; import { useState, useEffect } from "react"; -import NetworkError from "../../../Components/GenericFallback/NetworkError.jsx"; +import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; diff --git a/client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx b/client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx index 299331a79..5fe1d8984 100644 --- a/client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx +++ b/client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx @@ -1,4 +1,4 @@ -import ConfigBox from "../../../../../Components/ConfigBox/index.jsx"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; import { Box, Stack, Typography } from "@mui/material"; import { CustomThreshold } from "./CustomThreshold/index.jsx"; import { capitalizeFirstLetter } from "../../../../../Utils/stringUtils.js"; diff --git a/client/src/Pages/v1/Infrastructure/Create/Components/CustomThreshold/index.jsx b/client/src/Pages/v1/Infrastructure/Create/Components/CustomThreshold/index.jsx index 4edc0b766..d783f7985 100644 --- a/client/src/Pages/v1/Infrastructure/Create/Components/CustomThreshold/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Create/Components/CustomThreshold/index.jsx @@ -1,6 +1,6 @@ import { Box, Stack, Typography } from "@mui/material"; -import TextInput from "../../../../../../Components/Inputs/TextInput/index.jsx"; -import Checkbox from "../../../../../../Components/Inputs/Checkbox/index.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; +import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; import { useTheme } from "@emotion/react"; import PropTypes from "prop-types"; diff --git a/client/src/Pages/v1/Infrastructure/Create/Components/MonitorActionButtons.jsx b/client/src/Pages/v1/Infrastructure/Create/Components/MonitorActionButtons.jsx index 16df8ad01..871fe0d7b 100644 --- a/client/src/Pages/v1/Infrastructure/Create/Components/MonitorActionButtons.jsx +++ b/client/src/Pages/v1/Infrastructure/Create/Components/MonitorActionButtons.jsx @@ -4,7 +4,7 @@ import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline"; import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; -import Dialog from "../../../../../Components/Dialog/index.jsx"; +import Dialog from "@/Components/v1/Dialog/index.jsx"; import PropTypes from "prop-types"; const MonitorActionButtons = ({ monitor, isBusy, handlePause, handleRemove }) => { diff --git a/client/src/Pages/v1/Infrastructure/Create/Components/MonitorStatusHeader.jsx b/client/src/Pages/v1/Infrastructure/Create/Components/MonitorStatusHeader.jsx index 4313c34c0..570f38a7e 100644 --- a/client/src/Pages/v1/Infrastructure/Create/Components/MonitorStatusHeader.jsx +++ b/client/src/Pages/v1/Infrastructure/Create/Components/MonitorStatusHeader.jsx @@ -2,7 +2,7 @@ import { Box, Stack, Tooltip, Typography } from "@mui/material"; import { useMonitorUtils } from "../../../../../Hooks/v1/useMonitorUtils.js"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import PulseDot from "../../../../../Components/Animated/PulseDot.jsx"; +import PulseDot from "@/Components/v1/Animated/PulseDot.jsx"; import PropTypes from "prop-types"; const MonitorStatusHeader = ({ monitor, infrastructureMonitor }) => { const theme = useTheme(); diff --git a/client/src/Pages/v1/Infrastructure/Create/index.jsx b/client/src/Pages/v1/Infrastructure/Create/index.jsx index 06ecdff6c..d4fd859ec 100644 --- a/client/src/Pages/v1/Infrastructure/Create/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Create/index.jsx @@ -1,17 +1,17 @@ //Components -import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; -import ConfigBox from "../../../../Components/ConfigBox/index.jsx"; -import FieldWrapper from "../../../../Components/Inputs/FieldWrapper/index.jsx"; -import Link from "../../../../Components/Link/index.jsx"; -import Select from "../../../../Components/Inputs/Select/index.jsx"; -import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; +import FieldWrapper from "@/Components/v1/Inputs/FieldWrapper/index.jsx"; +import Link from "@/Components/v1/Link/index.jsx"; +import Select from "@/Components/v1/Inputs/Select/index.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material"; -import { HttpAdornment } from "../../../../Components/Inputs/TextInput/Adornments/index.jsx"; +import { HttpAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; import MonitorStatusHeader from "./Components/MonitorStatusHeader.jsx"; import MonitorActionButtons from "./Components/MonitorActionButtons.jsx"; import CustomAlertsSection from "./Components/CustomAlertsSection.jsx"; // Utils -import NotificationsConfig from "../../../../Components/NotificationConfig/index.jsx"; +import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx"; import { useGetNotificationsByTeamId } from "../../../../Hooks/v1/useNotifications.js"; import { useParams } from "react-router-dom"; import { useState, useEffect } from "react"; diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx index 47f33a5e6..ee100bb0e 100644 --- a/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx +++ b/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx @@ -1,7 +1,7 @@ // Components import { Typography } from "@mui/material"; import BaseContainer from "../BaseContainer/index.jsx"; -import AreaChart from "../../../../../../Components/Charts/AreaChart/index.jsx"; +import AreaChart from "@/Components/v1/Charts/AreaChart/index.jsx"; // Utils import { useTheme } from "@emotion/react"; import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/index.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/index.jsx index 3dfb4edd8..3465683d9 100644 --- a/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/index.jsx @@ -9,7 +9,7 @@ import { TzTick, InfrastructureTooltip, TemperatureTooltip, -} from "../../../../../../Components/Charts/Utils/chartUtils.jsx"; +} from "@/Components/v1/Charts/Utils/chartUtils.jsx"; import { useTheme } from "@emotion/react"; import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx index 5c55119ad..cd61b7109 100644 --- a/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx +++ b/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx @@ -1,5 +1,5 @@ // Components -import CustomGauge from "../../../../../../Components/Charts/CustomGauge/index.jsx"; +import CustomGauge from "@/Components/v1/Charts/CustomGauge/index.jsx"; import BaseContainer from "../BaseContainer/index.jsx"; import { Stack, Typography, Box } from "@mui/material"; // Utils diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx index 5cfdba350..d9f93311c 100644 --- a/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx +++ b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx @@ -6,7 +6,7 @@ import { TzTick, InfrastructureTooltip, NetworkTick, -} from "../../../../../../Components/Charts/Utils/chartUtils.jsx"; +} from "@/Components/v1/Charts/Utils/chartUtils.jsx"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx index 52ccf0805..0747dfcd1 100644 --- a/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx +++ b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx @@ -1,6 +1,6 @@ import PropTypes from "prop-types"; -import StatusBoxes from "../../../../../../Components/StatusBoxes/index.jsx"; -import StatBox from "../../../../../../Components/StatBox/index.jsx"; +import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; +import StatBox from "@/Components/v1/StatBox/index.jsx"; import { Typography } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/index.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/index.jsx index e0d6986e1..78db333e6 100644 --- a/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/index.jsx @@ -3,10 +3,10 @@ import { useState, useEffect } from "react"; import { Box } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useTheme } from "@emotion/react"; -import Select from "../../../../../../Components/Inputs/Select/index.jsx"; +import Select from "@/Components/v1/Inputs/Select/index.jsx"; import NetworkStatBoxes from "./NetworkStatBoxes.jsx"; import NetworkCharts from "./NetworkCharts.jsx"; -import MonitorTimeFrameHeader from "../../../../../../Components/MonitorTimeFrameHeader/index.jsx"; +import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx"; const getAvailableInterfaces = (net) => { return (net || []).map((iface) => iface.name).filter(Boolean); diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/StatusBoxes/index.jsx b/client/src/Pages/v1/Infrastructure/Details/Components/StatusBoxes/index.jsx index 47862449e..7f09e82d5 100644 --- a/client/src/Pages/v1/Infrastructure/Details/Components/StatusBoxes/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Details/Components/StatusBoxes/index.jsx @@ -1,7 +1,7 @@ // Components import { Stack, Typography } from "@mui/material"; -import StatusBoxes from "../../../../../../Components/StatusBoxes/index.jsx"; -import StatBox from "../../../../../../Components/StatBox/index.jsx"; +import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; +import StatBox from "@/Components/v1/StatBox/index.jsx"; //Utils import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js"; diff --git a/client/src/Pages/v1/Infrastructure/Details/index.jsx b/client/src/Pages/v1/Infrastructure/Details/index.jsx index 04691d985..f9c51c27d 100644 --- a/client/src/Pages/v1/Infrastructure/Details/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Details/index.jsx @@ -1,14 +1,14 @@ // Components import { Stack, Typography, Tab } from "@mui/material"; -import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; -import MonitorDetailsControlHeader from "../../../../Components/MonitorDetailsControlHeader/index.jsx"; -import MonitorTimeFrameHeader from "../../../../Components/MonitorTimeFrameHeader/index.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; +import MonitorDetailsControlHeader from "@/Components/v1/MonitorDetailsControlHeader/index.jsx"; +import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx"; import StatusBoxes from "./Components/StatusBoxes/index.jsx"; import GaugeBoxes from "./Components/GaugeBoxes/index.jsx"; import AreaChartBoxes from "./Components/AreaChartBoxes/index.jsx"; -import GenericFallback from "../../../../Components/GenericFallback/index.jsx"; +import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; import NetworkStats from "./Components/NetworkStats/index.jsx"; -import CustomTabList from "../../../../Components/Tab/index.jsx"; +import CustomTabList from "@/Components/v1/Tab/index.jsx"; import TabContext from "@mui/lab/TabContext"; // Utils diff --git a/client/src/Pages/v1/Infrastructure/Monitors/Components/Filters/index.jsx b/client/src/Pages/v1/Infrastructure/Monitors/Components/Filters/index.jsx index bef632e88..70bf502af 100644 --- a/client/src/Pages/v1/Infrastructure/Monitors/Components/Filters/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Monitors/Components/Filters/index.jsx @@ -1,6 +1,6 @@ import { useTheme } from "@emotion/react"; import PropTypes from "prop-types"; -import FilterHeader from "../../../../../../Components/FilterHeader/index.jsx"; +import FilterHeader from "@/Components/v1/FilterHeader/index.jsx"; import { useMemo } from "react"; import { Box, Button } from "@mui/material"; import ClearIcon from "@mui/icons-material/Clear"; diff --git a/client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTable/index.jsx b/client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTable/index.jsx index 801a9bc6a..cc2cd5f98 100644 --- a/client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTable/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTable/index.jsx @@ -1,14 +1,14 @@ // Components import { Box } from "@mui/material"; -import DataTable from "../../../../../../Components/Table/index.jsx"; -import Host from "../../../../../../Components/Host/index.jsx"; -import { StatusLabel } from "../../../../../../Components/Label/index.jsx"; +import DataTable from "@/Components/v1/Table/index.jsx"; +import Host from "@/Components/v1/Host/index.jsx"; +import { StatusLabel } from "@/Components/v1/Label/index.jsx"; import { Stack } from "@mui/material"; import { InfrastructureMenu } from "../MonitorsTableMenu/index.jsx"; import LoadingSpinner from "../../../../Uptime/Monitors/Components/LoadingSpinner/index.jsx"; // Assets import CPUChipIcon from "../../../../../../assets/icons/cpu-chip.svg?react"; -import CustomGauge from "../../../../../../Components/Charts/CustomGauge/index.jsx"; +import CustomGauge from "@/Components/v1/Charts/CustomGauge/index.jsx"; // Utils import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx b/client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx index c2270c691..d3af34d00 100644 --- a/client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx @@ -7,7 +7,7 @@ import { createToast } from "../../../../../../Utils/toastUtils.jsx"; import { IconButton, Menu, MenuItem } from "@mui/material"; import Settings from "../../../../../../assets/icons/settings-bold.svg?react"; import PropTypes from "prop-types"; -import Dialog from "../../../../../../Components/Dialog/index.jsx"; +import Dialog from "@/Components/v1/Dialog/index.jsx"; import { networkService } from "../../../../../../Utils/NetworkService.js"; import { usePauseMonitor } from "../../../../../../Hooks/v1/monitorHooks.js"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/v1/Infrastructure/Monitors/index.jsx b/client/src/Pages/v1/Infrastructure/Monitors/index.jsx index 7fbfa1167..6c3a657ab 100644 --- a/client/src/Pages/v1/Infrastructure/Monitors/index.jsx +++ b/client/src/Pages/v1/Infrastructure/Monitors/index.jsx @@ -1,11 +1,11 @@ // Components import { Stack } from "@mui/material"; -import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; -import MonitorCountHeader from "../../../../Components/MonitorCountHeader/index.jsx"; -import MonitorCreateHeader from "../../../../Components/MonitorCreateHeader/index.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; +import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx"; +import MonitorCreateHeader from "@/Components/v1/MonitorCreateHeader/index.jsx"; import MonitorsTable from "./Components/MonitorsTable/index.jsx"; -import Pagination from "../../../../Components/Table/TablePagination/index.jsx"; -import PageStateWrapper from "../../../../Components/PageStateWrapper/index.jsx"; +import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; +import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; import Filter from "./Components/Filters/index.jsx"; import SearchComponent from "../../Uptime/Monitors/Components/SearchComponent/index.jsx"; // Utils diff --git a/client/src/Pages/v1/Logs/Diagnostics/components/gauges/index.jsx b/client/src/Pages/v1/Logs/Diagnostics/components/gauges/index.jsx index 082be2587..b38837cf2 100644 --- a/client/src/Pages/v1/Logs/Diagnostics/components/gauges/index.jsx +++ b/client/src/Pages/v1/Logs/Diagnostics/components/gauges/index.jsx @@ -1,5 +1,5 @@ import Stack from "@mui/material/Stack"; -import CustomGauge from "../../../../../../Components/Charts/CustomGauge/index.jsx"; +import CustomGauge from "@/Components/v1/Charts/CustomGauge/index.jsx"; import Typography from "@mui/material/Typography"; // Utils diff --git a/client/src/Pages/v1/Logs/Diagnostics/index.jsx b/client/src/Pages/v1/Logs/Diagnostics/index.jsx index d7a1a8a7c..54feee516 100644 --- a/client/src/Pages/v1/Logs/Diagnostics/index.jsx +++ b/client/src/Pages/v1/Logs/Diagnostics/index.jsx @@ -2,8 +2,8 @@ import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; import Gauges from "./components/gauges/index.jsx"; import Button from "@mui/material/Button"; -import StatBox from "../../../../Components/StatBox/index.jsx"; -import StatusBoxes from "../../../../Components/StatusBoxes/index.jsx"; +import StatBox from "@/Components/v1/StatBox/index.jsx"; +import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; import { useFetchDiagnostics } from "../../../../Hooks/v1/logHooks.js"; diff --git a/client/src/Pages/v1/Logs/Logs/index.jsx b/client/src/Pages/v1/Logs/Logs/index.jsx index 5c7ad9fa9..64c4cb97a 100644 --- a/client/src/Pages/v1/Logs/Logs/index.jsx +++ b/client/src/Pages/v1/Logs/Logs/index.jsx @@ -1,6 +1,6 @@ import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; -import Select from "../../../../Components/Inputs/Select/index.jsx"; +import Select from "@/Components/v1/Inputs/Select/index.jsx"; import Typography from "@mui/material/Typography"; import Divider from "@mui/material/Divider"; diff --git a/client/src/Pages/v1/Logs/Queue/components/FailedJobTable/index.jsx b/client/src/Pages/v1/Logs/Queue/components/FailedJobTable/index.jsx index 46be9520c..743f52639 100644 --- a/client/src/Pages/v1/Logs/Queue/components/FailedJobTable/index.jsx +++ b/client/src/Pages/v1/Logs/Queue/components/FailedJobTable/index.jsx @@ -1,6 +1,6 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import DataTable from "../../../../../../Components/Table/index.jsx"; +import DataTable from "@/Components/v1/Table/index.jsx"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/v1/Logs/Queue/components/JobTable/index.jsx b/client/src/Pages/v1/Logs/Queue/components/JobTable/index.jsx index 4ac4069bd..7d309e845 100644 --- a/client/src/Pages/v1/Logs/Queue/components/JobTable/index.jsx +++ b/client/src/Pages/v1/Logs/Queue/components/JobTable/index.jsx @@ -1,5 +1,5 @@ import Stack from "@mui/material/Stack"; -import DataTable from "../../../../../../Components/Table/index.jsx"; +import DataTable from "@/Components/v1/Table/index.jsx"; import Typography from "@mui/material/Typography"; // Utils import PropTypes from "prop-types"; @@ -7,7 +7,7 @@ import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; import { TypeToPathMap } from "../../../../../../Utils/monitorUtils.js"; import { useTranslation } from "react-i18next"; -import { createHeaderFactory } from "../../../../../../Components/Table/TableUtils.js"; +import { createHeaderFactory } from "@/Components/v1/Table/TableUtils.js"; const JobTable = ({ jobs = [] }) => { const theme = useTheme(); diff --git a/client/src/Pages/v1/Logs/Queue/components/Metrics/index.jsx b/client/src/Pages/v1/Logs/Queue/components/Metrics/index.jsx index dd9fdcc6a..c4f4389c3 100644 --- a/client/src/Pages/v1/Logs/Queue/components/Metrics/index.jsx +++ b/client/src/Pages/v1/Logs/Queue/components/Metrics/index.jsx @@ -1,6 +1,6 @@ import Stack from "@mui/material/Stack"; -import StatBox from "../../../../../../Components/StatBox/index.jsx"; -import StatusBoxes from "../../../../../../Components/StatusBoxes/index.jsx"; +import StatBox from "@/Components/v1/StatBox/index.jsx"; +import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; import { useTranslation } from "react-i18next"; import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/v1/Logs/Queue/components/MetricsTable/index.jsx b/client/src/Pages/v1/Logs/Queue/components/MetricsTable/index.jsx index 58f13fd5d..d96f37a20 100644 --- a/client/src/Pages/v1/Logs/Queue/components/MetricsTable/index.jsx +++ b/client/src/Pages/v1/Logs/Queue/components/MetricsTable/index.jsx @@ -1,6 +1,6 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import DataTable from "../../../../../../Components/Table/index.jsx"; +import DataTable from "@/Components/v1/Table/index.jsx"; // Utils import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/v1/Logs/index.jsx b/client/src/Pages/v1/Logs/index.jsx index c6dfe4d99..e5bdf623d 100644 --- a/client/src/Pages/v1/Logs/index.jsx +++ b/client/src/Pages/v1/Logs/index.jsx @@ -1,5 +1,5 @@ import Stack from "@mui/material/Stack"; -import Breadcrumbs from "../../../Components/Breadcrumbs/index.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; import Tabs from "@mui/material/Tabs"; import Tab from "@mui/material/Tab"; import Queue from "./Queue/index.jsx"; diff --git a/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx b/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx index b43bcbfc7..8d4c0ae53 100644 --- a/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx +++ b/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx @@ -1,4 +1,4 @@ -import Select from "../../../../../../Components/Inputs/Select/index.jsx"; +import Select from "@/Components/v1/Inputs/Select/index.jsx"; import PropTypes from "prop-types"; const ConfigSelect = ({ configSelection, valueSelect, onChange, ...props }) => { const getValueById = (config, id) => { diff --git a/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/MonitorsConfig/index.jsx b/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/MonitorsConfig/index.jsx index 91e6a1531..14920d81c 100644 --- a/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/MonitorsConfig/index.jsx +++ b/client/src/Pages/v1/Maintenance/CreateMaintenance/Components/MonitorsConfig/index.jsx @@ -1,7 +1,7 @@ import PropTypes from "prop-types"; -import Search from "../../../../../../Components/Inputs/Search/index.jsx"; +import Search from "@/Components/v1/Inputs/Search/index.jsx"; import { useState } from "react"; -import ConfigRow from "../../../../../../Components/ConfigRow/index.jsx"; +import ConfigRow from "@/Components/v1/ConfigRow/index.jsx"; /** * MonitorsConfig is a component that allows users to select and apply a maintenance window to a list of monitors. diff --git a/client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx b/client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx index da3e72d8a..d0e7b03cd 100644 --- a/client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx +++ b/client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx @@ -2,7 +2,7 @@ import { Box, Button, Stack, Typography } from "@mui/material"; import { useSelector } from "react-redux"; import { useTheme } from "@emotion/react"; import { useEffect, useState } from "react"; -import ConfigBox from "../../../../Components/ConfigBox/index.jsx"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { LocalizationProvider } from "@mui/x-date-pickers"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; @@ -14,11 +14,11 @@ import ConfigSelect from "./Components/ConfigSelect/index.jsx"; import useMaintenanceData from "./hooks/useMaintenanceData.jsx"; import useMaintenanceActions from "./hooks/useMaintenanceActions.jsx"; -import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; -import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; import CalendarIcon from "../../../../assets/icons/calendar.svg?react"; import "./index.css"; -import Search from "../../../../Components/Inputs/Search/index.jsx"; +import Search from "@/Components/v1/Inputs/Search/index.jsx"; import { logger } from "../../../../Utils/Logger.js"; import { useNavigate, useParams } from "react-router-dom"; diff --git a/client/src/Pages/v1/Maintenance/MaintenanceTable/ActionsMenu/index.jsx b/client/src/Pages/v1/Maintenance/MaintenanceTable/ActionsMenu/index.jsx index cc51697f7..b2654da49 100644 --- a/client/src/Pages/v1/Maintenance/MaintenanceTable/ActionsMenu/index.jsx +++ b/client/src/Pages/v1/Maintenance/MaintenanceTable/ActionsMenu/index.jsx @@ -10,7 +10,7 @@ import { networkService } from "../../../../../main.jsx"; import { createToast } from "../../../../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; -import Dialog from "../../../../../Components/Dialog/index.jsx"; +import Dialog from "@/Components/v1/Dialog/index.jsx"; const ActionsMenu = ({ /* isAdmin, */ maintenanceWindow, updateCallback }) => { maintenanceWindow; diff --git a/client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx b/client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx index fb18cd380..f1d44f944 100644 --- a/client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx +++ b/client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx @@ -1,14 +1,14 @@ import PropTypes from "prop-types"; import { Box } from "@mui/material"; -import DataTable from "../../../../Components/Table/index.jsx"; -import Pagination from "../../../../Components/Table/TablePagination/index.jsx"; +import DataTable from "@/Components/v1/Table/index.jsx"; +import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; import ActionsMenu from "./ActionsMenu/index.jsx"; import { memo } from "react"; import { useDispatch, useSelector } from "react-redux"; import { formatDurationRounded } from "../../../../Utils/timeUtils.js"; -import { StatusLabel } from "../../../../Components/Label/index.jsx"; +import { StatusLabel } from "@/Components/v1/Label/index.jsx"; import { setRowsPerPage } from "../../../../Features/UI/uiSlice.js"; import { useTranslation } from "react-i18next"; import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/v1/Maintenance/index.jsx b/client/src/Pages/v1/Maintenance/index.jsx index a993c153f..cba0e9f18 100644 --- a/client/src/Pages/v1/Maintenance/index.jsx +++ b/client/src/Pages/v1/Maintenance/index.jsx @@ -5,10 +5,10 @@ import "./index.css"; import MaintenanceTable from "./MaintenanceTable/index.jsx"; import { useSelector } from "react-redux"; import { networkService } from "../../../main.jsx"; -import Breadcrumbs from "../../../Components/Breadcrumbs/index.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import PageStateWrapper from "../../../Components/PageStateWrapper/index.jsx"; +import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; const Maintenance = () => { const theme = useTheme(); diff --git a/client/src/Pages/v1/Notifications/create/index.jsx b/client/src/Pages/v1/Notifications/create/index.jsx index 0d541736d..eb2b04aa6 100644 --- a/client/src/Pages/v1/Notifications/create/index.jsx +++ b/client/src/Pages/v1/Notifications/create/index.jsx @@ -1,13 +1,13 @@ // Components import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; import Button from "@mui/material/Button"; -import ConfigBox from "../../../../Components/ConfigBox/index.jsx"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; import Box from "@mui/material/Box"; -import Select from "../../../../Components/Inputs/Select/index.jsx"; -import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; -import Dialog from "../../../../Components/Dialog/index.jsx"; +import Select from "@/Components/v1/Inputs/Select/index.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; +import Dialog from "@/Components/v1/Dialog/index.jsx"; // Utils import { useState } from "react"; diff --git a/client/src/Pages/v1/Notifications/index.jsx b/client/src/Pages/v1/Notifications/index.jsx index 5649224b6..78ec6e9bb 100644 --- a/client/src/Pages/v1/Notifications/index.jsx +++ b/client/src/Pages/v1/Notifications/index.jsx @@ -1,11 +1,11 @@ // Components import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import Breadcrumbs from "../../../Components/Breadcrumbs/index.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; import Button from "@mui/material/Button"; -import DataTable from "../../../Components/Table/index.jsx"; +import DataTable from "@/Components/v1/Table/index.jsx"; import ActionMenu from "./components/ActionMenu.jsx"; -import PageStateWrapper from "../../../Components/PageStateWrapper/index.jsx"; +import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; // Utils import { useState } from "react"; diff --git a/client/src/Pages/v1/PageSpeed/Create/index.jsx b/client/src/Pages/v1/PageSpeed/Create/index.jsx index 7cc244848..cb856bd37 100644 --- a/client/src/Pages/v1/PageSpeed/Create/index.jsx +++ b/client/src/Pages/v1/PageSpeed/Create/index.jsx @@ -1,17 +1,17 @@ // Components import { Box, Stack, Tooltip, Typography, Button, ButtonGroup } from "@mui/material"; -import ConfigBox from "../../../../Components/ConfigBox/index.jsx"; -import Select from "../../../../Components/Inputs/Select/index.jsx"; -import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; +import Select from "@/Components/v1/Inputs/Select/index.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline"; -import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; -import PulseDot from "../../../../Components/Animated/PulseDot.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; +import PulseDot from "@/Components/v1/Animated/PulseDot.jsx"; import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; import SkeletonLayout from "./skeleton.jsx"; -import NotificationsConfig from "../../../../Components/NotificationConfig/index.jsx"; -import Dialog from "../../../../Components/Dialog/index.jsx"; -import { HttpAdornment } from "../../../../Components/Inputs/TextInput/Adornments/index.jsx"; -import Radio from "../../../../Components/Inputs/Radio/index.jsx"; +import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx"; +import Dialog from "@/Components/v1/Dialog/index.jsx"; +import { HttpAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; +import Radio from "@/Components/v1/Inputs/Radio/index.jsx"; // Utils import { useState, useEffect } from "react"; diff --git a/client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx index cb6f18bd2..c0da1600f 100644 --- a/client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx +++ b/client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx @@ -1,7 +1,7 @@ import { Box, Typography, Divider } from "@mui/material"; -import Checkbox from "../../../../../../Components/Inputs/Checkbox/index.jsx"; +import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; import MetricsIcon from "../../../../../../assets/icons/ruler-icon.svg?react"; -import LegendBox from "../../../../../../Components/Charts/LegendBox/index.jsx"; +import LegendBox from "@/Components/v1/Charts/LegendBox/index.jsx"; import { useTranslation } from "react-i18next"; import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/v1/PageSpeed/Details/Components/Charts/PieChartLegend.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/Charts/PieChartLegend.jsx index fc4b190d8..1297bd699 100644 --- a/client/src/Pages/v1/PageSpeed/Details/Components/Charts/PieChartLegend.jsx +++ b/client/src/Pages/v1/PageSpeed/Details/Components/Charts/PieChartLegend.jsx @@ -1,6 +1,6 @@ import { Stack, Box, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; -import LegendBox from "../../../../../../Components/Charts/LegendBox/index.jsx"; +import LegendBox from "@/Components/v1/Charts/LegendBox/index.jsx"; import SpeedometerIcon from "../../../../../../assets/icons/speedometer-icon.svg?react"; import PropTypes from "prop-types"; diff --git a/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx index bb45117fe..1281a396e 100644 --- a/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx +++ b/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx @@ -1,4 +1,4 @@ -import ChartBox from "../../../../../../Components/Charts/ChartBox/index.jsx"; +import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; import AreaChart from "../Charts/AreaChart.jsx"; import AreaChartLegend from "../Charts/AreaChartLegend.jsx"; import SkeletonLayout from "./skeleton.jsx"; diff --git a/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx index 0811ae707..ff265317c 100644 --- a/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx +++ b/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx @@ -1,5 +1,5 @@ -import StatusBoxes from "../../../../../../Components/StatusBoxes/index.jsx"; -import StatBox from "../../../../../../Components/StatBox/index.jsx"; +import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; +import StatBox from "@/Components/v1/StatBox/index.jsx"; import { Typography } from "@mui/material"; import { getHumanReadableDuration } from "../../../../../../Utils/timeUtils.js"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/index.jsx b/client/src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/index.jsx index ce4704144..27f9e885b 100644 --- a/client/src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/index.jsx +++ b/client/src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/index.jsx @@ -1,4 +1,4 @@ -import ChartBox from "../../../../../../Components/Charts/ChartBox/index.jsx"; +import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; import PerformanceIcon from "../../../../../../assets/icons/performance-report.svg?react"; import PieChart from "../Charts/PieChart.jsx"; import { Typography } from "@mui/material"; diff --git a/client/src/Pages/v1/PageSpeed/Details/index.jsx b/client/src/Pages/v1/PageSpeed/Details/index.jsx index cf9652bce..836f0ca37 100644 --- a/client/src/Pages/v1/PageSpeed/Details/index.jsx +++ b/client/src/Pages/v1/PageSpeed/Details/index.jsx @@ -1,12 +1,12 @@ // Components import { Stack, Typography } from "@mui/material"; -import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; -import MonitorTimeFrameHeader from "../../../../Components/MonitorTimeFrameHeader/index.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; +import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx"; import PageSpeedStatusBoxes from "./Components/PageSpeedStatusBoxes/index.jsx"; -import MonitorDetailsControlHeader from "../../../../Components/MonitorDetailsControlHeader/index.jsx"; +import MonitorDetailsControlHeader from "@/Components/v1/MonitorDetailsControlHeader/index.jsx"; import PageSpeedAreaChart from "./Components/PageSpeedAreaChart/index.jsx"; import PerformanceReport from "./Components/PerformanceReport/index.jsx"; -import GenericFallback from "../../../../Components/GenericFallback/index.jsx"; +import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; // Utils import { useTheme } from "@emotion/react"; import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; diff --git a/client/src/Pages/v1/PageSpeed/Monitors/Components/Card/index.jsx b/client/src/Pages/v1/PageSpeed/Monitors/Components/Card/index.jsx index 5857a7a7e..4e551526b 100644 --- a/client/src/Pages/v1/PageSpeed/Monitors/Components/Card/index.jsx +++ b/client/src/Pages/v1/PageSpeed/Monitors/Components/Card/index.jsx @@ -1,7 +1,7 @@ import PropTypes from "prop-types"; // import PageSpeedIcon from "../../../../assets/icons/page-speed.svg?react"; import PageSpeedIcon from "../../../../../../assets/icons/page-speed.svg?react"; -import { StatusLabel } from "../../../../../../Components/Label/index.jsx"; +import { StatusLabel } from "@/Components/v1/Label/index.jsx"; import { Box, Grid, Stack, Typography } from "@mui/material"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; @@ -14,7 +14,7 @@ import { import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import IconBox from "../../../../../../Components/IconBox/index.jsx"; +import IconBox from "@/Components/v1/IconBox/index.jsx"; /** * CustomToolTip displays a tooltip with formatted date and score information. * @param {Object} props diff --git a/client/src/Pages/v1/PageSpeed/Monitors/index.jsx b/client/src/Pages/v1/PageSpeed/Monitors/index.jsx index e568e8d82..7485a72c9 100644 --- a/client/src/Pages/v1/PageSpeed/Monitors/index.jsx +++ b/client/src/Pages/v1/PageSpeed/Monitors/index.jsx @@ -1,12 +1,12 @@ // Components import { useState } from "react"; -import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; import { Stack } from "@mui/material"; -import CreateMonitorHeader from "../../../../Components/MonitorCreateHeader/index.jsx"; -import MonitorCountHeader from "../../../../Components/MonitorCountHeader/index.jsx"; +import CreateMonitorHeader from "@/Components/v1/MonitorCreateHeader/index.jsx"; +import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx"; import MonitorGrid from "./Components/MonitorGrid/index.jsx"; -import PageStateWrapper from "../../../../Components/PageStateWrapper/index.jsx"; -import FallbackPageSpeedWarning from "../../../../Components/Fallback/FallbackPageSpeedWarning.jsx"; +import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; +import FallbackPageSpeedWarning from "@/Components/v1/Fallback/FallbackPageSpeedWarning.jsx"; // Utils import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/v1/ServerUnreachable.jsx b/client/src/Pages/v1/ServerUnreachable.jsx index 0db697543..8230f473b 100644 --- a/client/src/Pages/v1/ServerUnreachable.jsx +++ b/client/src/Pages/v1/ServerUnreachable.jsx @@ -3,12 +3,12 @@ import { Box, Typography, Button, Stack } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; import { networkService } from "../../Utils/NetworkService.js"; -import Alert from "../../Components/Alert/index.jsx"; +import Alert from "@/Components/v1/Alert/index.jsx"; import { createToast } from "../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; import Background from "../../assets/Images/background-grid.svg?react"; import Logo from "../../assets/icons/checkmate-icon.svg?react"; -import ThemeSwitch from "../../Components/ThemeSwitch/index.jsx"; +import ThemeSwitch from "@/Components/v1/ThemeSwitch/index.jsx"; import LanguageSelector from "../../Components/LanguageSelector.jsx"; const ServerUnreachable = () => { diff --git a/client/src/Pages/v1/Settings/SettingsAbout.jsx b/client/src/Pages/v1/Settings/SettingsAbout.jsx index 3d5f5224e..6a8125af3 100644 --- a/client/src/Pages/v1/Settings/SettingsAbout.jsx +++ b/client/src/Pages/v1/Settings/SettingsAbout.jsx @@ -1,10 +1,10 @@ import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; -import ConfigBox from "../../../Components/ConfigBox/index.jsx"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; // Utils import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import Link from "../../../Components/Link/index.jsx"; +import Link from "@/Components/v1/Link/index.jsx"; const SettingsAbout = () => { const theme = useTheme(); diff --git a/client/src/Pages/v1/Settings/SettingsDemoMonitors.jsx b/client/src/Pages/v1/Settings/SettingsDemoMonitors.jsx index 96f907970..47e699851 100644 --- a/client/src/Pages/v1/Settings/SettingsDemoMonitors.jsx +++ b/client/src/Pages/v1/Settings/SettingsDemoMonitors.jsx @@ -1,12 +1,12 @@ import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import Button from "@mui/material/Button"; -import ConfigBox from "../../../Components/ConfigBox/index.jsx"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; // Utils import { useTheme } from "@emotion/react"; import { PropTypes } from "prop-types"; import { useTranslation } from "react-i18next"; -import Dialog from "../../../Components/Dialog/index.jsx"; +import Dialog from "@/Components/v1/Dialog/index.jsx"; import { useState } from "react"; const SettingsDemoMonitors = ({ isAdmin, HEADER_SX, handleChange, isLoading }) => { diff --git a/client/src/Pages/v1/Settings/SettingsEmail.jsx b/client/src/Pages/v1/Settings/SettingsEmail.jsx index c2389b277..7b5ad175e 100644 --- a/client/src/Pages/v1/Settings/SettingsEmail.jsx +++ b/client/src/Pages/v1/Settings/SettingsEmail.jsx @@ -1,17 +1,17 @@ import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; -import ConfigBox from "../../../Components/ConfigBox/index.jsx"; -import TextInput from "../../../Components/Inputs/TextInput/index.jsx"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; import Button from "@mui/material/Button"; import Stack from "@mui/material/Stack"; import { Switch } from "@mui/material"; -import TextLink from "../../../Components/TextLink/index.jsx"; +import TextLink from "@/Components/v1/TextLink/index.jsx"; // Utils import { useTheme } from "@emotion/react"; import { PropTypes } from "prop-types"; import { useState } from "react"; import { useTranslation } from "react-i18next"; -import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments/index.jsx"; +import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; import { useSendTestEmail } from "../../../Hooks/v1/useSendTestEmail.js"; import { createToast } from "../../../Utils/toastUtils.jsx"; diff --git a/client/src/Pages/v1/Settings/SettingsGlobalThresholds.jsx b/client/src/Pages/v1/Settings/SettingsGlobalThresholds.jsx index e238c6475..ea58478e2 100644 --- a/client/src/Pages/v1/Settings/SettingsGlobalThresholds.jsx +++ b/client/src/Pages/v1/Settings/SettingsGlobalThresholds.jsx @@ -1,8 +1,8 @@ import Box from "@mui/material/Box"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import ConfigBox from "../../../Components/ConfigBox/index.jsx"; -import TextInput from "../../../Components/Inputs/TextInput/index.jsx"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; import { useTheme } from "@emotion/react"; import { PropTypes } from "prop-types"; diff --git a/client/src/Pages/v1/Settings/SettingsPagespeed.jsx b/client/src/Pages/v1/Settings/SettingsPagespeed.jsx index 83b2e23c7..b0d5d6441 100644 --- a/client/src/Pages/v1/Settings/SettingsPagespeed.jsx +++ b/client/src/Pages/v1/Settings/SettingsPagespeed.jsx @@ -2,9 +2,9 @@ import Box from "@mui/material/Box"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import Button from "@mui/material/Button"; -import ConfigBox from "../../../Components/ConfigBox/index.jsx"; -import TextInput from "../../../Components/Inputs/TextInput/index.jsx"; -import { PasswordEndAdornment } from "../../../Components/Inputs/TextInput/Adornments/index.jsx"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; +import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; // Utils import { useTheme } from "@emotion/react"; import { PropTypes } from "prop-types"; diff --git a/client/src/Pages/v1/Settings/SettingsStats.jsx b/client/src/Pages/v1/Settings/SettingsStats.jsx index 52f739ecf..b68006ab2 100644 --- a/client/src/Pages/v1/Settings/SettingsStats.jsx +++ b/client/src/Pages/v1/Settings/SettingsStats.jsx @@ -2,9 +2,9 @@ import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import Button from "@mui/material/Button"; import Stack from "@mui/material/Stack"; -import ConfigBox from "../../../Components/ConfigBox/index.jsx"; -import TextInput from "../../../Components/Inputs/TextInput/index.jsx"; -import Dialog from "../../../Components/Dialog/index.jsx"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; +import Dialog from "@/Components/v1/Dialog/index.jsx"; // Utils import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/v1/Settings/SettingsTimeZone.jsx b/client/src/Pages/v1/Settings/SettingsTimeZone.jsx index 59b2008f4..f684c871d 100644 --- a/client/src/Pages/v1/Settings/SettingsTimeZone.jsx +++ b/client/src/Pages/v1/Settings/SettingsTimeZone.jsx @@ -1,8 +1,8 @@ import Box from "@mui/material/Box"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import ConfigBox from "../../../Components/ConfigBox/index.jsx"; -import Search from "../../../Components/Inputs/Search/index.jsx"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; +import Search from "@/Components/v1/Inputs/Search/index.jsx"; import timezones from "../../../Utils/timezones.json"; // Utils diff --git a/client/src/Pages/v1/Settings/SettingsUI.jsx b/client/src/Pages/v1/Settings/SettingsUI.jsx index 91f4cea3c..7129e775b 100644 --- a/client/src/Pages/v1/Settings/SettingsUI.jsx +++ b/client/src/Pages/v1/Settings/SettingsUI.jsx @@ -1,8 +1,8 @@ import Box from "@mui/material/Box"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import ConfigBox from "../../../Components/ConfigBox/index.jsx"; -import Select from "../../../Components/Inputs/Select/index.jsx"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; +import Select from "@/Components/v1/Inputs/Select/index.jsx"; // Utils import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/v1/Settings/SettingsURL.jsx b/client/src/Pages/v1/Settings/SettingsURL.jsx index 105658d4f..c52dcf317 100644 --- a/client/src/Pages/v1/Settings/SettingsURL.jsx +++ b/client/src/Pages/v1/Settings/SettingsURL.jsx @@ -1,8 +1,8 @@ import Box from "@mui/material/Box"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import ConfigBox from "../../../Components/ConfigBox/index.jsx"; -import Select from "../../../Components/Inputs/Select/index.jsx"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; +import Select from "@/Components/v1/Inputs/Select/index.jsx"; // Utils import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index d1788eddf..2732e331a 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -1,6 +1,6 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import Breadcrumbs from "../../../Components/Breadcrumbs/index.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; import SettingsTimeZone from "./SettingsTimeZone.jsx"; import SettingsUI from "./SettingsUI.jsx"; import SettingsURL from "./SettingsURL.jsx"; diff --git a/client/src/Pages/v1/StatusPage/Create/Components/Progress/index.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Progress/index.jsx index c9f7d5f8f..232a12f1d 100644 --- a/client/src/Pages/v1/StatusPage/Create/Components/Progress/index.jsx +++ b/client/src/Pages/v1/StatusPage/Create/Components/Progress/index.jsx @@ -1,5 +1,5 @@ import { Button, Box } from "@mui/material"; -import ProgressUpload from "../../../../../../Components/ProgressBars/index.jsx"; +import ProgressUpload from "@/Components/v1/ProgressBars/index.jsx"; import ImageIcon from "@mui/icons-material/Image"; import { useTranslation } from "react-i18next"; import { formatBytes } from "../../../../../../Utils/fileUtils.js"; diff --git a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/ConfigStack.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/ConfigStack.jsx index fb5278c4e..f31c456ef 100644 --- a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/ConfigStack.jsx +++ b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/ConfigStack.jsx @@ -1,5 +1,5 @@ import { Stack, Typography } from "@mui/material"; -import ConfigBox from "../../../../../../Components/ConfigBox/index.jsx"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Content.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Content.jsx index 23e713c5a..d11825e69 100644 --- a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Content.jsx +++ b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Content.jsx @@ -2,8 +2,8 @@ import { Stack, Typography } from "@mui/material"; import { TabPanel } from "@mui/lab"; import MonitorList from "../MonitorList/index.jsx"; -import Search from "../../../../../../Components/Inputs/Search/index.jsx"; -import Checkbox from "../../../../../../Components/Inputs/Checkbox/index.jsx"; +import Search from "@/Components/v1/Inputs/Search/index.jsx"; +import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; // Utils import { useState } from "react"; import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Settings.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Settings.jsx index a4612e3da..a668a9db8 100644 --- a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Settings.jsx +++ b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Settings.jsx @@ -1,12 +1,12 @@ // Components import { Stack, Typography } from "@mui/material"; import { TabPanel } from "@mui/lab"; -import ConfigBox from "../../../../../../Components/ConfigBox/index.jsx"; -import Checkbox from "../../../../../../Components/Inputs/Checkbox/index.jsx"; -import TextInput from "../../../../../../Components/Inputs/TextInput/index.jsx"; -import Search from "../../../../../../Components/Inputs/Search/index.jsx"; -import ImageUpload from "../../../../../../Components/Inputs/ImageUpload/index.jsx"; -import ColorPicker from "../../../../../../Components/Inputs/ColorPicker/index.jsx"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; +import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; +import Search from "@/Components/v1/Inputs/Search/index.jsx"; +import ImageUpload from "@/Components/v1/Inputs/ImageUpload/index.jsx"; +import ColorPicker from "@/Components/v1/Inputs/ColorPicker/index.jsx"; import Progress from "../Progress/index.jsx"; // Utils diff --git a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/index.jsx b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/index.jsx index 971b8d85c..e8669c474 100644 --- a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/index.jsx +++ b/client/src/Pages/v1/StatusPage/Create/Components/Tabs/index.jsx @@ -6,7 +6,7 @@ import Content from "./Content.jsx"; // Utils import PropTypes from "prop-types"; -import CustomTabList from "../../../../../../Components/Tab/index.jsx"; +import CustomTabList from "@/Components/v1/Tab/index.jsx"; const Tabs = ({ isCreate, diff --git a/client/src/Pages/v1/StatusPage/Create/index.jsx b/client/src/Pages/v1/StatusPage/Create/index.jsx index 674a158ac..d9fafed70 100644 --- a/client/src/Pages/v1/StatusPage/Create/index.jsx +++ b/client/src/Pages/v1/StatusPage/Create/index.jsx @@ -1,10 +1,10 @@ // Components import { Stack, Button, Typography } from "@mui/material"; import Tabs from "./Components/Tabs/index.jsx"; -import GenericFallback from "../../../../Components/GenericFallback/index.jsx"; +import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; import SkeletonLayout from "./Components/Skeleton/index.jsx"; -import Dialog from "../../../../Components/Dialog/index.jsx"; -import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; +import Dialog from "@/Components/v1/Dialog/index.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; //Utils import { useTheme } from "@emotion/react"; import { useState, useEffect, useRef, useCallback } from "react"; diff --git a/client/src/Pages/v1/StatusPage/Status/Components/ControlsHeader/index.jsx b/client/src/Pages/v1/StatusPage/Status/Components/ControlsHeader/index.jsx index fa5ed71f1..c56aa40cc 100644 --- a/client/src/Pages/v1/StatusPage/Status/Components/ControlsHeader/index.jsx +++ b/client/src/Pages/v1/StatusPage/Status/Components/ControlsHeader/index.jsx @@ -1,8 +1,8 @@ // Components import { Box, Stack, Typography, Button } from "@mui/material"; -import Image from "../../../../../../Components/Image/index.jsx"; +import Image from "@/Components/v1/Image/index.jsx"; import SettingsIcon from "../../../../../../assets/icons/settings-bold.svg?react"; -import ThemeSwitch from "../../../../../../Components/ThemeSwitch/index.jsx"; +import ThemeSwitch from "@/Components/v1/ThemeSwitch/index.jsx"; import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward"; //Utils import { useTheme } from "@mui/material/styles"; diff --git a/client/src/Pages/v1/StatusPage/Status/Components/MonitorsList/index.jsx b/client/src/Pages/v1/StatusPage/Status/Components/MonitorsList/index.jsx index fd04dae66..59ad4d522 100644 --- a/client/src/Pages/v1/StatusPage/Status/Components/MonitorsList/index.jsx +++ b/client/src/Pages/v1/StatusPage/Status/Components/MonitorsList/index.jsx @@ -1,8 +1,8 @@ // Components import { Stack, Box } from "@mui/material"; -import Host from "../../../../../../Components/Host/index.jsx"; -import StatusPageBarChart from "../../../../../../Components/Charts/StatusPageBarChart/index.jsx"; -import { StatusLabel } from "../../../../../../Components/Label/index.jsx"; +import Host from "@/Components/v1/Host/index.jsx"; +import StatusPageBarChart from "@/Components/v1/Charts/StatusPageBarChart/index.jsx"; +import { StatusLabel } from "@/Components/v1/Label/index.jsx"; //Utils import { useTheme } from "@mui/material/styles"; diff --git a/client/src/Pages/v1/StatusPage/Status/index.jsx b/client/src/Pages/v1/StatusPage/Status/index.jsx index 48fc44fc9..e700bbbd9 100644 --- a/client/src/Pages/v1/StatusPage/Status/index.jsx +++ b/client/src/Pages/v1/StatusPage/Status/index.jsx @@ -1,13 +1,13 @@ // Components import { Typography, Stack } from "@mui/material"; -import GenericFallback from "../../../../Components/GenericFallback/index.jsx"; +import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; import AdminLink from "./Components/AdminLink/index.jsx"; import ControlsHeader from "./Components/ControlsHeader/index.jsx"; import SkeletonLayout from "./Components/Skeleton/index.jsx"; import StatusBar from "./Components/StatusBar/index.jsx"; import MonitorsList from "./Components/MonitorsList/index.jsx"; -import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; -import TextLink from "../../../../Components/TextLink/index.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; +import TextLink from "@/Components/v1/TextLink/index.jsx"; // Utils import { useStatusPageFetch } from "./Hooks/useStatusPageFetch.jsx"; diff --git a/client/src/Pages/v1/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx b/client/src/Pages/v1/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx index d8d68fb2e..1afe25409 100644 --- a/client/src/Pages/v1/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx +++ b/client/src/Pages/v1/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx @@ -1,7 +1,7 @@ -import DataTable from "../../../../../../Components/Table/index.jsx"; +import DataTable from "@/Components/v1/Table/index.jsx"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; -import { StatusLabel } from "../../../../../../Components/Label/index.jsx"; +import { StatusLabel } from "@/Components/v1/Label/index.jsx"; import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward"; import { Stack, Typography } from "@mui/material"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/v1/StatusPage/StatusPages/index.jsx b/client/src/Pages/v1/StatusPage/StatusPages/index.jsx index 8e0c89ce3..3c60fe950 100644 --- a/client/src/Pages/v1/StatusPage/StatusPages/index.jsx +++ b/client/src/Pages/v1/StatusPage/StatusPages/index.jsx @@ -1,9 +1,9 @@ // Components import { Stack } from "@mui/material"; -import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; -import MonitorCreateHeader from "../../../../Components/MonitorCreateHeader/index.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; +import MonitorCreateHeader from "@/Components/v1/MonitorCreateHeader/index.jsx"; import StatusPagesTable from "./Components/StatusPagesTable/index.jsx"; -import PageStateWrapper from "../../../../Components/PageStateWrapper/index.jsx"; +import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; // Utils import { useTheme } from "@emotion/react"; import { useStatusPagesFetch } from "./Hooks/useStatusPagesFetch.jsx"; diff --git a/client/src/Pages/v1/Uptime/BulkImport/index.jsx b/client/src/Pages/v1/Uptime/BulkImport/index.jsx index 412ba0f97..9f3efcd67 100644 --- a/client/src/Pages/v1/Uptime/BulkImport/index.jsx +++ b/client/src/Pages/v1/Uptime/BulkImport/index.jsx @@ -6,8 +6,8 @@ import { Box, Stack, Typography, Button, Link } from "@mui/material"; //Components import { createToast } from "../../../../Utils/toastUtils.jsx"; -import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; -import ConfigBox from "../../../../Components/ConfigBox/index.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; import UploadFile from "./Upload.jsx"; import { useSelector } from "react-redux"; import { useNavigate } from "react-router-dom"; diff --git a/client/src/Pages/v1/Uptime/Create/index.jsx b/client/src/Pages/v1/Uptime/Create/index.jsx index 6bb2a3b33..202f64d85 100644 --- a/client/src/Pages/v1/Uptime/Create/index.jsx +++ b/client/src/Pages/v1/Uptime/Create/index.jsx @@ -9,16 +9,16 @@ import { Tooltip, Typography, } from "@mui/material"; -import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; -import TextInput from "../../../../Components/Inputs/TextInput/index.jsx"; -import { HttpAdornment } from "../../../../Components/Inputs/TextInput/Adornments/index.jsx"; -import Radio from "../../../../Components/Inputs/Radio/index.jsx"; -import Select from "../../../../Components/Inputs/Select/index.jsx"; -import ConfigBox from "../../../../Components/ConfigBox/index.jsx"; -import NotificationsConfig from "../../../../Components/NotificationConfig/index.jsx"; -import Checkbox from "../../../../Components/Inputs/Checkbox/index.jsx"; -import Dialog from "../../../../Components/Dialog/index.jsx"; -import PulseDot from "../../../../Components/Animated/PulseDot.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; +import { HttpAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; +import Radio from "@/Components/v1/Inputs/Radio/index.jsx"; +import Select from "@/Components/v1/Inputs/Select/index.jsx"; +import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; +import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx"; +import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; +import Dialog from "@/Components/v1/Dialog/index.jsx"; +import PulseDot from "@/Components/v1/Animated/PulseDot.jsx"; import SkeletonLayout from "./skeleton.jsx"; // Utils diff --git a/client/src/Pages/v1/Uptime/Details/Components/ChartBoxes/index.jsx b/client/src/Pages/v1/Uptime/Details/Components/ChartBoxes/index.jsx index c6b76c289..45f35a42e 100644 --- a/client/src/Pages/v1/Uptime/Details/Components/ChartBoxes/index.jsx +++ b/client/src/Pages/v1/Uptime/Details/Components/ChartBoxes/index.jsx @@ -1,6 +1,6 @@ // Components import { Stack, Typography, Box } from "@mui/material"; -import ChartBox from "../../../../../../Components/Charts/ChartBox/index.jsx"; +import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; import UptimeIcon from "../../../../../../assets/icons/uptime-icon.svg?react"; import IncidentsIcon from "../../../../../../assets/icons/incidents.svg?react"; import AverageResponseIcon from "../../../../../../assets/icons/average-response-icon.svg?react"; diff --git a/client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChart.jsx b/client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChart.jsx index f306dfd76..daa7b4f53 100644 --- a/client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChart.jsx +++ b/client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChart.jsx @@ -1,5 +1,5 @@ -import ChartBox from "../../../../../../Components/Charts/ChartBox/index.jsx"; -import MonitorDetailsAreaChart from "../../../../../../Components/Charts/MonitorDetailsAreaChart/index.jsx"; +import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; +import MonitorDetailsAreaChart from "@/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx"; import ResponseTimeIcon from "../../../../../../assets/icons/response-time-icon.svg?react"; import SkeletonLayout from "./ResponseTimeChartSkeleton.jsx"; import PropTypes from "prop-types"; diff --git a/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx b/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx index cc5745a26..948489ac1 100644 --- a/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx +++ b/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx @@ -1,9 +1,9 @@ -import ChartBox from "../../../../../../Components/Charts/ChartBox/index.jsx"; +import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; import PropTypes from "prop-types"; import HistoryIcon from "../../../../../../assets/icons/history-icon.svg?react"; -import Table from "../../../../../../Components/Table/index.jsx"; -import TablePagination from "../../../../../../Components/Table/TablePagination/index.jsx"; -import { StatusLabel } from "../../../../../../Components/Label/index.jsx"; +import Table from "@/Components/v1/Table/index.jsx"; +import TablePagination from "@/Components/v1/Table/TablePagination/index.jsx"; +import { StatusLabel } from "@/Components/v1/Label/index.jsx"; import { useTranslation } from "react-i18next"; import { formatDateWithTz } from "../../../../../../Utils/timeUtils.js"; import SkeletonLayout from "./skeleton.jsx"; diff --git a/client/src/Pages/v1/Uptime/Details/Components/UptimeStatusBoxes/index.jsx b/client/src/Pages/v1/Uptime/Details/Components/UptimeStatusBoxes/index.jsx index f0988466c..a7b971839 100644 --- a/client/src/Pages/v1/Uptime/Details/Components/UptimeStatusBoxes/index.jsx +++ b/client/src/Pages/v1/Uptime/Details/Components/UptimeStatusBoxes/index.jsx @@ -1,5 +1,5 @@ -import StatusBoxes from "../../../../../../Components/StatusBoxes/index.jsx"; -import StatBox from "../../../../../../Components/StatBox/index.jsx"; +import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; +import StatBox from "@/Components/v1/StatBox/index.jsx"; import PropTypes from "prop-types"; import { getHumanReadableDuration } from "../../../../../../Utils/timeUtils.js"; diff --git a/client/src/Pages/v1/Uptime/Details/index.jsx b/client/src/Pages/v1/Uptime/Details/index.jsx index 4e773dc2b..f5e283495 100644 --- a/client/src/Pages/v1/Uptime/Details/index.jsx +++ b/client/src/Pages/v1/Uptime/Details/index.jsx @@ -1,12 +1,12 @@ // Components -import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; -import MonitorDetailsControlHeader from "../../../../Components/MonitorDetailsControlHeader/index.jsx"; -import MonitorTimeFrameHeader from "../../../../Components/MonitorTimeFrameHeader/index.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; +import MonitorDetailsControlHeader from "@/Components/v1/MonitorDetailsControlHeader/index.jsx"; +import MonitorTimeFrameHeader from "@/Components/v1/MonitorTimeFrameHeader/index.jsx"; import ChartBoxes from "./Components/ChartBoxes/index.jsx"; import ResponseTimeChart from "./Components/Charts/ResponseTimeChart.jsx"; import ResponseTable from "./Components/ResponseTable/index.jsx"; import UptimeStatusBoxes from "./Components/UptimeStatusBoxes/index.jsx"; -import GenericFallback from "../../../../Components/GenericFallback/index.jsx"; +import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; diff --git a/client/src/Pages/v1/Uptime/Monitors/Components/Filter/index.jsx b/client/src/Pages/v1/Uptime/Monitors/Components/Filter/index.jsx index 85c4a8540..b9a6824c1 100644 --- a/client/src/Pages/v1/Uptime/Monitors/Components/Filter/index.jsx +++ b/client/src/Pages/v1/Uptime/Monitors/Components/Filter/index.jsx @@ -1,6 +1,6 @@ import { useTheme } from "@emotion/react"; import PropTypes from "prop-types"; -import FilterHeader from "../../../../../../Components/FilterHeader/index.jsx"; +import FilterHeader from "@/Components/v1/FilterHeader/index.jsx"; import { useMemo } from "react"; import { Box, Button } from "@mui/material"; import ClearIcon from "@mui/icons-material/Clear"; diff --git a/client/src/Pages/v1/Uptime/Monitors/Components/SearchComponent/index.jsx b/client/src/Pages/v1/Uptime/Monitors/Components/SearchComponent/index.jsx index 3339102a2..64c546a8a 100644 --- a/client/src/Pages/v1/Uptime/Monitors/Components/SearchComponent/index.jsx +++ b/client/src/Pages/v1/Uptime/Monitors/Components/SearchComponent/index.jsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import Search from "../../../../../../Components/Inputs/Search/index.jsx"; +import Search from "@/Components/v1/Inputs/Search/index.jsx"; import { Box } from "@mui/material"; import useDebounce from "../../Hooks/useDebounce.jsx"; import { useEffect, useRef } from "react"; diff --git a/client/src/Pages/v1/Uptime/Monitors/Components/UptimeDataTable/index.jsx b/client/src/Pages/v1/Uptime/Monitors/Components/UptimeDataTable/index.jsx index 575647eab..9cf2d247d 100644 --- a/client/src/Pages/v1/Uptime/Monitors/Components/UptimeDataTable/index.jsx +++ b/client/src/Pages/v1/Uptime/Monitors/Components/UptimeDataTable/index.jsx @@ -1,15 +1,15 @@ // Components import { Box, Stack } from "@mui/material"; -import DataTable from "../../../../../../Components/Table/index.jsx"; +import DataTable from "@/Components/v1/Table/index.jsx"; import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; -import Host from "../../../../../../Components/Host/index.jsx"; -import { StatusLabel } from "../../../../../../Components/Label/index.jsx"; -import BarChart from "../../../../../../Components/Charts/BarChart/index.jsx"; -import ActionsMenu from "../../../../../../Components/ActionsMenu/index.jsx"; +import Host from "@/Components/v1/Host/index.jsx"; +import { StatusLabel } from "@/Components/v1/Label/index.jsx"; +import BarChart from "@/Components/v1/Charts/BarChart/index.jsx"; +import ActionsMenu from "@/Components/v1/ActionsMenu/index.jsx"; import LoadingSpinner from "../LoadingSpinner/index.jsx"; -import TableSkeleton from "../../../../../../Components/Table/skeleton.jsx"; +import TableSkeleton from "@/Components/v1/Table/skeleton.jsx"; // Utils import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/v1/Uptime/Monitors/index.jsx b/client/src/Pages/v1/Uptime/Monitors/index.jsx index dae0bc2d5..cacfc23b4 100644 --- a/client/src/Pages/v1/Uptime/Monitors/index.jsx +++ b/client/src/Pages/v1/Uptime/Monitors/index.jsx @@ -4,17 +4,17 @@ // 2a.List of monitors must have the total number of monitors that match. // Components -import Breadcrumbs from "../../../../Components/Breadcrumbs/index.jsx"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; import Greeting from "../../../../Utils/greeting.jsx"; import StatusBoxes from "./Components/StatusBoxes/index.jsx"; import UptimeDataTable from "./Components/UptimeDataTable/index.jsx"; -import Pagination from "../../../../Components/Table/TablePagination/index.jsx"; -import CreateMonitorHeader from "../../../../Components/MonitorCreateHeader/index.jsx"; +import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; +import CreateMonitorHeader from "@/Components/v1/MonitorCreateHeader/index.jsx"; import SearchComponent from "./Components/SearchComponent/index.jsx"; import Filter from "./Components/Filter/index.jsx"; -import PageStateWrapper from "../../../../Components/PageStateWrapper/index.jsx"; +import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; -import MonitorCountHeader from "../../../../Components/MonitorCountHeader/index.jsx"; +import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx"; // MUI Components import { Stack, Box, Button } from "@mui/material"; diff --git a/client/src/Pages/v2/Auth/Login.tsx b/client/src/Pages/v2/Auth/Login.tsx index 58eec01dc..fd4a08e93 100644 --- a/client/src/Pages/v2/Auth/Login.tsx +++ b/client/src/Pages/v2/Auth/Login.tsx @@ -4,7 +4,7 @@ import { useTranslation } from "react-i18next"; import { useForm, Controller } from "react-hook-form"; import { useTheme } from "@mui/material/styles"; -import { TextInput } from "@/Components/Inputs/TextInput/indexV2.tsx"; +import { TextInput } from "@/Components/v2/Inputs/TextInput"; import Button from "@mui/material/Button"; import Stack from "@mui/material/Stack"; import { usePost } from "@/Hooks/v2/UseApi"; diff --git a/client/src/Pages/v2/Auth/Register.tsx b/client/src/Pages/v2/Auth/Register.tsx index a9043a2c9..2a02c401f 100644 --- a/client/src/Pages/v2/Auth/Register.tsx +++ b/client/src/Pages/v2/Auth/Register.tsx @@ -5,7 +5,7 @@ import { useForm, Controller } from "react-hook-form"; import { useTheme } from "@mui/material/styles"; import Typography from "@mui/material/Typography"; -import { TextInput } from "@/Components/Inputs/TextInput/indexV2.tsx"; +import { TextInput } from "@/Components/v2/Inputs/TextInput"; import Button from "@mui/material/Button"; import Stack from "@mui/material/Stack"; import { usePost } from "@/Hooks/v2/UseApi"; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 2e4fd9c50..90dcaa3f5 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -1,7 +1,7 @@ import { useSelector } from "react-redux"; import { lightTheme, darkTheme } from "@/Utils/Theme/v2/theme"; import { Navigate, Route, Routes as LibRoutes } from "react-router"; -import HomeLayout from "../Components/Layouts/HomeLayout"; +import HomeLayout from "@/Components/v1/Layouts/HomeLayout"; import NotFound from "../Pages/v1/NotFound"; // Auth import AuthLogin from "../Pages/v1/Auth/Login"; @@ -47,10 +47,10 @@ import Settings from "../Pages/v1/Settings"; import Maintenance from "../Pages/v1/Maintenance"; -import ProtectedRoute from "../Components/ProtectedRoute"; -import RoleProtectedRoute from "../Components/RoleProtectedRoute"; +import ProtectedRoute from "../Components/v1/ProtectedRoute"; +import RoleProtectedRoute from "../Components/v1/RoleProtectedRoute"; import CreateNewMaintenanceWindow from "../Pages/v1/Maintenance/CreateMaintenance"; -import withAdminCheck from "../Components/HOC/withAdminCheck"; +import withAdminCheck from "@/Components/v1/HOC/withAdminCheck"; import BulkImport from "../Pages/v1/Uptime/BulkImport"; import Logs from "../Pages/v1/Logs"; diff --git a/client/src/Utils/toastUtils.jsx b/client/src/Utils/toastUtils.jsx index 21cf13e2b..41b2d5482 100644 --- a/client/src/Utils/toastUtils.jsx +++ b/client/src/Utils/toastUtils.jsx @@ -1,6 +1,6 @@ import PropTypes from "prop-types"; import { toast, Slide } from "react-toastify"; -import Toast from "../Components/Toast"; +import Toast from "../Components/v1/Toast"; /** * @param {object} props * @param {'info' | 'error' | 'warning'} - The variant of the alert (e.g., "info", "error"). diff --git a/client/src/main.jsx b/client/src/main.jsx index 01ecefcc6..dd42587af 100644 --- a/client/src/main.jsx +++ b/client/src/main.jsx @@ -8,7 +8,7 @@ import { PersistGate } from "redux-persist/integration/react"; import NetworkServiceProvider from "./Utils/NetworkServiceProvider.jsx"; import { networkService } from "./Utils/NetworkService"; export { networkService }; -import I18nLoader from "./Components/I18nLoader"; +import I18nLoader from "./Components/v1/I18nLoader"; ReactDOM.createRoot(document.getElementById("root")).render( From b57ed47592153337df4fc81ea64d6a43cc935606 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sat, 27 Sep 2025 14:57:55 -0700 Subject: [PATCH 038/400] remove mode prop --- client/src/Routes/v2router.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/Routes/v2router.tsx b/client/src/Routes/v2router.tsx index d450854f9..941550026 100644 --- a/client/src/Routes/v2router.tsx +++ b/client/src/Routes/v2router.tsx @@ -22,7 +22,7 @@ const V2Routes = ({ mode = "light" }) => { /> } + element={} > Date: Sat, 27 Sep 2025 15:47:56 -0700 Subject: [PATCH 039/400] Add collapse button and logo --- client/src/Components/v2/Arrows/ArrowLeft.tsx | 36 +++++++++++ .../src/Components/v2/Arrows/ArrowRight.tsx | 28 +++++++++ .../src/Components/v2/Layouts/RootLayout.tsx | 35 +---------- .../v2/Layouts/Sidebar/CollapseButton.tsx | 50 ++++++++++++++++ .../Components/v2/Layouts/Sidebar/Logo.tsx | 59 +++++++++++++++++++ .../Components/v2/Layouts/Sidebar/index.tsx | 40 +++++++++++++ client/src/Features/UI/uiSlice.js | 5 ++ client/src/Utils/Theme/v2/palette.ts | 10 ++++ client/src/Utils/Theme/v2/theme.ts | 3 + client/src/types/env.d.ts | 8 +++ client/src/types/mui.d.ts | 3 + 11 files changed, 243 insertions(+), 34 deletions(-) create mode 100644 client/src/Components/v2/Arrows/ArrowLeft.tsx create mode 100644 client/src/Components/v2/Arrows/ArrowRight.tsx create mode 100644 client/src/Components/v2/Layouts/Sidebar/CollapseButton.tsx create mode 100644 client/src/Components/v2/Layouts/Sidebar/Logo.tsx create mode 100644 client/src/Components/v2/Layouts/Sidebar/index.tsx diff --git a/client/src/Components/v2/Arrows/ArrowLeft.tsx b/client/src/Components/v2/Arrows/ArrowLeft.tsx new file mode 100644 index 000000000..01d1342c8 --- /dev/null +++ b/client/src/Components/v2/Arrows/ArrowLeft.tsx @@ -0,0 +1,36 @@ +import LeftArrow from "@/assets/icons/left-arrow.svg?react"; +import LeftArrowDouble from "@/assets/icons/left-arrow-double.svg?react"; +import LeftArrowLong from "@/assets/icons/left-arrow-long.svg?react"; + +export const ArrowLeft = ({ + type, + color = "#667085", + ...props +}: { + type?: string; + color?: string | undefined; + [key: string]: any; +}) => { + if (type === "double") { + return ( + + ); + } else if (type === "long") { + return ( + + ); + } else { + return ( + + ); + } +}; diff --git a/client/src/Components/v2/Arrows/ArrowRight.tsx b/client/src/Components/v2/Arrows/ArrowRight.tsx new file mode 100644 index 000000000..c73165f16 --- /dev/null +++ b/client/src/Components/v2/Arrows/ArrowRight.tsx @@ -0,0 +1,28 @@ +import RightArrow from "@/assets/icons/right-arrow.svg?react"; +import RightArrowDouble from "@/assets/icons/right-arrow-double.svg?react"; + +export const ArrowRight = ({ + type, + color = "#667085", + ...props +}: { + type?: string; + color?: string | undefined; + [key: string]: any; +}) => { + if (type === "double") { + return ( + + ); + } else { + return ( + + ); + } +}; diff --git a/client/src/Components/v2/Layouts/RootLayout.tsx b/client/src/Components/v2/Layouts/RootLayout.tsx index be2f42e30..dbc6f2e5e 100644 --- a/client/src/Components/v2/Layouts/RootLayout.tsx +++ b/client/src/Components/v2/Layouts/RootLayout.tsx @@ -1,39 +1,6 @@ -import { useState, useEffect } from "react"; -import useMediaQuery from "@mui/material/useMediaQuery"; -import { useTheme } from "@mui/material/styles"; - import { Outlet } from "react-router"; import Stack from "@mui/material/Stack"; -import Box from "@mui/material/Box"; - -const COLLAPSED_WIDTH = 50; -const EXPANDED_WIDTH = 250; - -const SideBar = () => { - const theme = useTheme(); - const isSmall = useMediaQuery(theme.breakpoints.down("md")); - - const [collapsed, setCollapsed] = useState(false); - useEffect(() => { - setCollapsed(isSmall); - }, [isSmall]); - return ( - - setCollapsed(!collapsed)} - > - Sidebar Content - - - ); -}; +import { SideBar } from "@/Components/v2/Layouts/Sidebar"; const RootLayout = () => { return ( diff --git a/client/src/Components/v2/Layouts/Sidebar/CollapseButton.tsx b/client/src/Components/v2/Layouts/Sidebar/CollapseButton.tsx new file mode 100644 index 000000000..564c41591 --- /dev/null +++ b/client/src/Components/v2/Layouts/Sidebar/CollapseButton.tsx @@ -0,0 +1,50 @@ +import IconButton from "@mui/material/IconButton"; +import { ArrowRight } from "@/Components/v2/Arrows/ArrowRight"; +import { ArrowLeft } from "@/Components/v2/Arrows/ArrowLeft"; +import { useTheme } from "@mui/material/styles"; +import { useDispatch } from "react-redux"; +import { toggleSidebar } from "../../../../Features/UI/uiSlice.js"; + +export const CollapseButton = ({ collapsed }: { collapsed: boolean }) => { + const theme = useTheme(); + const dispatch = useDispatch(); + const arrowIcon = collapsed ? ( + + ) : ( + + ); + + return ( + { + dispatch(toggleSidebar()); + }} + > + {arrowIcon} + + ); +}; diff --git a/client/src/Components/v2/Layouts/Sidebar/Logo.tsx b/client/src/Components/v2/Layouts/Sidebar/Logo.tsx new file mode 100644 index 000000000..6d190b99d --- /dev/null +++ b/client/src/Components/v2/Layouts/Sidebar/Logo.tsx @@ -0,0 +1,59 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import { useNavigate } from "react-router"; +import { useTranslation } from "react-i18next"; + +export const Logo = ({ collapsed }: { collapsed: boolean }) => { + const { t } = useTranslation(); + const theme = useTheme(); + const navigate = useNavigate(); + + return ( + navigate("/")} + sx={{ cursor: "pointer" }} + > + + C + + + {" "} + + {t("common.appName")} + + + + ); +}; diff --git a/client/src/Components/v2/Layouts/Sidebar/index.tsx b/client/src/Components/v2/Layouts/Sidebar/index.tsx new file mode 100644 index 000000000..43975adfc --- /dev/null +++ b/client/src/Components/v2/Layouts/Sidebar/index.tsx @@ -0,0 +1,40 @@ +import { useEffect } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { setCollapsed } from "@/Features/UI/uiSlice"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useTheme } from "@mui/material/styles"; + +import { CollapseButton } from "@/Components/v2/Layouts/Sidebar/CollapseButton"; +import Stack from "@mui/material/Stack"; +import { Logo } from "@/Components/v2/Layouts/Sidebar/Logo"; + +export const COLLAPSED_WIDTH = 64; +export const EXPANDED_WIDTH = 250; + +export const SideBar = () => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const dispatch = useDispatch(); + const collapsed = useSelector((state: any) => state.ui.sidebar.collapsed); + + useEffect(() => { + dispatch(setCollapsed({ collapsed: isSmall })); + }, [isSmall]); + + return ( + + + + + ); +}; diff --git a/client/src/Features/UI/uiSlice.js b/client/src/Features/UI/uiSlice.js index 45ba267b0..63f7ae90e 100644 --- a/client/src/Features/UI/uiSlice.js +++ b/client/src/Features/UI/uiSlice.js @@ -47,6 +47,10 @@ const uiSlice = createSlice({ toggleSidebar: (state) => { state.sidebar.collapsed = !state.sidebar.collapsed; }, + setCollapsed: (state, action) => { + const { collapsed } = action.payload; + state.sidebar.collapsed = collapsed; + }, setMode: (state, action) => { state.mode = action.payload; }, @@ -73,6 +77,7 @@ export default uiSlice.reducer; export const { setRowsPerPage, toggleSidebar, + setCollapsed, setMode, setShowURL, setGreeting, diff --git a/client/src/Utils/Theme/v2/palette.ts b/client/src/Utils/Theme/v2/palette.ts index 2875f750d..2e41a159c 100644 --- a/client/src/Utils/Theme/v2/palette.ts +++ b/client/src/Utils/Theme/v2/palette.ts @@ -54,12 +54,17 @@ export const lightPalette = { main: colors.offWhite, contrastText: colors.blueGray800, contrastTextSecondary: colors.blueGray600, + lowContrast: colors.gray250, }, secondary: { main: colors.gray200, light: colors.lightBlueWave, contrastText: colors.blueGray600, }, + tertiary: { + main: colors.gray100, + contrastText: colors.blueGray800, + }, }; export const darkPalette = { @@ -73,10 +78,15 @@ export const darkPalette = { main: colors.offBlack, contrastText: colors.blueGray50, contrastTextSecondary: colors.gray200, + lowContrast: colors.blueGray600, }, secondary: { main: "#313131", light: colors.lightBlueWave, contrastText: colors.gray200, }, + tertiary: { + main: colors.blueGray800, + contrastText: colors.gray100, + }, }; diff --git a/client/src/Utils/Theme/v2/theme.ts b/client/src/Utils/Theme/v2/theme.ts index 55d2b9bb9..77bab3e70 100644 --- a/client/src/Utils/Theme/v2/theme.ts +++ b/client/src/Utils/Theme/v2/theme.ts @@ -67,6 +67,9 @@ export const theme = (mode: string, palette: any) => }, }, }, + shape: { + borderRadius: 2, + }, }); export const lightTheme = createTheme(theme("light", lightPalette)); diff --git a/client/src/types/env.d.ts b/client/src/types/env.d.ts index e2bce23dd..74a2a29e7 100644 --- a/client/src/types/env.d.ts +++ b/client/src/types/env.d.ts @@ -7,3 +7,11 @@ interface ImportMetaEnv { interface ImportMeta { readonly env: ImportMetaEnv; } + +declare module "*.svg?react" { + import * as React from "react"; + const ReactComponent: React.FunctionComponent< + React.SVGProps & { title?: string } + >; + export default ReactComponent; +} diff --git a/client/src/types/mui.d.ts b/client/src/types/mui.d.ts index 851ffea70..5f5c1c38b 100644 --- a/client/src/types/mui.d.ts +++ b/client/src/types/mui.d.ts @@ -3,13 +3,16 @@ import "@mui/material/Button"; declare module "@mui/material/styles" { interface Palette { accent: Palette["primary"]; + tertiary: Palette["primary"]; } interface PaletteOptions { accent?: PaletteOptions["primary"]; + tertiary?: PaletteOptions["primary"]; } interface PaletteColor { contrastTextSecondary?: string; + lowContrast?: string; } } From 7f2318da01a21f47216f666ebdd8192b48fe6b2a Mon Sep 17 00:00:00 2001 From: ariretiarno Date: Tue, 30 Sep 2025 11:28:22 +0700 Subject: [PATCH 040/400] helm chart --- .../templates/client-deployment.yaml | 8 ++-- .../checkmate/templates/client-ingress.yaml | 9 ++++- .../checkmate/templates/client-service.yaml | 4 +- .../helm/checkmate/templates/mongodb-pvc.yaml | 10 ----- .../checkmate/templates/mongodb-service.yaml | 4 +- ...loyment.yaml => mongodb-statefulsets.yaml} | 21 ++++++---- .../checkmate/templates/redis-deployment.yaml | 26 ------------- .../helm/checkmate/templates/redis-pvc.yaml | 10 ----- .../checkmate/templates/redis-service.yaml | 6 ++- .../templates/redis-statefulsets.yaml | 33 ++++++++++++++++ .../templates/server-deployment.yaml | 6 +-- .../checkmate/templates/server-ingress.yaml | 21 ++++++---- .../checkmate/templates/server-service.yaml | 4 +- charts/helm/checkmate/values.yaml | 38 +++++++++++-------- 14 files changed, 107 insertions(+), 93 deletions(-) delete mode 100644 charts/helm/checkmate/templates/mongodb-pvc.yaml rename charts/helm/checkmate/templates/{mongodb-deployment.yaml => mongodb-statefulsets.yaml} (54%) delete mode 100644 charts/helm/checkmate/templates/redis-deployment.yaml delete mode 100644 charts/helm/checkmate/templates/redis-pvc.yaml create mode 100644 charts/helm/checkmate/templates/redis-statefulsets.yaml diff --git a/charts/helm/checkmate/templates/client-deployment.yaml b/charts/helm/checkmate/templates/client-deployment.yaml index 6c1f70fd7..b6e13fc7d 100644 --- a/charts/helm/checkmate/templates/client-deployment.yaml +++ b/charts/helm/checkmate/templates/client-deployment.yaml @@ -1,16 +1,16 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: client + name: checkmate-client spec: replicas: 1 selector: matchLabels: - app: client + app: checkmate-client template: metadata: labels: - app: client + app: checkmate-client spec: containers: - name: client @@ -20,3 +20,5 @@ spec: env: - name: UPTIME_APP_API_BASE_URL value: "{{ .Values.server.protocol }}://{{ .Values.server.ingress.host }}/api/v1" + - name: UPTIME_APP_CLIENT_HOST + value: "http://{{ .Values.client.ingress.host }}" diff --git a/charts/helm/checkmate/templates/client-ingress.yaml b/charts/helm/checkmate/templates/client-ingress.yaml index da059a863..014aa53b3 100644 --- a/charts/helm/checkmate/templates/client-ingress.yaml +++ b/charts/helm/checkmate/templates/client-ingress.yaml @@ -2,10 +2,15 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: client-ingress + name: checkmate-client-ingress + {{- if .Values.client.ingress.annotations }} annotations: - nginx.ingress.kubernetes.io/rewrite-target: / + {{- range $key, $value := .Values.client.ingress.annotations }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} spec: + ingressClassName: {{ .Values.client.ingress.className }} rules: - host: {{ .Values.client.ingress.host }} http: diff --git a/charts/helm/checkmate/templates/client-service.yaml b/charts/helm/checkmate/templates/client-service.yaml index afb88188c..b56eaeedf 100644 --- a/charts/helm/checkmate/templates/client-service.yaml +++ b/charts/helm/checkmate/templates/client-service.yaml @@ -1,9 +1,9 @@ apiVersion: v1 kind: Service metadata: - name: client + name: checkmate-client spec: selector: - app: client + app: checkmate-client ports: - port: {{ .Values.client.port }} diff --git a/charts/helm/checkmate/templates/mongodb-pvc.yaml b/charts/helm/checkmate/templates/mongodb-pvc.yaml deleted file mode 100644 index 435b662e6..000000000 --- a/charts/helm/checkmate/templates/mongodb-pvc.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: mongodb-pvc -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: {{ .Values.persistence.mongodbSize }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/mongodb-service.yaml b/charts/helm/checkmate/templates/mongodb-service.yaml index 2911dc3eb..b96fe1a82 100644 --- a/charts/helm/checkmate/templates/mongodb-service.yaml +++ b/charts/helm/checkmate/templates/mongodb-service.yaml @@ -1,9 +1,9 @@ apiVersion: v1 kind: Service metadata: - name: mongodb + name: checkmate-mongodb spec: selector: - app: mongodb + app: checkmate-mongodb ports: - port: {{ .Values.mongodb.port }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/mongodb-deployment.yaml b/charts/helm/checkmate/templates/mongodb-statefulsets.yaml similarity index 54% rename from charts/helm/checkmate/templates/mongodb-deployment.yaml rename to charts/helm/checkmate/templates/mongodb-statefulsets.yaml index 4c906fe09..4b490d7d7 100644 --- a/charts/helm/checkmate/templates/mongodb-deployment.yaml +++ b/charts/helm/checkmate/templates/mongodb-statefulsets.yaml @@ -1,16 +1,16 @@ apiVersion: apps/v1 -kind: Deployment +kind: StatefulSet metadata: - name: mongodb + name: checkmate-mongodb spec: replicas: 1 selector: matchLabels: - app: mongodb + app: checkmate-mongodb template: metadata: labels: - app: mongodb + app: checkmate-mongodb spec: containers: - name: mongodb @@ -21,7 +21,12 @@ spec: volumeMounts: - name: mongodb-data mountPath: /data/db - volumes: - - name: mongodb-data - persistentVolumeClaim: - claimName: mongodb-pvc \ No newline at end of file + volumeClaimTemplates: + - metadata: + name: checkmate-mongo-persistent-storage + spec: + storageClassName: "gp3" + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: {{ .Values.persistence.mongodbSize }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/redis-deployment.yaml b/charts/helm/checkmate/templates/redis-deployment.yaml deleted file mode 100644 index cc9ffdaf9..000000000 --- a/charts/helm/checkmate/templates/redis-deployment.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: redis -spec: - replicas: 1 - selector: - matchLabels: - app: redis - template: - metadata: - labels: - app: redis - spec: - containers: - - name: redis - image: {{ .Values.redis.image }} - ports: - - containerPort: {{ .Values.redis.port }} - volumeMounts: - - name: redis-data - mountPath: /data - volumes: - - name: redis-data - persistentVolumeClaim: - claimName: redis-pvc diff --git a/charts/helm/checkmate/templates/redis-pvc.yaml b/charts/helm/checkmate/templates/redis-pvc.yaml deleted file mode 100644 index 2f0d81960..000000000 --- a/charts/helm/checkmate/templates/redis-pvc.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: v1 -kind: PersistentVolumeClaim -metadata: - name: redis-pvc -spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: {{ .Values.persistence.redisSize }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/redis-service.yaml b/charts/helm/checkmate/templates/redis-service.yaml index f29fa46de..2720350da 100644 --- a/charts/helm/checkmate/templates/redis-service.yaml +++ b/charts/helm/checkmate/templates/redis-service.yaml @@ -1,9 +1,11 @@ +{{- if .Values.redis.enabled }} apiVersion: v1 kind: Service metadata: - name: redis + name: checkmate-redis spec: selector: - app: redis + app: checkmate-redis ports: - port: {{ .Values.redis.port }} +{{- end }} diff --git a/charts/helm/checkmate/templates/redis-statefulsets.yaml b/charts/helm/checkmate/templates/redis-statefulsets.yaml new file mode 100644 index 000000000..252361730 --- /dev/null +++ b/charts/helm/checkmate/templates/redis-statefulsets.yaml @@ -0,0 +1,33 @@ +{{- if .Values.redis.enabled }} +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: checkmate-redis +spec: + replicas: 1 + selector: + matchLabels: + app: checkmate-redis + template: + metadata: + labels: + app: checkmate-redis + spec: + containers: + - name: redis + image: {{ .Values.redis.image }} + ports: + - containerPort: {{ .Values.redis.port }} + volumeMounts: + - name: redis-data + mountPath: /data + volumeClaimTemplates: + - metadata: + name: checkmate-redis-persistent-storage + spec: + storageClassName: "gp3" + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: {{ .Values.persistence.redisSize }} +{{- end }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/server-deployment.yaml b/charts/helm/checkmate/templates/server-deployment.yaml index 6f21f86f3..19affe8d8 100644 --- a/charts/helm/checkmate/templates/server-deployment.yaml +++ b/charts/helm/checkmate/templates/server-deployment.yaml @@ -1,16 +1,16 @@ apiVersion: apps/v1 kind: Deployment metadata: - name: server + name: checkmate-server spec: replicas: 1 selector: matchLabels: - app: server + app: checkmate-server template: metadata: labels: - app: server + app: checkmate-server spec: containers: - name: server diff --git a/charts/helm/checkmate/templates/server-ingress.yaml b/charts/helm/checkmate/templates/server-ingress.yaml index c1982d451..3755c94b8 100644 --- a/charts/helm/checkmate/templates/server-ingress.yaml +++ b/charts/helm/checkmate/templates/server-ingress.yaml @@ -2,15 +2,22 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: server-ingress + name: checkmate-server-ingress + {{- if .Values.server.ingress.annotations }} annotations: - nginx.ingress.kubernetes.io/rewrite-target: / - nginx.ingress.kubernetes.io/enable-cors: "true" - nginx.ingress.kubernetes.io/cors-allow-origin: "http://{{ .Values.client.ingress.host }},https://{{ .Values.client.ingress.host }}" - nginx.ingress.kubernetes.io/cors-allow-methods: "GET, PUT, POST, DELETE, PATCH, OPTIONS" - nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" - nginx.ingress.kubernetes.io/cors-allow-credentials: "true" + {{- range $key, $value := .Values.server.ingress.annotations }} + {{ $key }}: {{ $value | quote }} + {{- end }} + {{- end }} + {{/*#annotations: + #nginx.ingress.kubernetes.io/rewrite-target: / + #nginx.ingress.kubernetes.io/enable-cors: "true" + #nginx.ingress.kubernetes.io/cors-allow-origin: "http://{{ .Values.client.ingress.host }},https://{{ .Values.client.ingress.host }}" + #nginx.ingress.kubernetes.io/cors-allow-methods: "GET, PUT, POST, DELETE, PATCH, OPTIONS" + #nginx.ingress.kubernetes.io/cors-allow-headers: "DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range,Authorization" + #nginx.ingress.kubernetes.io/cors-allow-credentials: "true"*/}} spec: + ingressClassName: {{ .Values.server.ingress.className }} rules: - host: {{ .Values.server.ingress.host }} http: diff --git a/charts/helm/checkmate/templates/server-service.yaml b/charts/helm/checkmate/templates/server-service.yaml index 64494ff90..d8a7fbe86 100644 --- a/charts/helm/checkmate/templates/server-service.yaml +++ b/charts/helm/checkmate/templates/server-service.yaml @@ -1,9 +1,9 @@ apiVersion: v1 kind: Service metadata: - name: server + name: checkmate-server spec: selector: - app: server + app: checkmate-server ports: - port: {{ .Values.server.port }} diff --git a/charts/helm/checkmate/values.yaml b/charts/helm/checkmate/values.yaml index 79c77a49e..1840cf909 100644 --- a/charts/helm/checkmate/values.yaml +++ b/charts/helm/checkmate/values.yaml @@ -1,40 +1,46 @@ client: - image: ghcr.io/bluewave-labs/checkmate-client:v2.3 + image: ghcr.io/bluewave-labs/checkmate-client:v3.2.0 port: 80 ingress: enabled: true host: change_me + className: nginx + annotations: {} server: - image: ghcr.io/bluewave-labs/checkmate-backend:v2.3 + image: ghcr.io/bluewave-labs/checkmate-backend:v3.2.0 port: 52345 - protocol: change_me + protocol: http ingress: enabled: true host: change_me + className: nginx + annotations: {} redis: + enabled: false image: redis:7.2 port: 6379 mongodb: - image: mongo:7.0 + image: ghcr.io/bluewave-labs/checkmate-mongo:v3.2.0 port: 27017 secrets: JWT_SECRET: change_me - REFRESH_TOKEN_SECRET: change_me - SYSTEM_EMAIL_ADDRESS: test@example.com - SYSTEM_EMAIL_PASSWORD: change_me - SYSTEM_EMAIL_HOST: smtp.example.com - SYSTEM_EMAIL_PORT: "587" - PAGESPEED_API_KEY: change_me - DB_CONNECTION_STRING: mongodb://mongodb:27017/uptime_db - REDIS_HOST: redis - REDIS_PORT: "6379" - DB_TYPE: MongoDB - TOKEN_TTL: 99d - REFRESH_TOKEN_TTL: 99d +# REFRESH_TOKEN_SECRET: change_me +# SYSTEM_EMAIL_ADDRESS: test@example.com +# SYSTEM_EMAIL_PASSWORD: change_me +# SYSTEM_EMAIL_HOST: smtp.example.com +# SYSTEM_EMAIL_PORT: "587" +# PAGESPEED_API_KEY: change_me + DB_CONNECTION_STRING: mongodb://checkmate-mongodb.namespace.svc:27017/uptime_db + CLIENT_HOST: change_me +# REDIS_HOST: redis +# REDIS_PORT: "6379" +# DB_TYPE: MongoDB +# TOKEN_TTL: 99d +# REFRESH_TOKEN_TTL: 99d persistence: mongodbSize: 5Gi From c6cf7a46391a9608156c385e0f626d922e6b2e90 Mon Sep 17 00:00:00 2001 From: ariretiarno Date: Tue, 30 Sep 2025 11:30:21 +0700 Subject: [PATCH 041/400] helm chart --- charts/helm/checkmate/templates/secrets.yaml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/charts/helm/checkmate/templates/secrets.yaml b/charts/helm/checkmate/templates/secrets.yaml index 0a3f1ace5..af104e7d1 100644 --- a/charts/helm/checkmate/templates/secrets.yaml +++ b/charts/helm/checkmate/templates/secrets.yaml @@ -4,10 +4,6 @@ {{- fail "secrets.JWT_SECRET must be overridden and cannot be 'change_me'" }} {{- end }} -{{- if or (not $secrets.REFRESH_TOKEN_SECRET) (eq $secrets.REFRESH_TOKEN_SECRET "change_me") }} - {{- fail "secrets.REFRESH_TOKEN_SECRET must be overridden and cannot be 'change_me'" }} -{{- end }} - apiVersion: v1 kind: Secret metadata: From 4744c05f07669a0110ae634d26f756c38d88c633 Mon Sep 17 00:00:00 2001 From: ariretiarno Date: Tue, 30 Sep 2025 11:35:14 +0700 Subject: [PATCH 042/400] pvc --- charts/helm/checkmate/templates/mongodb-statefulsets.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/helm/checkmate/templates/mongodb-statefulsets.yaml b/charts/helm/checkmate/templates/mongodb-statefulsets.yaml index 4b490d7d7..1ffd1c228 100644 --- a/charts/helm/checkmate/templates/mongodb-statefulsets.yaml +++ b/charts/helm/checkmate/templates/mongodb-statefulsets.yaml @@ -19,7 +19,7 @@ spec: - containerPort: {{ .Values.mongodb.port }} command: ["mongod", "--quiet", "--bind_ip_all"] volumeMounts: - - name: mongodb-data + - name: checkmate-mongo-persistent-storage mountPath: /data/db volumeClaimTemplates: - metadata: From 7a352cdcb34a962798dae4f03c1a073809b5addf Mon Sep 17 00:00:00 2001 From: ariretiarno Date: Wed, 1 Oct 2025 09:16:45 +0700 Subject: [PATCH 043/400] client host --- charts/helm/checkmate/server-nginx-cm.yaml | 41 +++++++++++++++++++ .../templates/server-deployment.yaml | 3 ++ 2 files changed, 44 insertions(+) create mode 100644 charts/helm/checkmate/server-nginx-cm.yaml diff --git a/charts/helm/checkmate/server-nginx-cm.yaml b/charts/helm/checkmate/server-nginx-cm.yaml new file mode 100644 index 000000000..64eca82b7 --- /dev/null +++ b/charts/helm/checkmate/server-nginx-cm.yaml @@ -0,0 +1,41 @@ +apiVersion: apps/v1 +kind: ConfigMap +metadata: + name: checkmate-server-nginx-cm +data: + default.conf: | + server { + listen 80; + listen [::]:80; + + server_name checkmate-demo.bluewavelabs.ca; + server_tokens off; + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + } + + location / { + root /usr/share/nginx/html; + index index.html index.htm; + try_files $uri $uri/ /index.html; + } + + # location /api/ { + # proxy_pass http://server:5000/api/; + # proxy_http_version 1.1; + # proxy_set_header Host $host; + # proxy_set_header X-Real-IP $remote_addr; + # proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + # proxy_set_header X-Forwarded-Proto $scheme; + # } + + location /api-docs/ { + proxy_pass http://server:5000/api-docs/; + proxy_http_version 1.1; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + } \ No newline at end of file diff --git a/charts/helm/checkmate/templates/server-deployment.yaml b/charts/helm/checkmate/templates/server-deployment.yaml index 19affe8d8..856e16922 100644 --- a/charts/helm/checkmate/templates/server-deployment.yaml +++ b/charts/helm/checkmate/templates/server-deployment.yaml @@ -20,3 +20,6 @@ spec: envFrom: - secretRef: name: checkmate-secrets + env: + - name: CLIENT_HOST + value: {{ .Values.client.ingress.host }} From d63731fcedcab31c58a94121e4d93dcfe21559ab Mon Sep 17 00:00:00 2001 From: ariretiarno Date: Wed, 1 Oct 2025 09:22:42 +0700 Subject: [PATCH 044/400] client host --- charts/helm/checkmate/templates/server-deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/helm/checkmate/templates/server-deployment.yaml b/charts/helm/checkmate/templates/server-deployment.yaml index 856e16922..8c9403916 100644 --- a/charts/helm/checkmate/templates/server-deployment.yaml +++ b/charts/helm/checkmate/templates/server-deployment.yaml @@ -22,4 +22,4 @@ spec: name: checkmate-secrets env: - name: CLIENT_HOST - value: {{ .Values.client.ingress.host }} + value: "http://{{ .Values.client.ingress.host }}" From 8d48b2cc8a8c24e1c9c5af4f512708fa70dfe463 Mon Sep 17 00:00:00 2001 From: ariretiarno Date: Wed, 1 Oct 2025 09:33:32 +0700 Subject: [PATCH 045/400] client host --- charts/helm/checkmate/templates/client-deployment.yaml | 8 ++++++++ charts/helm/checkmate/templates/client-ingress.yaml | 2 +- charts/helm/checkmate/templates/server-ingress.yaml | 2 +- .../helm/checkmate/{ => templates}/server-nginx-cm.yaml | 6 +++--- 4 files changed, 13 insertions(+), 5 deletions(-) rename charts/helm/checkmate/{ => templates}/server-nginx-cm.yaml (86%) diff --git a/charts/helm/checkmate/templates/client-deployment.yaml b/charts/helm/checkmate/templates/client-deployment.yaml index b6e13fc7d..a5d487dc8 100644 --- a/charts/helm/checkmate/templates/client-deployment.yaml +++ b/charts/helm/checkmate/templates/client-deployment.yaml @@ -22,3 +22,11 @@ spec: value: "{{ .Values.server.protocol }}://{{ .Values.server.ingress.host }}/api/v1" - name: UPTIME_APP_CLIENT_HOST value: "http://{{ .Values.client.ingress.host }}" + volumeMounts: + - name: config-volume + mountPath: /etc/nginx/conf.d/default.conf + subPath: default.conf + volumes: + - name: config-volume + configMap: + name: checkmate-server-nginx-cm diff --git a/charts/helm/checkmate/templates/client-ingress.yaml b/charts/helm/checkmate/templates/client-ingress.yaml index 014aa53b3..89e001e15 100644 --- a/charts/helm/checkmate/templates/client-ingress.yaml +++ b/charts/helm/checkmate/templates/client-ingress.yaml @@ -19,7 +19,7 @@ spec: pathType: Prefix backend: service: - name: client + name: checkmate-client port: number: {{ .Values.client.port }} {{- end }} diff --git a/charts/helm/checkmate/templates/server-ingress.yaml b/charts/helm/checkmate/templates/server-ingress.yaml index 3755c94b8..0d7ca867b 100644 --- a/charts/helm/checkmate/templates/server-ingress.yaml +++ b/charts/helm/checkmate/templates/server-ingress.yaml @@ -26,7 +26,7 @@ spec: pathType: Prefix backend: service: - name: server + name: checkmate-server port: number: {{ .Values.server.port }} {{- end }} diff --git a/charts/helm/checkmate/server-nginx-cm.yaml b/charts/helm/checkmate/templates/server-nginx-cm.yaml similarity index 86% rename from charts/helm/checkmate/server-nginx-cm.yaml rename to charts/helm/checkmate/templates/server-nginx-cm.yaml index 64eca82b7..84da55299 100644 --- a/charts/helm/checkmate/server-nginx-cm.yaml +++ b/charts/helm/checkmate/templates/server-nginx-cm.yaml @@ -1,4 +1,4 @@ -apiVersion: apps/v1 +apiVersion: v1 kind: ConfigMap metadata: name: checkmate-server-nginx-cm @@ -22,7 +22,7 @@ data: } # location /api/ { - # proxy_pass http://server:5000/api/; + # proxy_pass http://{{ .Values.server.ingress.host }}:5000/api/; # proxy_http_version 1.1; # proxy_set_header Host $host; # proxy_set_header X-Real-IP $remote_addr; @@ -31,7 +31,7 @@ data: # } location /api-docs/ { - proxy_pass http://server:5000/api-docs/; + proxy_pass http://{{ .Values.server.ingress.host }}:5000/api-docs/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; From 468a67f937353580e10c5ef370f403de6bf70285 Mon Sep 17 00:00:00 2001 From: karenvicent Date: Wed, 1 Oct 2025 11:34:51 -0400 Subject: [PATCH 046/400] git stash push -m "work in progress" --- client/src/Hooks/v1/userHooks.js | 26 +++- .../components/ChangePasswordModal/index.jsx | 140 ++++++++++++++++++ .../src/Pages/v1/Account/EditUser/index.jsx | 19 ++- client/src/Utils/NetworkService.js | 10 ++ client/src/locales/en.json | 8 +- server/src/controllers/v1/authController.js | 23 +++ server/src/routes/v1/authRoute.js | 1 + server/src/service/v1/business/userService.js | 5 + server/src/validation/joi.js | 5 + 9 files changed, 230 insertions(+), 7 deletions(-) create mode 100644 client/src/Pages/Account/components/ChangePasswordModal/index.jsx diff --git a/client/src/Hooks/v1/userHooks.js b/client/src/Hooks/v1/userHooks.js index 71b156594..f9c454450 100644 --- a/client/src/Hooks/v1/userHooks.js +++ b/client/src/Hooks/v1/userHooks.js @@ -7,7 +7,6 @@ export const useGetUser = (userId) => { const [user, setUser] = useState(undefined); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); - const fetchUser = useCallback(async () => { try { setIsLoading(true); @@ -54,8 +53,29 @@ export const useEditUser = (userId) => { setIsLoading(false); } }, - [userId] + [userId, t] ); - return [editUser, isLoading, error]; + const changePassword = useCallback( + async (passwordForm) => { + try { + setIsLoading(true); + + await networkService.changePasswordByAdmin({ userId, passwordForm }); + createToast({ + body: t("teamPanel.changeTeamPassword.success"), + }); + } catch (error) { + createToast({ + body: error.message, + }); + setError(error); + } finally { + setIsLoading(false); + } + }, + [userId, t] + ); + + return [editUser, isLoading, error, changePassword]; }; diff --git a/client/src/Pages/Account/components/ChangePasswordModal/index.jsx b/client/src/Pages/Account/components/ChangePasswordModal/index.jsx new file mode 100644 index 000000000..375595b29 --- /dev/null +++ b/client/src/Pages/Account/components/ChangePasswordModal/index.jsx @@ -0,0 +1,140 @@ +import { useState } from "react"; +import { Button, Stack } from "@mui/material"; +import { GenericDialog } from "../../../../Components/Dialog/genericDialog"; +import TextInput from "../../../../Components/Inputs/TextInput"; +import PasswordTooltip from "../../../Auth/components/PasswordTooltip"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import { createToast } from "../../../../Utils/toastUtils"; +import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments"; +import usePasswordFeedback from "../../../Auth/hooks/usePasswordFeedback"; + +const ChangePasswordModal = ({ isSaving, isLoading, userId, changePassword, email }) => { + const INITIAL_FORM_STATE = { + password: "", + confirm: "", + }; + console.log("email", email); + const theme = useTheme(); + const { t } = useTranslation(); + const { feedback, handlePasswordFeedback } = usePasswordFeedback(); + const [form, setForm] = useState(INITIAL_FORM_STATE); + const [isChangePasswordOpen, setIsChangePasswordOpen] = useState(false); + const [errors, setErrors] = useState({}); + const [isLoadingSubmit, setIsLoadingSubmit] = useState(false); + const closeChangePasswordModal = () => { + setIsChangePasswordOpen(false); + setForm(INITIAL_FORM_STATE); + }; + + const onChange = (e) => { + let { name, value } = e.target; + const updatedForm = { ...form, [name]: value }; + //validateFields(name, value, updatedForm); + setForm(updatedForm); + + handlePasswordFeedback(updatedForm, name, value, form, errors, setErrors); + }; + const isFormValid = !errors.password && !errors.confirm; + const onsubmitChangePassword = async (event) => { + event.preventDefault(); + if (!isFormValid) return; + const newPasswordForm = { + password: form.password, + newPassword: form.confirm, + email: email, + }; + try { + setIsLoadingSubmit(true); + await changePassword(newPasswordForm); + closeChangePasswordModal(); + } catch (error) { + const errorMsg = error.response?.data?.msg || error.message || "unknownError"; + createToast({ + type: "error", + body: t(errorMsg), + }); + } finally { + setIsLoadingSubmit(false); + } + }; + return ( + <> + + + + } + sx={{ mb: theme.spacing(5) }} + /> + + } + sx={{ mb: theme.spacing(5) }} + /> + + + + + + + + ); +}; +export default ChangePasswordModal; diff --git a/client/src/Pages/v1/Account/EditUser/index.jsx b/client/src/Pages/v1/Account/EditUser/index.jsx index e2e976916..9fe307cf2 100644 --- a/client/src/Pages/v1/Account/EditUser/index.jsx +++ b/client/src/Pages/v1/Account/EditUser/index.jsx @@ -7,6 +7,7 @@ import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; import Search from "@/Components/v1/Inputs/Search/index.jsx"; import Button from "@mui/material/Button"; import RoleTable from "../components/RoleTable/index.jsx"; +import ChangePasswordModal from "../components/ChangePasswordModal"; // Utils import { useParams } from "react-router-dom"; @@ -26,7 +27,7 @@ const EditUser = () => { ]; const [user, isLoading, error] = useGetUser(userId); - const [editUser, isSaving, saveError] = useEditUser(userId); + const [editUser, isSaving, saveError, changePassword] = useEditUser(userId); const [ form, setForm, @@ -104,7 +105,12 @@ const EditUser = () => { roles={form?.role} handleDeleteRole={handleDeleteRole} /> - + - + + ); diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js index dce0faaf5..7401994bb 100644 --- a/client/src/Utils/NetworkService.js +++ b/client/src/Utils/NetworkService.js @@ -1136,6 +1136,16 @@ class NetworkService { }, }); } + + async changePasswordByAdmin(config) { + const { userId, passwordForm } = config; + console.log("Password form data:", passwordForm); + return this.axiosInstance.put(`auth/users/${userId}/password`, passwordForm, { + headers: { + "Content-Type": "application/json", + }, + }); + } } export default NetworkService; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index d89ebc44e..642c8e0f6 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -425,7 +425,13 @@ } } }, - "role": "Role" + "role": "Role", + "changeTeamPassword": { + "changePasswordMenu": "Reset Password", + "title": "Reset team member password", + "description": "Create a new password for this team member. You will need to share the password with them securely.", + "success": "Password successfully reset. Make sure to provide the credentials to the member in a secure way." + } }, "monitorState": { "paused": "Paused", diff --git a/server/src/controllers/v1/authController.js b/server/src/controllers/v1/authController.js index 17ed517eb..b84aaacd6 100755 --- a/server/src/controllers/v1/authController.js +++ b/server/src/controllers/v1/authController.js @@ -456,6 +456,29 @@ class AuthController extends BaseController { SERVICE_NAME, "editUserById" ); + editUserPasswordById = this.asyncHandler( + async (req, res) => { + console.log("editUserPasswordById called"); + const roles = req?.user?.role; + if (!roles.includes("superadmin")) { + console.log("entro a error editUserPasswordById"); + throw createError("Unauthorized", 403); + } + + const userId = req.params.userId; + console.log("userId param:", userId); + const updates = { ...req.body }; + console.log("newPassword body:", req.body); + await editUserByIdParamValidation.validateAsync(req.params); + console.log("params validated"); + //await editUserPasswordByIdBodyValidation.validateAsync(req.body); + console.log("body validated"); + await this.userService.setPasswordByUserId( userId, updates ); + return res.success({ msg: "Password reset successfully" }); + }, + SERVICE_NAME, "editUserPasswordById" + ) + } export default AuthController; diff --git a/server/src/routes/v1/authRoute.js b/server/src/routes/v1/authRoute.js index d9b036b05..63b4c4201 100755 --- a/server/src/routes/v1/authRoute.js +++ b/server/src/routes/v1/authRoute.js @@ -25,6 +25,7 @@ class AuthRoutes { this.router.get("/users", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.getAllUsers); this.router.get("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.getUserById); this.router.put("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserById); + this.router.put("/users/:userId/password", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserPasswordById); this.router.put("/user", verifyJWT, upload.single("profileImage"), this.authController.editUser); this.router.delete("/user", verifyJWT, this.authController.deleteUser); diff --git a/server/src/service/v1/business/userService.js b/server/src/service/v1/business/userService.js index 0a965bced..6c036a5c8 100644 --- a/server/src/service/v1/business/userService.js +++ b/server/src/service/v1/business/userService.js @@ -211,5 +211,10 @@ class UserService { editUserById = async (userId, user) => { await this.db.userModule.editUserById(userId, user); }; + setPasswordByUserId = async (userId, updates) => { + const updatedUser = await this.db.userModule.updateUser({ userId: userId, user: updates, file: null }); + return updatedUser; + } + } export default UserService; diff --git a/server/src/validation/joi.js b/server/src/validation/joi.js index fa8dccde2..209135769 100755 --- a/server/src/validation/joi.js +++ b/server/src/validation/joi.js @@ -670,6 +670,10 @@ const editSuperadminUserByIdBodyValidation = joi.object({ .required(), }); +const editUserPasswordByIdBodyValidation = joi.object({ + newPassword: joi.string().min(8).required().pattern(passwordPattern), +}); + export { roleValidatior, loginValidation, @@ -739,4 +743,5 @@ export { editUserByIdParamValidation, editUserByIdBodyValidation, editSuperadminUserByIdBodyValidation, + editUserPasswordByIdBodyValidation, }; From 3ae2f857d4169f13cec49166dae0dd42ed9776ed Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 1 Oct 2025 13:55:32 -0700 Subject: [PATCH 047/400] add v2 sidebar --- .../v2/Layouts/Sidebar/BottomControls.tsx | 16 +++ .../Components/v2/Layouts/Sidebar/Menu.tsx | 58 ++++++++++ .../Components/v2/Layouts/Sidebar/NavItem.tsx | 103 ++++++++++++++++++ .../Components/v2/Layouts/Sidebar/index.tsx | 65 ++++++++++- client/src/Features/Auth/v2AuthSlice.ts | 19 ++++ client/src/Pages/v2/Auth/Login.tsx | 11 +- client/src/Routes/v2router.tsx | 6 +- client/src/Utils/Theme/v2/palette.ts | 2 + client/src/store.js | 4 +- client/src/types/mui.d.ts | 1 + 10 files changed, 279 insertions(+), 6 deletions(-) create mode 100644 client/src/Components/v2/Layouts/Sidebar/BottomControls.tsx create mode 100644 client/src/Components/v2/Layouts/Sidebar/Menu.tsx create mode 100644 client/src/Components/v2/Layouts/Sidebar/NavItem.tsx create mode 100644 client/src/Features/Auth/v2AuthSlice.ts diff --git a/client/src/Components/v2/Layouts/Sidebar/BottomControls.tsx b/client/src/Components/v2/Layouts/Sidebar/BottomControls.tsx new file mode 100644 index 000000000..a07ccde6c --- /dev/null +++ b/client/src/Components/v2/Layouts/Sidebar/BottomControls.tsx @@ -0,0 +1,16 @@ +import { useTheme } from "@mui/material/styles"; + +import Stack from "@mui/material/Stack"; + +export const BottomControls = ({}) => { + const theme = useTheme(); + return ( + + ); +}; diff --git a/client/src/Components/v2/Layouts/Sidebar/Menu.tsx b/client/src/Components/v2/Layouts/Sidebar/Menu.tsx new file mode 100644 index 000000000..5b3ed705e --- /dev/null +++ b/client/src/Components/v2/Layouts/Sidebar/Menu.tsx @@ -0,0 +1,58 @@ +import Notifications from "@/assets/icons/notifications.svg?react"; +import Monitors from "@/assets/icons/monitors.svg?react"; +import PageSpeed from "@/assets/icons/page-speed.svg?react"; +import Integrations from "@/assets/icons/integrations.svg?react"; +import Incidents from "@/assets/icons/incidents.svg?react"; +import StatusPages from "@/assets/icons/status-pages.svg?react"; +import Maintenance from "@/assets/icons/maintenance.svg?react"; +import Logs from "@/assets/icons/logs.svg?react"; +import Settings from "@/assets/icons/settings.svg?react"; +import Support from "@/assets/icons/support.svg?react"; +import Discussions from "@/assets/icons/discussions.svg?react"; +import Docs from "@/assets/icons/docs.svg?react"; +import ChangeLog from "@/assets/icons/changeLog.svg?react"; + +export const getMenu = (t: Function) => [ + { name: t("menu.uptime"), path: "v2/uptime", icon: }, + { name: t("menu.pagespeed"), path: "pagespeed", icon: }, + + { name: t("menu.infrastructure"), path: "infrastructure", icon: }, + { + name: t("menu.notifications"), + path: "notifications", + icon: , + }, + { name: t("menu.incidents"), path: "incidents", icon: }, + + { name: t("menu.statusPages"), path: "status", icon: }, + { name: t("menu.maintenance"), path: "maintenance", icon: }, + { name: t("menu.logs"), path: "logs", icon: }, + + { + name: t("menu.settings"), + icon: , + path: "settings", + }, +]; + +export const getBottomMenu = (t: Function) => [ + { name: t("menu.support"), path: "support", icon: , url: "invite" }, + { + name: t("menu.discussions"), + path: "discussions", + icon: , + url: "https://github.com/bluewave-labs/checkmate/discussions", + }, + { + name: t("menu.docs"), + path: "docs", + icon: , + url: "https://bluewavelabs.gitbook.io/checkmate", + }, + { + name: t("menu.changelog"), + path: "changelog", + icon: , + url: "https://github.com/bluewave-labs/checkmate/releases", + }, +]; diff --git a/client/src/Components/v2/Layouts/Sidebar/NavItem.tsx b/client/src/Components/v2/Layouts/Sidebar/NavItem.tsx new file mode 100644 index 000000000..8f7f5d68c --- /dev/null +++ b/client/src/Components/v2/Layouts/Sidebar/NavItem.tsx @@ -0,0 +1,103 @@ +import Tooltip from "@mui/material/Tooltip"; +import ListItemButton from "@mui/material/ListItemButton"; +import ListItemIcon from "@mui/material/ListItemIcon"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; + +export interface NavData { + name: string; + icon: JSX.Element; +} + +export const NavItem = ({ + item, + collapsed, + selected, + onClick, +}: { + item: NavData; + collapsed: boolean; + selected: boolean; + onClick: (event: React.MouseEvent) => void; +}) => { + const theme = useTheme(); + const iconStroke = selected + ? theme.palette.primary.contrastText + : theme.palette.primary.contrastTextTertiary; + + const buttonBgColor = selected ? theme.palette.secondary.main : "transparent"; + const buttonBgHoverColor = selected + ? theme.palette.secondary.main + : theme.palette.tertiary.main; + const fontWeight = selected ? 600 : 400; + return ( + + + + {item.icon} + + + + {item.name} + + + + + ); +}; diff --git a/client/src/Components/v2/Layouts/Sidebar/index.tsx b/client/src/Components/v2/Layouts/Sidebar/index.tsx index 43975adfc..b05716aee 100644 --- a/client/src/Components/v2/Layouts/Sidebar/index.tsx +++ b/client/src/Components/v2/Layouts/Sidebar/index.tsx @@ -3,10 +3,17 @@ import { useSelector, useDispatch } from "react-redux"; import { setCollapsed } from "@/Features/UI/uiSlice"; import useMediaQuery from "@mui/material/useMediaQuery"; import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; +import { useNavigate, useLocation } from "react-router-dom"; import { CollapseButton } from "@/Components/v2/Layouts/Sidebar/CollapseButton"; import Stack from "@mui/material/Stack"; +import List from "@mui/material/List"; +import Divider from "@mui/material/Divider"; import { Logo } from "@/Components/v2/Layouts/Sidebar/Logo"; +import { getMenu, getBottomMenu } from "@/Components/v2/Layouts/Sidebar/Menu"; +import { NavItem } from "@/Components/v2/Layouts/Sidebar/NavItem"; +import { BottomControls } from "@/Components/v2/Layouts/Sidebar/BottomControls"; export const COLLAPSED_WIDTH = 64; export const EXPANDED_WIDTH = 250; @@ -16,6 +23,11 @@ export const SideBar = () => { const isSmall = useMediaQuery(theme.breakpoints.down("md")); const dispatch = useDispatch(); const collapsed = useSelector((state: any) => state.ui.sidebar.collapsed); + const { t } = useTranslation(); + const navigate = useNavigate(); + const location = useLocation(); + const menu = getMenu(t); + const bottomMenu = getBottomMenu(t); useEffect(() => { dispatch(setCollapsed({ collapsed: isSmall })); @@ -23,6 +35,7 @@ export const SideBar = () => { return ( { borderRight={`1px solid ${theme.palette.primary.lowContrast}`} width={collapsed ? COLLAPSED_WIDTH : EXPANDED_WIDTH} sx={{ - transition: "width 0.3s ease", + transition: "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)", }} > + + {menu.map((item) => { + const selected = location.pathname.startsWith(`/${item.path}`); + return ( + navigate(`/${item.path}`)} + /> + ); + })} + + + {bottomMenu.map((item) => { + const selected = location.pathname.startsWith(`/${item.path}`); + + return ( + { + if (item.url) { + window.open(item.url, "_blank", "noreferrer"); + } else { + navigate(`/${item.path}`); + } + }} + /> + ); + })} + + + ); }; diff --git a/client/src/Features/Auth/v2AuthSlice.ts b/client/src/Features/Auth/v2AuthSlice.ts new file mode 100644 index 000000000..28a18b659 --- /dev/null +++ b/client/src/Features/Auth/v2AuthSlice.ts @@ -0,0 +1,19 @@ +import { createSlice } from "@reduxjs/toolkit"; + +const initialState = { + isAuthenticated: false, +}; + +const v2AuthSlice = createSlice({ + name: "v2Auth", + initialState, + reducers: { + setIsAuthenticated: (state, action) => { + const { authenticated } = action.payload; + state.isAuthenticated = authenticated; + }, + }, +}); + +export default v2AuthSlice.reducer; +export const { setIsAuthenticated } = v2AuthSlice.actions; diff --git a/client/src/Pages/v2/Auth/Login.tsx b/client/src/Pages/v2/Auth/Login.tsx index fd4a08e93..2a5629917 100644 --- a/client/src/Pages/v2/Auth/Login.tsx +++ b/client/src/Pages/v2/Auth/Login.tsx @@ -3,7 +3,9 @@ import { z } from "zod"; import { useTranslation } from "react-i18next"; import { useForm, Controller } from "react-hook-form"; import { useTheme } from "@mui/material/styles"; - +import { useDispatch } from "react-redux"; +import { setIsAuthenticated } from "@/Features/Auth/v2AuthSlice"; +import { useNavigate } from "react-router"; import { TextInput } from "@/Components/v2/Inputs/TextInput"; import Button from "@mui/material/Button"; import Stack from "@mui/material/Stack"; @@ -19,8 +21,10 @@ type FormData = z.infer; const Login = () => { const { t } = useTranslation(); + const dispatch = useDispatch(); const theme = useTheme(); const { post, loading, error } = usePost("/auth/login"); + const navigate = useNavigate(); const { handleSubmit, @@ -37,9 +41,10 @@ const Login = () => { const onSubmit = async (data: FormData) => { const result = await post(data); if (result) { - console.log(result.message); + dispatch(setIsAuthenticated({ authenticated: true })); + navigate("/v2/uptime"); } else { - console.error("Login failed:", error); + dispatch(setIsAuthenticated({ authenticated: false })); } }; diff --git a/client/src/Routes/v2router.tsx b/client/src/Routes/v2router.tsx index 941550026..12d3eb595 100644 --- a/client/src/Routes/v2router.tsx +++ b/client/src/Routes/v2router.tsx @@ -25,7 +25,11 @@ const V2Routes = ({ mode = "light" }) => { element={} > Uptime} + /> + Test Page} /> diff --git a/client/src/Utils/Theme/v2/palette.ts b/client/src/Utils/Theme/v2/palette.ts index 2e41a159c..855f7b6b8 100644 --- a/client/src/Utils/Theme/v2/palette.ts +++ b/client/src/Utils/Theme/v2/palette.ts @@ -54,6 +54,7 @@ export const lightPalette = { main: colors.offWhite, contrastText: colors.blueGray800, contrastTextSecondary: colors.blueGray600, + contrastTextTertiary: colors.blueGray500, lowContrast: colors.gray250, }, secondary: { @@ -78,6 +79,7 @@ export const darkPalette = { main: colors.offBlack, contrastText: colors.blueGray50, contrastTextSecondary: colors.gray200, + contrastTextTertiary: colors.gray500, lowContrast: colors.blueGray600, }, secondary: { diff --git a/client/src/store.js b/client/src/store.js index 69aa3d50b..565d68519 100644 --- a/client/src/store.js +++ b/client/src/store.js @@ -1,6 +1,7 @@ import { configureStore, combineReducers } from "@reduxjs/toolkit"; import authReducer from "./Features/Auth/authSlice"; +import v2AuthReducer from "./Features/Auth/v2AuthSlice"; import uiReducer from "./Features/UI/uiSlice"; import storage from "redux-persist/lib/storage"; import { persistReducer, persistStore, createTransform } from "redux-persist"; @@ -19,12 +20,13 @@ const authTransform = createTransform( const persistConfig = { key: "root", storage, - whitelist: ["auth", "ui"], + whitelist: ["auth", "v2Auth", "ui"], transforms: [authTransform], }; const rootReducer = combineReducers({ auth: authReducer, + v2Auth: v2AuthReducer, ui: uiReducer, }); diff --git a/client/src/types/mui.d.ts b/client/src/types/mui.d.ts index 5f5c1c38b..84bcd799f 100644 --- a/client/src/types/mui.d.ts +++ b/client/src/types/mui.d.ts @@ -12,6 +12,7 @@ declare module "@mui/material/styles" { interface PaletteColor { contrastTextSecondary?: string; + contrastTextTertiary?: string; lowContrast?: string; } } From 735054b4e15a5fbd024522f56019c4723053631e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 1 Oct 2025 13:57:04 -0700 Subject: [PATCH 048/400] remove unused error --- client/src/Pages/v2/Auth/Login.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/Pages/v2/Auth/Login.tsx b/client/src/Pages/v2/Auth/Login.tsx index 2a5629917..a41750c27 100644 --- a/client/src/Pages/v2/Auth/Login.tsx +++ b/client/src/Pages/v2/Auth/Login.tsx @@ -23,7 +23,7 @@ const Login = () => { const { t } = useTranslation(); const dispatch = useDispatch(); const theme = useTheme(); - const { post, loading, error } = usePost("/auth/login"); + const { post, loading } = usePost("/auth/login"); const navigate = useNavigate(); const { From 3b22ce75dbe1d261d0cd3ab72538431613cfcca7 Mon Sep 17 00:00:00 2001 From: AlexHladin Date: Thu, 2 Oct 2025 00:47:11 +0300 Subject: [PATCH 049/400] chore: drop nodemon --- server/package-lock.json | 97 ---------------------------------------- server/package.json | 5 +-- 2 files changed, 2 insertions(+), 100 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 8c9eac34c..3ea4269eb 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -56,7 +56,6 @@ "esm": "3.2.25", "globals": "^15.14.0", "mocha": "11.1.0", - "nodemon": "3.1.9", "prettier": "^3.3.3", "sinon": "19.0.2", "tsx": "4.20.5", @@ -6480,13 +6479,6 @@ "node": ">= 4" } }, - "node_modules/ignore-by-default": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", - "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", - "dev": true, - "license": "ISC" - }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -8322,58 +8314,6 @@ "node": ">=6.0.0" } }, - "node_modules/nodemon": { - "version": "3.1.9", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", - "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chokidar": "^3.5.2", - "debug": "^4", - "ignore-by-default": "^1.0.1", - "minimatch": "^3.1.2", - "pstree.remy": "^1.1.8", - "semver": "^7.5.3", - "simple-update-notifier": "^2.0.0", - "supports-color": "^5.5.0", - "touch": "^3.1.0", - "undefsafe": "^2.0.5" - }, - "bin": { - "nodemon": "bin/nodemon.js" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/nodemon" - } - }, - "node_modules/nodemon/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/nodemon/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -9421,13 +9361,6 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, - "node_modules/pstree.remy": { - "version": "1.1.8", - "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", - "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", - "dev": true, - "license": "MIT" - }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -9948,19 +9881,6 @@ "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", "license": "MIT" }, - "node_modules/simple-update-notifier": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", - "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^7.5.3" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/sinon": { "version": "19.0.2", "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", @@ -10520,16 +10440,6 @@ "node": ">=0.6" } }, - "node_modules/touch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", - "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", - "dev": true, - "license": "ISC", - "bin": { - "nodetouch": "bin/nodetouch.js" - } - }, "node_modules/tough-cookie": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", @@ -10676,13 +10586,6 @@ "node": ">=0.8.0" } }, - "node_modules/undefsafe": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", - "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", - "dev": true, - "license": "MIT" - }, "node_modules/undici": { "version": "7.15.0", "resolved": "https://registry.npmjs.org/undici/-/undici-7.15.0.tgz", diff --git a/server/package.json b/server/package.json index 3bd79722a..1ce33e237 100755 --- a/server/package.json +++ b/server/package.json @@ -6,8 +6,8 @@ "type": "module", "scripts": { "test": "c8 mocha", - "dev": "nodemon --exec tsx src/index.js", - "start": "nodemon ./dist/index.js", + "dev": "tsx watch src/index.js", + "start": "node --watch ./dist/index.js", "build": "tsc", "lint": "eslint .", "lint-fix": "eslint --fix .", @@ -65,7 +65,6 @@ "esm": "3.2.25", "globals": "^15.14.0", "mocha": "11.1.0", - "nodemon": "3.1.9", "prettier": "^3.3.3", "sinon": "19.0.2", "tsx": "4.20.5", From 851d722746cde4445c50225f24e4b9e6d50b0ef9 Mon Sep 17 00:00:00 2001 From: Vivek Kavala Date: Thu, 2 Oct 2025 12:46:51 +0530 Subject: [PATCH 050/400] fix: remove manual response time calculation in favor of library-native timing The manual timing calculations were adding Node.js processing overhead and were less accurate than the native timing provided by the respective libraries (ping, got, etc.). Now using response.time from each library for more accurate network response time measurements. --- .../v1/infrastructure/networkService.js | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/server/src/service/v1/infrastructure/networkService.js b/server/src/service/v1/infrastructure/networkService.js index 47ef6878f..676a56da8 100644 --- a/server/src/service/v1/infrastructure/networkService.js +++ b/server/src/service/v1/infrastructure/networkService.js @@ -33,11 +33,11 @@ class NetworkService { const start = process.hrtime.bigint(); try { const response = await operation(); - const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000); - return { response, responseTime: elapsedMs }; + // const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000); + return { response }; } catch (error) { - const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000); - return { response: null, responseTime: elapsedMs, error }; + // const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000); + return { response: null, error }; } } @@ -70,7 +70,12 @@ class NetworkService { throw new Error("Monitor URL is required"); } - const { response, responseTime, error } = await this.timeRequest(() => this.ping.promise.probe(monitor.url)); + const rawUrl = monitor.url; + const sanitizedHost = rawUrl + .replace(/^https?:\/\//, "") + .replace(/\/.*$/, "") + .replace(/:.*/, ""); + const { response, error } = await this.timeRequest(() => this.ping.promise.probe(sanitizedHost)); if (!response) { if (error) { @@ -84,7 +89,7 @@ class NetworkService { type: "ping", status: response.alive, code: 200, - responseTime, + responseTime: response.time, message: "Success", payload: response, }; @@ -345,9 +350,9 @@ class NetworkService { } const container = docker.getContainer(targetContainer.Id); - const { response, responseTime, error } = await this.timeRequest(() => container.inspect()); + const { response, error } = await this.timeRequest(() => container.inspect()); - dockerResponse.responseTime = responseTime; + dockerResponse.responseTime = response.time; dockerResponse.status = response?.State?.Status === "running" ? true : false; dockerResponse.code = 200; dockerResponse.message = "Docker container status fetched successfully"; @@ -370,7 +375,7 @@ class NetworkService { async requestPort(monitor) { try { const { url, port } = monitor; - const { response, responseTime, error } = await this.timeRequest(async () => { + const { response, error } = await this.timeRequest(async () => { return new Promise((resolve, reject) => { const socket = this.net.createConnection( { @@ -403,7 +408,7 @@ class NetworkService { message: this.stringService.portSuccess, monitorId: monitor._id, type: monitor.type, - responseTime, + responseTime: response.time, }; if (error) { From 080a33fac71cc5305e5c99f2d8f52ccf713c4f04 Mon Sep 17 00:00:00 2001 From: jithendra Kavala Date: Thu, 2 Oct 2025 13:51:30 +0530 Subject: [PATCH 051/400] docs: improve local development setup instructions in CONTRIBUTING.md --- CONTRIBUTING.md | 184 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 172 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 702436b38..848b7d2f7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ We truly appreciate all kinds of contributions — code, ideas, translations or Before you start, please take a moment to read the relevant section. It helps us review and accept contributions faster, and makes the whole process smoother for everyone. 💚 -PS: **We work closely with contributors on our [Discord channel](https://discord.com/invite/NAb6H3UTjK)**. You’ll find community members, core maintainers, and first-timers helping each other out. +PS: **We work closely with contributors on our [Discord channel](https://discord.com/invite/NAb6H3UTjK)**. You'll find community members, core maintainers, and first-timers helping each other out. --- @@ -32,12 +32,12 @@ PS: **We work closely with contributors on our [Discord channel](https://discord ### Get help or ask a question? -Ask anything in our [Discord server](https://discord.com/invite/NAb6H3UTjK) — we’re friendly and happy to help. [Our core contributors](https://github.com/bluewave-labs/checkmate?tab=readme-ov-file#-contributing) are active and ready to support you. You can also use [GitHub Discussions](https://github.com/bluewave-labs/Checkmate/discussions) section to ask your questions. +Ask anything in our [Discord server](https://discord.com/invite/NAb6H3UTjK) — we're friendly and happy to help. [Our core contributors](https://github.com/bluewave-labs/checkmate?tab=readme-ov-file#-contributing) are active and ready to support you. You can also use [GitHub Discussions](https://github.com/bluewave-labs/Checkmate/discussions) section to ask your questions. ### Report a bug? 1. Search [existing issues](https://github.com/bluewave-labs/checkmate/issues). -2. If it’s not listed, open a **new issue**. +2. If it's not listed, open a **new issue**. 3. Include as much detail as possible: what happened, what you expected, and steps to reproduce. Logs and screenshots help. ### Suggest a new feature? @@ -48,19 +48,181 @@ Ask anything in our [Discord server](https://discord.com/invite/NAb6H3UTjK) — ### Set up Checkmate locally? -Frontend & backend: +#### Prerequisites + +- Node.js (with npm) +- Docker (for MongoDB and Redis) +- Git + +#### Step 1: Clone the Repository + +```bash +git clone https://github.com/bluewave-labs/Checkmate.git +cd Checkmate +``` + +#### Step 2: Set Up Docker Containers (MongoDB & Redis) + +Navigate to the Docker dev directory: + +```bash +cd server/docker/dev +``` + +Build the Docker images: + +```bash +./build_images.sh +``` + +Run Redis container: + +```bash +docker run -d -p 6379:6379 -v $(pwd)/redis/data:/data --name uptime_redis uptime_redis +``` + +Run MongoDB container: + +```bash +docker run -d -p 27017:27017 -v $(pwd)/mongo/data:/data/db --name uptime_database_mongo uptime_database_mongo +``` + +Navigate back to the root directory: + +```bash +cd ../../.. +``` + +#### Step 3: Set Up the Backend (Server) + +Navigate to the server directory: + +```bash +cd server +``` + +Install dependencies: + ```bash npm install +``` + +Create a `.env` file in the `server` directory with the following minimum required configuration: + +```env +CLIENT_HOST="http://localhost:5173" +JWT_SECRET="my_secret_key_change_this" +DB_CONNECTION_STRING="mongodb://localhost:27017/uptime_db" +TOKEN_TTL="99d" +ORIGIN="localhost" +LOG_LEVEL="debug" +``` + +**Environment Variables Explained:** + +- `CLIENT_HOST`: Frontend URL (default: http://localhost:5173) +- `JWT_SECRET`: Secret key for JWT tokens (change to something secure) +- `DB_CONNECTION_STRING`: MongoDB connection URL +- `ORIGIN`: Origin for CORS purposes +- `TOKEN_TTL`: Token time to live (in vercel/ms format) +- `LOG_LEVEL`: Debug level (debug, info, warn, error) + +Start the backend server: + +```bash npm run dev ``` -By default, the frontend expects the backend on `http://localhost:3001`. Update configs if needed. +The server will run at `http://localhost:52345`. + +#### Step 4: Set Up the Frontend (Client) + +Open a new terminal window and navigate to the client directory from the root: + +```bash +cd client +``` + +Install dependencies: + +```bash +npm install +``` + +Create a `.env` file in the `client` directory: + +```env +VITE_APP_API_BASE_URL="http://localhost:52345/api/v1" +VITE_APP_LOG_LEVEL="debug" +``` + +**Environment Variables Explained:** + +- `VITE_APP_API_BASE_URL`: Backend API URL +- `VITE_APP_LOG_LEVEL`: Log level (none, error, warn, debug, info) + +Start the frontend: + +```bash +npm run dev +``` + +The client will run at `http://localhost:5173`. + +#### Step 5: Access the Application + +Open your browser and navigate to: + +- **Frontend**: http://localhost:5173 +- **Backend API**: http://localhost:52345 +- **API Documentation**: http://localhost:52345/api-docs + +#### Managing Docker Containers + +Stop containers: + +```bash +docker stop uptime_redis uptime_database_mongo +``` + +Start containers: + +```bash +docker start uptime_redis uptime_database_mongo +``` + +Remove containers (if needed): + +```bash +docker rm uptime_redis uptime_database_mongo +``` + +#### Troubleshooting + +**Port already in use:** + +- Check if another service is using ports 5173, 52345, 27017, or 6379 +- Stop the conflicting service or change the port in `.env` files + +**MongoDB/Redis connection issues:** + +- Verify containers are running: `docker ps` +- Check container logs: `docker logs uptime_database_mongo` or `docker logs uptime_redis` + +**Module not found errors:** + +- Ensure you ran `npm install` in both `client` and `server` directories + +**Need more help?** + +- Check the [full documentation](https://docs.checkmate.so) +- Ask on [Discord](https://discord.com/invite/NAb6H3UTjK) ### Start contributing code? 1. Pick or open an issue (check `good-first-issue`s first) 2. (optional but highly suggested) Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture. -3. Ask to be assigned. If there is alrady someone assigned and it's been more than 7 days, you can raise the flag and ask to be assigned as well. +3. Ask to be assigned. If there is already someone assigned and it's been more than 7 days, you can raise the flag and ask to be assigned as well. 4. Create a branch from `develop`. 5. Write your code. 6. Run and test locally. @@ -75,6 +237,7 @@ Docs live in [checkmate-documentation](https://github.com/bluewave-labs/checkmat ### Help with translations? We use [PoEditor](https://poeditor.com) for translations. You can: + - [Sign up and join your language team](https://poeditor.com/join/project/lRUoGZFCsJ). - Translate UI strings. - Ask questions on Discord in the relevant #translations channel. @@ -84,6 +247,7 @@ Make sure all new UI strings in code use `t('key')`. ### Submit a pull request? Follow the [pull request checklist](#pull-request-checklist). Your PR should: + - Be focused on one issue. - Be tested locally. - Use our linting and translation rules. @@ -123,7 +287,6 @@ If one or more of these are missing, we may ask you to update your pull request ## Branching model - - Code contributions should go to the `develop` branch. - `master` is used for stable releases. - Use descriptive branch names, like `fix/login-error` or `feat/add-alerts`. @@ -137,13 +300,10 @@ If one or more of these are missing, we may ask you to update your pull request ## Thank you -Thanks for making Checkmate better. We mean it. Whether it’s your first pull request or your 50th, we’re excited to build with you. +Thanks for making Checkmate better. We mean it. Whether it's your first pull request or your 50th, we're excited to build with you. - -PS: feel free to introduce yourself on [Discord](https://discord.gg/YOUR-DISCORD-LINK) and say hi. +PS: feel free to introduce yourself on [Discord](https://discord.gg/NAb6H3UTjK) and say hi. -- Checkmate team Also make sure you read the [document about how to make a good pull request](/PULLREQUESTS.md). - - From 8bf3875d9a54af7b60793cc6eecd68c52f1a1f7f Mon Sep 17 00:00:00 2001 From: AlexHladin Date: Thu, 2 Oct 2025 14:17:23 +0300 Subject: [PATCH 052/400] chore: add engines section to package.json --- server/package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/package.json b/server/package.json index 1ce33e237..74bd55804 100755 --- a/server/package.json +++ b/server/package.json @@ -17,6 +17,9 @@ "keywords": [], "author": "", "license": "ISC", + "engines": { + "node": ">=20" + }, "dependencies": { "@pulsecron/pulse": "1.6.8", "axios": "^1.7.2", From a1c04ba35eacf1e57e230390c7783b1e6fcc7666 Mon Sep 17 00:00:00 2001 From: a-y-a-n-das Date: Thu, 2 Oct 2025 20:43:00 +0530 Subject: [PATCH 053/400] fixed the format --- server/src/db/v1/modules/monitorModule.js | 1138 ++++++++++----------- 1 file changed, 569 insertions(+), 569 deletions(-) diff --git a/server/src/db/v1/modules/monitorModule.js b/server/src/db/v1/modules/monitorModule.js index 55cf37799..f795a7f2e 100755 --- a/server/src/db/v1/modules/monitorModule.js +++ b/server/src/db/v1/modules/monitorModule.js @@ -1,578 +1,578 @@ import { - buildUptimeDetailsPipeline, - buildHardwareDetailsPipeline, - buildMonitorSummaryByTeamIdPipeline, - buildMonitorsByTeamIdPipeline, - buildMonitorsAndSummaryByTeamIdPipeline, - buildMonitorsWithChecksByTeamIdPipeline, - buildFilteredMonitorsByTeamIdPipeline, + buildUptimeDetailsPipeline, + buildHardwareDetailsPipeline, + buildMonitorSummaryByTeamIdPipeline, + buildMonitorsByTeamIdPipeline, + buildMonitorsAndSummaryByTeamIdPipeline, + buildMonitorsWithChecksByTeamIdPipeline, + buildFilteredMonitorsByTeamIdPipeline, } from "./monitorModuleQueries.js"; const SERVICE_NAME = "monitorModule"; class MonitorModule { - constructor({ Monitor, MonitorStats, Check, stringService, fs, path, fileURLToPath, ObjectId, NormalizeData, NormalizeDataUptimeDetails }) { - this.Monitor = Monitor; - this.MonitorStats = MonitorStats; - this.Check = Check; - this.stringService = stringService; - this.fs = fs; - this.path = path; - this.fileURLToPath = fileURLToPath; - this.ObjectId = ObjectId; - this.NormalizeData = NormalizeData; - this.NormalizeDataUptimeDetails = NormalizeDataUptimeDetails; - - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); - - this.demoMonitorsPath = path.resolve(__dirname, "../../../utils/demoMonitors.json"); - } - - // Helper - calculateUptimeDuration = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - const latestCheck = new Date(checks[0].createdAt); - let latestDownCheck = 0; - - for (let i = checks.length - 1; i >= 0; i--) { - if (checks[i].status === false) { - latestDownCheck = new Date(checks[i].createdAt); - break; - } - } - - // If no down check is found, uptime is from the last check to now - if (latestDownCheck === 0) { - return Date.now() - new Date(checks[checks.length - 1].createdAt); - } - - // Otherwise the uptime is from the last check to the last down check - return latestCheck - latestDownCheck; - }; - - // Helper - getLastChecked = (checks) => { - if (!checks || checks.length === 0) { - return 0; // Handle case when no checks are available - } - // Data is sorted newest->oldest, so last check is the most recent - return new Date() - new Date(checks[0].createdAt); - }; - getLatestResponseTime = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - - return checks[0]?.responseTime ?? 0; - }; - - // Helper - getAverageResponseTime = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - - const validChecks = checks.filter((check) => typeof check.responseTime === "number"); - if (validChecks.length === 0) { - return 0; - } - const aggResponseTime = validChecks.reduce((sum, check) => { - return sum + check.responseTime; - }, 0); - return aggResponseTime / validChecks.length; - }; - - // Helper - getUptimePercentage = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - const upCount = checks.reduce((count, check) => { - return check.status === true ? count + 1 : count; - }, 0); - return (upCount / checks.length) * 100; - }; - - // Helper - getIncidents = (checks) => { - if (!checks || checks.length === 0) { - return 0; // Handle case when no checks are available - } - return checks.reduce((acc, check) => { - return check.status === false ? (acc += 1) : acc; - }, 0); - }; - - // Helper - getDateRange = (dateRange) => { - const startDates = { - recent: new Date(new Date().setHours(new Date().getHours() - 2)), - day: new Date(new Date().setDate(new Date().getDate() - 1)), - week: new Date(new Date().setDate(new Date().getDate() - 7)), - month: new Date(new Date().setMonth(new Date().getMonth() - 1)), - all: new Date(0), - }; - return { - start: startDates[dateRange], - end: new Date(), - }; - }; - - //Helper - getMonitorChecks = async (monitorId, dateRange, sortOrder) => { - const indexSpec = { - monitorId: 1, - updatedAt: sortOrder, // This will be 1 or -1 - }; - - const [checksAll, checksForDateRange] = await Promise.all([ - this.Check.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(), - this.Check.find({ - monitorId, - createdAt: { $gte: dateRange.start, $lte: dateRange.end }, - }) - .hint(indexSpec) - .lean(), - ]); - - return { checksAll, checksForDateRange }; - }; - - // Helper - processChecksForDisplay = (normalizeData, checks, numToDisplay, normalize) => { - let processedChecks = checks; - if (numToDisplay && checks.length > numToDisplay) { - const n = Math.ceil(checks.length / numToDisplay); - processedChecks = checks.filter((_, index) => index % n === 0); - } - return normalize ? normalizeData(processedChecks, 1, 100) : processedChecks; - }; - - // Helper - groupChecksByTime = (checks, dateRange) => { - return checks.reduce((acc, check) => { - // Validate the date - const checkDate = new Date(check.createdAt); - if (Number.isNaN(checkDate.getTime()) || checkDate.getTime() === 0) { - return acc; - } - - const time = dateRange === "day" ? checkDate.setMinutes(0, 0, 0) : checkDate.toISOString().split("T")[0]; - - if (!acc[time]) { - acc[time] = { time, checks: [] }; - } - acc[time].checks.push(check); - return acc; - }, {}); - }; - - // Helper - calculateGroupStats = (group) => { - const totalChecks = group.checks.length; - - const checksWithResponseTime = group.checks.filter((check) => typeof check.responseTime === "number" && !Number.isNaN(check.responseTime)); - - return { - time: group.time, - uptimePercentage: this.getUptimePercentage(group.checks), - totalChecks, - totalIncidents: group.checks.filter((check) => !check.status).length, - avgResponseTime: - checksWithResponseTime.length > 0 - ? checksWithResponseTime.reduce((sum, check) => sum + check.responseTime, 0) / checksWithResponseTime.length - : 0, - }; - }; - - getAllMonitors = async () => { - try { - const monitors = await this.Monitor.find(); - return monitors; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getAllMonitors"; - throw error; - } - }; - - getMonitorById = async (monitorId) => { - try { - const monitor = await this.Monitor.findById(monitorId); - if (monitor === null || monitor === undefined) { - const error = new Error(this.stringService.getDbFindMonitorById(monitorId)); - error.status = 404; - throw error; - } - - return monitor; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorById"; - throw error; - } - }; - - getMonitorsByIds = async (monitorIds) => { - try { - const objectIds = monitorIds.map((id) => new this.ObjectId(id)); - return await this.Monitor.find({ _id: { $in: objectIds } }, { _id: 1, teamId: 1 }).lean(); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorsByIds"; - throw error; - } - }; - getUptimeDetailsById = async ({ monitorId, dateRange }) => { - try { - const dates = this.getDateRange(dateRange); - const formatLookup = { - recent: "%Y-%m-%dT%H:%M:00Z", - day: "%Y-%m-%dT%H:00:00Z", - week: "%Y-%m-%dT00:00:00Z", - month: "%Y-%m-%dT00:00:00Z", - }; - - const dateString = formatLookup[dateRange]; - - const results = await this.Check.aggregate(buildUptimeDetailsPipeline(monitorId, dates, dateString)); - - const monitorData = results[0]; - - monitorData.groupedUpChecks = this.NormalizeDataUptimeDetails(monitorData.groupedUpChecks, 10, 100); - - monitorData.groupedDownChecks = this.NormalizeDataUptimeDetails(monitorData.groupedDownChecks, 10, 100); - - const normalizedGroupChecks = this.NormalizeDataUptimeDetails(monitorData.groupedChecks, 10, 100); - - monitorData.groupedChecks = normalizedGroupChecks; - const monitorStats = await this.MonitorStats.findOne({ monitorId }); - return { monitorData, monitorStats }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getUptimeDetailsById"; - throw error; - } - }; - - getMonitorStatsById = async ({ monitorId, sortOrder, dateRange, numToDisplay, normalize }) => { - try { - // Get monitor, if we can't find it, abort with error - const monitor = await this.Monitor.findById(monitorId); - if (monitor === null || monitor === undefined) { - throw new Error(this.stringService.getDbFindMonitorById(monitorId)); - } - - // Get query params - const sort = sortOrder === "asc" ? 1 : -1; - - // Get Checks for monitor in date range requested - const dates = this.getDateRange(dateRange); - const { checksAll, checksForDateRange } = await this.getMonitorChecks(monitorId, dates, sort); - - // Build monitor stats - const monitorStats = { - ...monitor.toObject(), - uptimeDuration: this.calculateUptimeDuration(checksAll), - lastChecked: this.getLastChecked(checksAll), - latestResponseTime: this.getLatestResponseTime(checksAll), - periodIncidents: this.getIncidents(checksForDateRange), - periodTotalChecks: checksForDateRange.length, - checks: this.processChecksForDisplay(this.NormalizeData, checksForDateRange, numToDisplay, normalize), - }; - - if (monitor.type === "http" || monitor.type === "ping" || monitor.type === "docker" || monitor.type === "port" || monitor.type === "game") { - // HTTP/PING Specific stats - monitorStats.periodAvgResponseTime = this.getAverageResponseTime(checksForDateRange); - monitorStats.periodUptime = this.getUptimePercentage(checksForDateRange); - const groupedChecks = this.groupChecksByTime(checksForDateRange, dateRange); - monitorStats.aggregateData = Object.values(groupedChecks).map(this.calculateGroupStats); - } - - return monitorStats; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorStatsById"; - throw error; - } - }; - - getHardwareDetailsById = async ({ monitorId, dateRange }) => { - try { - const monitor = await this.Monitor.findById(monitorId); - const dates = this.getDateRange(dateRange); - - const formatLookup = { - recent: "%Y-%m-%dT%H:%M:00Z", - day: "%Y-%m-%dT%H:00:00Z", - week: "%Y-%m-%dT00:00:00Z", - month: "%Y-%m-%dT00:00:00Z", - }; - const dateString = formatLookup[dateRange]; - - const hardwareStats = await this.Check.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString)); - - const stats = hardwareStats[0]; - - return { - ...monitor.toObject(), - stats, - }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getHardwareDetailsById"; - throw error; - } - }; - - getMonitorsByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId }) => { - limit = parseInt(limit); - page = parseInt(page); - rowsPerPage = parseInt(rowsPerPage); - if (field === undefined) { - field = "name"; - order = "asc"; - } - // Build match stage - const matchStage = { teamId: new this.ObjectId(teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - - const summaryResult = await this.Monitor.aggregate(buildMonitorSummaryByTeamIdPipeline({ matchStage })); - const summary = summaryResult[0]; - - const monitors = await this.Monitor.aggregate(buildMonitorsByTeamIdPipeline({ matchStage, field, order })); - - const filteredMonitors = await this.Monitor.aggregate( - buildFilteredMonitorsByTeamIdPipeline({ - matchStage, - filter, - page, - rowsPerPage, - field, - order, - limit, - type, - }) - ); - - const normalizedFilteredMonitors = filteredMonitors.map((monitor) => { - if (!monitor.checks) { - return monitor; - } - monitor.checks = this.NormalizeData(monitor.checks, 10, 100); - return monitor; - }); - - return { summary, monitors, filteredMonitors: normalizedFilteredMonitors }; - }; - - getMonitorsAndSummaryByTeamId = async ({ type, explain, teamId }) => { - try { - const matchStage = { teamId: new this.ObjectId(teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - - if (explain === true) { - return this.Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })).explain("executionStats"); - } - - const queryResult = await this.Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })); - const { monitors, summary } = queryResult?.[0] ?? {}; - return { monitors, summary }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorsAndSummaryByTeamId"; - throw error; - } - }; - - getMonitorsWithChecksByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId, explain }) => { - try { - limit = parseInt(limit); - page = parseInt(page); - rowsPerPage = parseInt(rowsPerPage); - if (field === undefined) { - field = "name"; - order = "asc"; - } - // Build match stage - const matchStage = { teamId: new this.ObjectId(teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - - if (explain === true) { - return this.Monitor.aggregate( - buildMonitorsWithChecksByTeamIdPipeline({ - matchStage, - filter, - page, - rowsPerPage, - field, - order, - limit, - type, - }) - ).explain("executionStats"); - } - - const queryResult = await this.Monitor.aggregate( - buildMonitorsWithChecksByTeamIdPipeline({ - matchStage, - filter, - page, - rowsPerPage, - field, - order, - limit, - type, - }) - ); - const monitors = queryResult[0]?.monitors; - const count = queryResult[0]?.count; - const normalizedFilteredMonitors = monitors.map((monitor) => { - if (!monitor.checks) { - return monitor; - } - monitor.checks = this.NormalizeData(monitor.checks, 10, 100); - return monitor; - }); - return { count, monitors: normalizedFilteredMonitors }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorsWithChecksByTeamId"; - throw error; - } - }; - createMonitor = async ({ body, teamId, userId }) => { - try { - const monitor = new this.Monitor({ ...body, teamId, userId }); - const saved = await monitor.save(); - return saved; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createMonitor"; - throw error; - } - }; - - createBulkMonitors = async (req) => { - try { - const monitors = req.map((item) => new this.Monitor({ ...item, notifications: undefined })); - await this.Monitor.bulkSave(monitors); - return monitors; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createBulkMonitors"; - throw error; - } - }; - - deleteMonitor = async ({ teamId, monitorId }) => { - try { - const deletedMonitor = await this.Monitor.findOneAndDelete({ _id: monitorId, teamId }); - - if (!deletedMonitor) { - throw new Error(this.stringService.getDbFindMonitorById(monitorId)); - } - - return deletedMonitor; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteMonitor"; - throw error; - } - }; - - deleteAllMonitors = async (teamId) => { - try { - const monitors = await this.Monitor.find({ teamId }); - const { deletedCount } = await this.Monitor.deleteMany({ teamId }); - - return { monitors, deletedCount }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteAllMonitors"; - throw error; - } - }; - - deleteMonitorsByUserId = async (userId) => { - try { - const result = await this.Monitor.deleteMany({ userId: userId }); - return result; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteMonitorsByUserId"; - throw error; - } - }; - - editMonitor = async ({ monitorId, body }) => { - try { - const editedMonitor = await this.Monitor.findByIdAndUpdate(monitorId, body, { - new: true, - }); - return editedMonitor; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "editMonitor"; - throw error; - } - }; - - addDemoMonitors = async (userId, teamId) => { - try { - const demoMonitors = JSON.parse(this.fs.readFileSync(this.demoMonitorsPath, "utf8")); - - const demoMonitorsToInsert = demoMonitors.map((monitor) => { - return { - userId, - teamId, - name: monitor.name, - description: monitor.name, - type: "http", - url: monitor.url, - interval: 60000, - }; - }); - const insertedMonitors = await this.Monitor.insertMany(demoMonitorsToInsert); - return insertedMonitors; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "addDemoMonitors"; - throw error; - } - }; - - pauseMonitor = async ({ monitorId }) => { - try { - const monitor = await this.Monitor.findOneAndUpdate( - { _id: monitorId }, - [ - { - $set: { - isActive: { $not: "$isActive" }, - status: "$$REMOVE", - }, - }, - ], - { new: true } - ); - - return monitor; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "pauseMonitor"; - throw error; - } - }; + constructor({ Monitor, MonitorStats, Check, stringService, fs, path, fileURLToPath, ObjectId, NormalizeData, NormalizeDataUptimeDetails }) { + this.Monitor = Monitor; + this.MonitorStats = MonitorStats; + this.Check = Check; + this.stringService = stringService; + this.fs = fs; + this.path = path; + this.fileURLToPath = fileURLToPath; + this.ObjectId = ObjectId; + this.NormalizeData = NormalizeData; + this.NormalizeDataUptimeDetails = NormalizeDataUptimeDetails; + + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + + this.demoMonitorsPath = path.resolve(__dirname, "../../../utils/demoMonitors.json"); + } + + // Helper + calculateUptimeDuration = (checks) => { + if (!checks || checks.length === 0) { + return 0; + } + const latestCheck = new Date(checks[0].createdAt); + let latestDownCheck = 0; + + for (let i = checks.length - 1; i >= 0; i--) { + if (checks[i].status === false) { + latestDownCheck = new Date(checks[i].createdAt); + break; + } + } + + // If no down check is found, uptime is from the last check to now + if (latestDownCheck === 0) { + return Date.now() - new Date(checks[checks.length - 1].createdAt); + } + + // Otherwise the uptime is from the last check to the last down check + return latestCheck - latestDownCheck; + }; + + // Helper + getLastChecked = (checks) => { + if (!checks || checks.length === 0) { + return 0; // Handle case when no checks are available + } + // Data is sorted newest->oldest, so last check is the most recent + return new Date() - new Date(checks[0].createdAt); + }; + getLatestResponseTime = (checks) => { + if (!checks || checks.length === 0) { + return 0; + } + + return checks[0]?.responseTime ?? 0; + }; + + // Helper + getAverageResponseTime = (checks) => { + if (!checks || checks.length === 0) { + return 0; + } + + const validChecks = checks.filter((check) => typeof check.responseTime === "number"); + if (validChecks.length === 0) { + return 0; + } + const aggResponseTime = validChecks.reduce((sum, check) => { + return sum + check.responseTime; + }, 0); + return aggResponseTime / validChecks.length; + }; + + // Helper + getUptimePercentage = (checks) => { + if (!checks || checks.length === 0) { + return 0; + } + const upCount = checks.reduce((count, check) => { + return check.status === true ? count + 1 : count; + }, 0); + return (upCount / checks.length) * 100; + }; + + // Helper + getIncidents = (checks) => { + if (!checks || checks.length === 0) { + return 0; // Handle case when no checks are available + } + return checks.reduce((acc, check) => { + return check.status === false ? (acc += 1) : acc; + }, 0); + }; + + // Helper + getDateRange = (dateRange) => { + const startDates = { + recent: new Date(new Date().setHours(new Date().getHours() - 2)), + day: new Date(new Date().setDate(new Date().getDate() - 1)), + week: new Date(new Date().setDate(new Date().getDate() - 7)), + month: new Date(new Date().setMonth(new Date().getMonth() - 1)), + all: new Date(0), + }; + return { + start: startDates[dateRange], + end: new Date(), + }; + }; + + //Helper + getMonitorChecks = async (monitorId, dateRange, sortOrder) => { + const indexSpec = { + monitorId: 1, + updatedAt: sortOrder, // This will be 1 or -1 + }; + + const [checksAll, checksForDateRange] = await Promise.all([ + this.Check.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(), + this.Check.find({ + monitorId, + createdAt: { $gte: dateRange.start, $lte: dateRange.end }, + }) + .hint(indexSpec) + .lean(), + ]); + + return { checksAll, checksForDateRange }; + }; + + // Helper + processChecksForDisplay = (normalizeData, checks, numToDisplay, normalize) => { + let processedChecks = checks; + if (numToDisplay && checks.length > numToDisplay) { + const n = Math.ceil(checks.length / numToDisplay); + processedChecks = checks.filter((_, index) => index % n === 0); + } + return normalize ? normalizeData(processedChecks, 1, 100) : processedChecks; + }; + + // Helper + groupChecksByTime = (checks, dateRange) => { + return checks.reduce((acc, check) => { + // Validate the date + const checkDate = new Date(check.createdAt); + if (Number.isNaN(checkDate.getTime()) || checkDate.getTime() === 0) { + return acc; + } + + const time = dateRange === "day" ? checkDate.setMinutes(0, 0, 0) : checkDate.toISOString().split("T")[0]; + + if (!acc[time]) { + acc[time] = { time, checks: [] }; + } + acc[time].checks.push(check); + return acc; + }, {}); + }; + + // Helper + calculateGroupStats = (group) => { + const totalChecks = group.checks.length; + + const checksWithResponseTime = group.checks.filter((check) => typeof check.responseTime === "number" && !Number.isNaN(check.responseTime)); + + return { + time: group.time, + uptimePercentage: this.getUptimePercentage(group.checks), + totalChecks, + totalIncidents: group.checks.filter((check) => !check.status).length, + avgResponseTime: + checksWithResponseTime.length > 0 + ? checksWithResponseTime.reduce((sum, check) => sum + check.responseTime, 0) / checksWithResponseTime.length + : 0, + }; + }; + + getAllMonitors = async () => { + try { + const monitors = await this.Monitor.find(); + return monitors; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getAllMonitors"; + throw error; + } + }; + + getMonitorById = async (monitorId) => { + try { + const monitor = await this.Monitor.findById(monitorId); + if (monitor === null || monitor === undefined) { + const error = new Error(this.stringService.getDbFindMonitorById(monitorId)); + error.status = 404; + throw error; + } + + return monitor; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getMonitorById"; + throw error; + } + }; + + getMonitorsByIds = async (monitorIds) => { + try { + const objectIds = monitorIds.map((id) => new this.ObjectId(id)); + return await this.Monitor.find({ _id: { $in: objectIds } }, { _id: 1, teamId: 1 }).lean(); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getMonitorsByIds"; + throw error; + } + }; + getUptimeDetailsById = async ({ monitorId, dateRange }) => { + try { + const dates = this.getDateRange(dateRange); + const formatLookup = { + recent: "%Y-%m-%dT%H:%M:00Z", + day: "%Y-%m-%dT%H:00:00Z", + week: "%Y-%m-%dT00:00:00Z", + month: "%Y-%m-%dT00:00:00Z", + }; + + const dateString = formatLookup[dateRange]; + + const results = await this.Check.aggregate(buildUptimeDetailsPipeline(monitorId, dates, dateString)); + + const monitorData = results[0]; + + monitorData.groupedUpChecks = this.NormalizeDataUptimeDetails(monitorData.groupedUpChecks, 10, 100); + + monitorData.groupedDownChecks = this.NormalizeDataUptimeDetails(monitorData.groupedDownChecks, 10, 100); + + const normalizedGroupChecks = this.NormalizeDataUptimeDetails(monitorData.groupedChecks, 10, 100); + + monitorData.groupedChecks = normalizedGroupChecks; + const monitorStats = await this.MonitorStats.findOne({ monitorId }); + return { monitorData, monitorStats }; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getUptimeDetailsById"; + throw error; + } + }; + + getMonitorStatsById = async ({ monitorId, sortOrder, dateRange, numToDisplay, normalize }) => { + try { + // Get monitor, if we can't find it, abort with error + const monitor = await this.Monitor.findById(monitorId); + if (monitor === null || monitor === undefined) { + throw new Error(this.stringService.getDbFindMonitorById(monitorId)); + } + + // Get query params + const sort = sortOrder === "asc" ? 1 : -1; + + // Get Checks for monitor in date range requested + const dates = this.getDateRange(dateRange); + const { checksAll, checksForDateRange } = await this.getMonitorChecks(monitorId, dates, sort); + + // Build monitor stats + const monitorStats = { + ...monitor.toObject(), + uptimeDuration: this.calculateUptimeDuration(checksAll), + lastChecked: this.getLastChecked(checksAll), + latestResponseTime: this.getLatestResponseTime(checksAll), + periodIncidents: this.getIncidents(checksForDateRange), + periodTotalChecks: checksForDateRange.length, + checks: this.processChecksForDisplay(this.NormalizeData, checksForDateRange, numToDisplay, normalize), + }; + + if (monitor.type === "http" || monitor.type === "ping" || monitor.type === "docker" || monitor.type === "port" || monitor.type === "game") { + // HTTP/PING Specific stats + monitorStats.periodAvgResponseTime = this.getAverageResponseTime(checksForDateRange); + monitorStats.periodUptime = this.getUptimePercentage(checksForDateRange); + const groupedChecks = this.groupChecksByTime(checksForDateRange, dateRange); + monitorStats.aggregateData = Object.values(groupedChecks).map(this.calculateGroupStats); + } + + return monitorStats; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getMonitorStatsById"; + throw error; + } + }; + + getHardwareDetailsById = async ({ monitorId, dateRange }) => { + try { + const monitor = await this.Monitor.findById(monitorId); + const dates = this.getDateRange(dateRange); + + const formatLookup = { + recent: "%Y-%m-%dT%H:%M:00Z", + day: "%Y-%m-%dT%H:00:00Z", + week: "%Y-%m-%dT00:00:00Z", + month: "%Y-%m-%dT00:00:00Z", + }; + const dateString = formatLookup[dateRange]; + + const hardwareStats = await this.Check.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString)); + + const stats = hardwareStats[0]; + + return { + ...monitor.toObject(), + stats, + }; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getHardwareDetailsById"; + throw error; + } + }; + + getMonitorsByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId }) => { + limit = parseInt(limit); + page = parseInt(page); + rowsPerPage = parseInt(rowsPerPage); + if (field === undefined) { + field = "name"; + order = "asc"; + } + // Build match stage + const matchStage = { teamId: new this.ObjectId(teamId) }; + if (type !== undefined) { + matchStage.type = Array.isArray(type) ? { $in: type } : type; + } + + const summaryResult = await this.Monitor.aggregate(buildMonitorSummaryByTeamIdPipeline({ matchStage })); + const summary = summaryResult[0]; + + const monitors = await this.Monitor.aggregate(buildMonitorsByTeamIdPipeline({ matchStage, field, order })); + + const filteredMonitors = await this.Monitor.aggregate( + buildFilteredMonitorsByTeamIdPipeline({ + matchStage, + filter, + page, + rowsPerPage, + field, + order, + limit, + type, + }) + ); + + const normalizedFilteredMonitors = filteredMonitors.map((monitor) => { + if (!monitor.checks) { + return monitor; + } + monitor.checks = this.NormalizeData(monitor.checks, 10, 100); + return monitor; + }); + + return { summary, monitors, filteredMonitors: normalizedFilteredMonitors }; + }; + + getMonitorsAndSummaryByTeamId = async ({ type, explain, teamId }) => { + try { + const matchStage = { teamId: new this.ObjectId(teamId) }; + if (type !== undefined) { + matchStage.type = Array.isArray(type) ? { $in: type } : type; + } + + if (explain === true) { + return this.Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })).explain("executionStats"); + } + + const queryResult = await this.Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })); + const { monitors, summary } = queryResult?.[0] ?? {}; + return { monitors, summary }; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getMonitorsAndSummaryByTeamId"; + throw error; + } + }; + + getMonitorsWithChecksByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId, explain }) => { + try { + limit = parseInt(limit); + page = parseInt(page); + rowsPerPage = parseInt(rowsPerPage); + if (field === undefined) { + field = "name"; + order = "asc"; + } + // Build match stage + const matchStage = { teamId: new this.ObjectId(teamId) }; + if (type !== undefined) { + matchStage.type = Array.isArray(type) ? { $in: type } : type; + } + + if (explain === true) { + return this.Monitor.aggregate( + buildMonitorsWithChecksByTeamIdPipeline({ + matchStage, + filter, + page, + rowsPerPage, + field, + order, + limit, + type, + }) + ).explain("executionStats"); + } + + const queryResult = await this.Monitor.aggregate( + buildMonitorsWithChecksByTeamIdPipeline({ + matchStage, + filter, + page, + rowsPerPage, + field, + order, + limit, + type, + }) + ); + const monitors = queryResult[0]?.monitors; + const count = queryResult[0]?.count; + const normalizedFilteredMonitors = monitors.map((monitor) => { + if (!monitor.checks) { + return monitor; + } + monitor.checks = this.NormalizeData(monitor.checks, 10, 100); + return monitor; + }); + return { count, monitors: normalizedFilteredMonitors }; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getMonitorsWithChecksByTeamId"; + throw error; + } + }; + createMonitor = async ({ body, teamId, userId }) => { + try { + const monitor = new this.Monitor({ ...body, teamId, userId }); + const saved = await monitor.save(); + return saved; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "createMonitor"; + throw error; + } + }; + + createBulkMonitors = async (req) => { + try { + const monitors = req.map((item) => new this.Monitor({ ...item, notifications: undefined })); + await this.Monitor.bulkSave(monitors); + return monitors; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "createBulkMonitors"; + throw error; + } + }; + + deleteMonitor = async ({ teamId, monitorId }) => { + try { + const deletedMonitor = await this.Monitor.findOneAndDelete({ _id: monitorId, teamId }); + + if (!deletedMonitor) { + throw new Error(this.stringService.getDbFindMonitorById(monitorId)); + } + + return deletedMonitor; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteMonitor"; + throw error; + } + }; + + deleteAllMonitors = async (teamId) => { + try { + const monitors = await this.Monitor.find({ teamId }); + const { deletedCount } = await this.Monitor.deleteMany({ teamId }); + + return { monitors, deletedCount }; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteAllMonitors"; + throw error; + } + }; + + deleteMonitorsByUserId = async (userId) => { + try { + const result = await this.Monitor.deleteMany({ userId: userId }); + return result; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteMonitorsByUserId"; + throw error; + } + }; + + editMonitor = async ({ monitorId, body }) => { + try { + const editedMonitor = await this.Monitor.findByIdAndUpdate(monitorId, body, { + new: true, + }); + return editedMonitor; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "editMonitor"; + throw error; + } + }; + + addDemoMonitors = async (userId, teamId) => { + try { + const demoMonitors = JSON.parse(this.fs.readFileSync(this.demoMonitorsPath, "utf8")); + + const demoMonitorsToInsert = demoMonitors.map((monitor) => { + return { + userId, + teamId, + name: monitor.name, + description: monitor.name, + type: "http", + url: monitor.url, + interval: 60000, + }; + }); + const insertedMonitors = await this.Monitor.insertMany(demoMonitorsToInsert); + return insertedMonitors; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "addDemoMonitors"; + throw error; + } + }; + + pauseMonitor = async ({ monitorId }) => { + try { + const monitor = await this.Monitor.findOneAndUpdate( + { _id: monitorId }, + [ + { + $set: { + isActive: { $not: "$isActive" }, + status: "$$REMOVE", + }, + }, + ], + { new: true } + ); + + return monitor; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "pauseMonitor"; + throw error; + } + }; } export default MonitorModule; From 4317d1a47e352d99d5d224416a54ba9f22a41049 Mon Sep 17 00:00:00 2001 From: karenvicent Date: Thu, 2 Oct 2025 15:19:42 -0400 Subject: [PATCH 054/400] chace password by superadmin --- .../components/ChangePasswordModal/index.jsx | 25 +++++++---- .../src/Pages/v1/Account/EditUser/index.jsx | 25 ++++++----- client/src/Utils/NetworkService.js | 1 - server/src/controllers/v1/authController.js | 45 +++++++++---------- server/src/service/v1/business/userService.js | 3 +- server/src/validation/joi.js | 2 + 6 files changed, 54 insertions(+), 47 deletions(-) diff --git a/client/src/Pages/Account/components/ChangePasswordModal/index.jsx b/client/src/Pages/Account/components/ChangePasswordModal/index.jsx index 375595b29..17984c3ae 100644 --- a/client/src/Pages/Account/components/ChangePasswordModal/index.jsx +++ b/client/src/Pages/Account/components/ChangePasswordModal/index.jsx @@ -1,20 +1,20 @@ import { useState } from "react"; import { Button, Stack } from "@mui/material"; -import { GenericDialog } from "../../../../Components/Dialog/genericDialog"; -import TextInput from "../../../../Components/Inputs/TextInput"; -import PasswordTooltip from "../../../Auth/components/PasswordTooltip"; +import { GenericDialog } from "@/Components/v1/Dialog/genericDialog"; +import TextInput from "@/Components/v1/Inputs/TextInput"; +import PasswordTooltip from "@/Pages/v1/Auth/components/PasswordTooltip"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; import { createToast } from "../../../../Utils/toastUtils"; -import { PasswordEndAdornment } from "../../../../Components/Inputs/TextInput/Adornments"; -import usePasswordFeedback from "../../../Auth/hooks/usePasswordFeedback"; +import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments"; +import usePasswordFeedback from "@/Pages/v1/Auth/hooks/usePasswordFeedback"; +import PropTypes from "prop-types"; -const ChangePasswordModal = ({ isSaving, isLoading, userId, changePassword, email }) => { +const ChangePasswordModal = ({ isSaving, isLoading, changePassword, email }) => { const INITIAL_FORM_STATE = { password: "", confirm: "", }; - console.log("email", email); const theme = useTheme(); const { t } = useTranslation(); const { feedback, handlePasswordFeedback } = usePasswordFeedback(); @@ -30,7 +30,6 @@ const ChangePasswordModal = ({ isSaving, isLoading, userId, changePassword, emai const onChange = (e) => { let { name, value } = e.target; const updatedForm = { ...form, [name]: value }; - //validateFields(name, value, updatedForm); setForm(updatedForm); handlePasswordFeedback(updatedForm, name, value, form, errors, setErrors); @@ -130,11 +129,19 @@ const ChangePasswordModal = ({ isSaving, isLoading, userId, changePassword, emai onClick={onsubmitChangePassword} disabled={isLoadingSubmit || !isFormValid} > - Save + {t("save")} ); }; + +ChangePasswordModal.propTypes = { + isSaving: PropTypes.bool.isRequired, + isLoading: PropTypes.bool.isRequired, + changePassword: PropTypes.func.isRequired, + email: PropTypes.string.isRequired, +}; + export default ChangePasswordModal; diff --git a/client/src/Pages/v1/Account/EditUser/index.jsx b/client/src/Pages/v1/Account/EditUser/index.jsx index 9fe307cf2..e11716683 100644 --- a/client/src/Pages/v1/Account/EditUser/index.jsx +++ b/client/src/Pages/v1/Account/EditUser/index.jsx @@ -7,8 +7,7 @@ import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; import Search from "@/Components/v1/Inputs/Search/index.jsx"; import Button from "@mui/material/Button"; import RoleTable from "../components/RoleTable/index.jsx"; -import ChangePasswordModal from "../components/ChangePasswordModal"; - +import ChangePasswordModal from "@/Pages/Account/components/ChangePasswordModal/index.jsx"; // Utils import { useParams } from "react-router-dom"; import { useTheme } from "@emotion/react"; @@ -16,9 +15,12 @@ import { useTranslation } from "react-i18next"; import { useGetUser, useEditUser } from "../../../../Hooks/v1/userHooks.js"; import { EDITABLE_ROLES, ROLES } from "../../../../Utils/roleUtils.js"; import { useEditUserForm, useValidateEditUserForm } from "./hooks/editUser.js"; +import { useSelector } from "react-redux"; const EditUser = () => { + const { user } = useSelector((state) => state.auth); const { userId } = useParams(); + const isSameUser = user?._id === userId; const theme = useTheme(); const { t } = useTranslation(); const BREADCRUMBS = [ @@ -26,7 +28,7 @@ const EditUser = () => { { name: t("editUserPage.title"), path: "" }, ]; - const [user, isLoading, error] = useGetUser(userId); + const [userToEdit, isLoading, error] = useGetUser(userId); const [editUser, isSaving, saveError, changePassword] = useEditUser(userId); const [ form, @@ -35,7 +37,7 @@ const EditUser = () => { handleDeleteRole, searchInput, handleSearchInput, - ] = useEditUserForm(user); + ] = useEditUserForm(userToEdit); const [errors, validateForm, validateField] = useValidateEditUserForm(); const onChange = (e) => { @@ -119,13 +121,14 @@ const EditUser = () => { > {t("editUserPage.form.save")} - + {!isSameUser && ( + + )} diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js index 7401994bb..6372be646 100644 --- a/client/src/Utils/NetworkService.js +++ b/client/src/Utils/NetworkService.js @@ -1139,7 +1139,6 @@ class NetworkService { async changePasswordByAdmin(config) { const { userId, passwordForm } = config; - console.log("Password form data:", passwordForm); return this.axiosInstance.put(`auth/users/${userId}/password`, passwordForm, { headers: { "Content-Type": "application/json", diff --git a/server/src/controllers/v1/authController.js b/server/src/controllers/v1/authController.js index b84aaacd6..600c6730f 100755 --- a/server/src/controllers/v1/authController.js +++ b/server/src/controllers/v1/authController.js @@ -10,10 +10,12 @@ import { editUserByIdParamValidation, editUserByIdBodyValidation, editSuperadminUserByIdBodyValidation, + editUserPasswordByIdBodyValidation, } from "../../validation/joi.js"; +import ErrorService from "../../service/v1/infrastructure/errorService.js"; const SERVICE_NAME = "authController"; - +const errorService = new ErrorService(); /** * Authentication Controller * @@ -436,7 +438,7 @@ class AuthController extends BaseController { async (req, res) => { const roles = req?.user?.role; if (!roles.includes("superadmin")) { - throw createError("Unauthorized", 403); + throw errorService.createError("Unauthorized", 403); } const userId = req.params.userId; @@ -456,29 +458,24 @@ class AuthController extends BaseController { SERVICE_NAME, "editUserById" ); - editUserPasswordById = this.asyncHandler( - async (req, res) => { - console.log("editUserPasswordById called"); - const roles = req?.user?.role; - if (!roles.includes("superadmin")) { - console.log("entro a error editUserPasswordById"); - throw createError("Unauthorized", 403); - } - - const userId = req.params.userId; - console.log("userId param:", userId); - const updates = { ...req.body }; - console.log("newPassword body:", req.body); - await editUserByIdParamValidation.validateAsync(req.params); - console.log("params validated"); - //await editUserPasswordByIdBodyValidation.validateAsync(req.body); - console.log("body validated"); - await this.userService.setPasswordByUserId( userId, updates ); - return res.success({ msg: "Password reset successfully" }); - }, - SERVICE_NAME, "editUserPasswordById" - ) + editUserPasswordById = this.asyncHandler( + async (req, res) => { + const roles = req?.user?.role; + if (!roles.includes("superadmin")) { + throw errorService.createError("Unauthorized", 403); + } + const userId = req.params.userId; + const updates = { ...req.body }; + console.log("body", req.body); + await editUserByIdParamValidation.validateAsync(req.params); + await editUserPasswordByIdBodyValidation.validateAsync(req.body); + await this.userService.setPasswordByUserId(userId, updates); + return res.success({ msg: "Password reset successfully" }); + }, + SERVICE_NAME, + "editUserPasswordById" + ); } export default AuthController; diff --git a/server/src/service/v1/business/userService.js b/server/src/service/v1/business/userService.js index 6c036a5c8..c81d9135e 100644 --- a/server/src/service/v1/business/userService.js +++ b/server/src/service/v1/business/userService.js @@ -214,7 +214,6 @@ class UserService { setPasswordByUserId = async (userId, updates) => { const updatedUser = await this.db.userModule.updateUser({ userId: userId, user: updates, file: null }); return updatedUser; - } - + }; } export default UserService; diff --git a/server/src/validation/joi.js b/server/src/validation/joi.js index 209135769..4005e2ebe 100755 --- a/server/src/validation/joi.js +++ b/server/src/validation/joi.js @@ -671,7 +671,9 @@ const editSuperadminUserByIdBodyValidation = joi.object({ }); const editUserPasswordByIdBodyValidation = joi.object({ + email: joi.string().required(), newPassword: joi.string().min(8).required().pattern(passwordPattern), + password: joi.string().min(8).required().pattern(passwordPattern), }); export { From 5159d43cad589f1b717c9625c9bb0bddd47dbfd7 Mon Sep 17 00:00:00 2001 From: karenvicent Date: Thu, 2 Oct 2025 15:40:21 -0400 Subject: [PATCH 055/400] fix save button validation --- .../Pages/Account/components/ChangePasswordModal/index.jsx | 7 ++++++- server/src/controllers/v1/authController.js | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/client/src/Pages/Account/components/ChangePasswordModal/index.jsx b/client/src/Pages/Account/components/ChangePasswordModal/index.jsx index 17984c3ae..b3f81eebb 100644 --- a/client/src/Pages/Account/components/ChangePasswordModal/index.jsx +++ b/client/src/Pages/Account/components/ChangePasswordModal/index.jsx @@ -34,7 +34,11 @@ const ChangePasswordModal = ({ isSaving, isLoading, changePassword, email }) => handlePasswordFeedback(updatedForm, name, value, form, errors, setErrors); }; - const isFormValid = !errors.password && !errors.confirm; + const isFormValid = + form.password.length > 1 && + form.confirm.length > 1 && + !errors.password && + !errors.confirm; const onsubmitChangePassword = async (event) => { event.preventDefault(); if (!isFormValid) return; @@ -57,6 +61,7 @@ const ChangePasswordModal = ({ isSaving, isLoading, changePassword, email }) => setIsLoadingSubmit(false); } }; + return ( <> + ( + + )} + /> + ( + + )} + /> + + + + - + ); }; diff --git a/client/src/Pages/v2/Auth/Register.tsx b/client/src/Pages/v2/Auth/Register.tsx index 2a02c401f..d4565c66c 100644 --- a/client/src/Pages/v2/Auth/Register.tsx +++ b/client/src/Pages/v2/Auth/Register.tsx @@ -1,14 +1,17 @@ +import { AuthBasePage } from "@/Components/v2/Auth"; +import { TextInput } from "@/Components/v2/Inputs"; +import { Button } from "@/Components/v2/Inputs"; +import Typography from "@mui/material/Typography"; +import Stack from "@mui/material/Stack"; + +import type { ApiResponse } from "@/Hooks/v2/UseApi"; import { zodResolver } from "@hookform/resolvers/zod"; import { z } from "zod"; import { useTranslation } from "react-i18next"; import { useForm, Controller } from "react-hook-form"; import { useTheme } from "@mui/material/styles"; -import Typography from "@mui/material/Typography"; - -import { TextInput } from "@/Components/v2/Inputs/TextInput"; -import Button from "@mui/material/Button"; -import Stack from "@mui/material/Stack"; import { usePost } from "@/Hooks/v2/UseApi"; +import { useNavigate } from "react-router"; const schema = z .object({ @@ -30,14 +33,15 @@ type FormData = z.infer; const Register = () => { const { t } = useTranslation(); const theme = useTheme(); - const { post, loading, error } = usePost("/auth/register"); + const navigate = useNavigate(); + const { post, loading, error } = usePost(); const { handleSubmit, control, formState: { errors }, } = useForm({ - resolver: zodResolver(schema), // ⬅️ connect Zod + resolver: zodResolver(schema), defaultValues: { email: "", password: "", @@ -45,124 +49,128 @@ const Register = () => { }); const onSubmit = async (data: FormData) => { - const result = await post(data); + const result = await post("/auth/register", data); if (result) { - console.log(result); + navigate("/v2/uptime"); } else { console.error("Login failed:", error); } }; return ( - - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - ( - - )} - /> - - {error && {error}} + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + ( + + )} + /> + + {error && {error}} + - + ); }; diff --git a/client/src/Pages/v2/Uptime/Create.tsx b/client/src/Pages/v2/Uptime/Create.tsx index a135ec84c..38b1511f4 100644 --- a/client/src/Pages/v2/Uptime/Create.tsx +++ b/client/src/Pages/v2/Uptime/Create.tsx @@ -44,7 +44,7 @@ const UptimeCreatePage = () => { mode: "onChange", }); const { response } = useGet("/notification-channels"); - const { post, loading, error } = usePost("/monitors"); + const { post, loading, error } = usePost(); const selectedType = useWatch({ control, name: "type", @@ -58,7 +58,7 @@ const UptimeCreatePage = () => { let interval = humanInterval(data.interval); if (!interval) interval = 60000; const submitData = { ...data, interval }; - const result = await post(submitData); + const result = await post("/monitors", submitData); if (result) { console.log(result); } else { From fb90f193b1509dcbcee4bee01d04794fc2178b94 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 9 Oct 2025 12:44:27 -0700 Subject: [PATCH 086/400] add fallbacks --- .../Components/v2/DesignElements/BasePage.tsx | 58 ++++++- .../v2/DesignElements/BulletPointCheck.tsx | 45 +++++ .../Components/v2/DesignElements/Fallback.tsx | 158 ++++++++++++++++++ .../Components/v2/DesignElements/index.tsx | 4 +- client/src/Pages/v2/Uptime/UptimeMonitors.tsx | 22 +-- 5 files changed, 275 insertions(+), 12 deletions(-) create mode 100644 client/src/Components/v2/DesignElements/BulletPointCheck.tsx create mode 100644 client/src/Components/v2/DesignElements/Fallback.tsx diff --git a/client/src/Components/v2/DesignElements/BasePage.tsx b/client/src/Components/v2/DesignElements/BasePage.tsx index ad516f2c6..f458cbb0a 100644 --- a/client/src/Components/v2/DesignElements/BasePage.tsx +++ b/client/src/Components/v2/DesignElements/BasePage.tsx @@ -1,7 +1,9 @@ import Stack from "@mui/material/Stack"; +import { ErrorFallback, EmptyFallback } from "./Fallback"; + import type { StackProps } from "@mui/material/Stack"; import { useTheme } from "@mui/material/styles"; - +import { useTranslation } from "react-i18next"; interface BasePageProps extends StackProps { children: React.ReactNode; } @@ -22,3 +24,57 @@ export const BasePage: React.FC = ({ ); }; + +interface BasePageWithStatesProps extends StackProps { + loading: boolean; + error: any; + items: any[]; + page: string; + actionLink?: string; + children: React.ReactNode; +} + +const isEmpty = (items: any[]) => { + if (!items) return true; + if (Array.isArray(items) && items.length === 0) return true; + return false; +}; + +export const BasePageWithStates: React.FC = ({ + loading, + error, + items, + page, + actionLink, + children, + ...props +}: BasePageWithStatesProps) => { + const { t } = useTranslation(); + + if (loading) { + return null; + } + + if (error) { + return ( + + ); + } + + if (isEmpty(items)) { + return ( + + ); + } + + return {children}; +}; diff --git a/client/src/Components/v2/DesignElements/BulletPointCheck.tsx b/client/src/Components/v2/DesignElements/BulletPointCheck.tsx new file mode 100644 index 000000000..03147e011 --- /dev/null +++ b/client/src/Components/v2/DesignElements/BulletPointCheck.tsx @@ -0,0 +1,45 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import CheckOutlined from "@/assets/icons/check-outlined.svg?react"; +import { useTheme } from "@mui/material/styles"; + +export const BulletPointCheck = ({ + text, + variant = "info", +}: { + text: string; + noHighlightText?: string; + variant?: "success" | "error" | "info"; +}) => { + const theme = useTheme(); + const colors: Record = { + success: theme.palette.success.main, + error: theme.palette.error.main, + info: theme.palette.primary.contrastTextSecondary, + }; + + return ( + + + + {text} + + + ); +}; diff --git a/client/src/Components/v2/DesignElements/Fallback.tsx b/client/src/Components/v2/DesignElements/Fallback.tsx new file mode 100644 index 000000000..f7b48a113 --- /dev/null +++ b/client/src/Components/v2/DesignElements/Fallback.tsx @@ -0,0 +1,158 @@ +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import OutputAnimation from "@/assets/Animations/output.gif"; +import DarkmodeOutput from "@/assets/Animations/darkmodeOutput.gif"; +import Typography from "@mui/material/Typography"; +import { BulletPointCheck } from "@/Components/v2/DesignElements"; +import { Button } from "@/Components/v2/Inputs"; + +import { useNavigate } from "react-router"; +import { useMediaQuery } from "@mui/material"; +import { useTheme } from "@mui/material/styles"; +import { useSelector } from "react-redux"; + +import type { BoxProps } from "@mui/material"; + +interface BaseFallbackProps extends BoxProps { + children: React.ReactNode; +} + +export const BaseFallback: React.FC = ({ children, ...props }) => { + const theme = useTheme(); + const mode = useSelector((state: any) => state.ui.mode); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + + return ( + + + + + + {children} + + + + ); +}; + +export const ErrorFallback = ({ + title, + subtitle, +}: { + title: string; + subtitle: string; +}) => { + const theme = useTheme(); + return ( + + + {title} + + {subtitle} + + ); +}; + +export const EmptyFallback = ({ + page, + title, + bullets, + actionButtonText, + actionLink, +}: { + page: string; + title: string; + bullets: any; + actionButtonText: string; + actionLink: string; +}) => { + const theme = useTheme(); + const navigate = useNavigate(); + return ( + + + + {title} + + + {bullets?.map((bullet: string, index: number) => ( + + ))} + + + + + + + ); +}; diff --git a/client/src/Components/v2/DesignElements/index.tsx b/client/src/Components/v2/DesignElements/index.tsx index 6ba0a19ac..3574daffb 100644 --- a/client/src/Components/v2/DesignElements/index.tsx +++ b/client/src/Components/v2/DesignElements/index.tsx @@ -1,7 +1,9 @@ export { SplitBox as HorizontalSplitBox, ConfigBox } from "./SplitBox"; -export { BasePage } from "./BasePage"; +export { BasePage, BasePageWithStates } from "./BasePage"; export { BGBox, UpStatusBox, DownStatusBox, PausedStatusBox } from "./StatusBox"; export { DataTable as Table, Pagination } from "./Table"; export { GradientBox, StatBox } from "./StatBox"; export { BaseBox } from "./BaseBox"; export { StatusLabel } from "./StatusLabel"; +export { BaseFallback, ErrorFallback, EmptyFallback } from "./Fallback"; +export { BulletPointCheck } from "./BulletPointCheck"; diff --git a/client/src/Pages/v2/Uptime/UptimeMonitors.tsx b/client/src/Pages/v2/Uptime/UptimeMonitors.tsx index 57865d929..e5a151eb1 100644 --- a/client/src/Pages/v2/Uptime/UptimeMonitors.tsx +++ b/client/src/Pages/v2/Uptime/UptimeMonitors.tsx @@ -1,5 +1,5 @@ import { - BasePage, + BasePageWithStates, UpStatusBox, DownStatusBox, PausedStatusBox, @@ -18,10 +18,10 @@ const UptimeMonitors = () => { const theme = useTheme(); const isSmall = useMediaQuery(theme.breakpoints.down("md")); - const { response, loading, refetch } = useGet( + const { response, isValidating, error, refetch } = useGet( "/monitors?embedChecks=true", {}, - { refreshInterval: 30000 } + { refreshInterval: 30000, keepPreviousData: true } ); const monitors: IMonitor[] = response?.data ?? ([] as IMonitor[]); @@ -43,14 +43,16 @@ const UptimeMonitors = () => { } ); - if (monitors.length === 0 && !loading) { - return "No monitors found"; - } - return ( - + { monitors={monitors} refetch={refetch} /> - + ); }; From ab3e6cdff92bb5ad071f2371e4602a39d1dabeb7 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 9 Oct 2025 13:53:34 -0700 Subject: [PATCH 087/400] normalzie on FE --- .../v2/DesignElements/StatusLabel.tsx | 10 ++++- .../Components/v2/Layouts/Sidebar/Menu.tsx | 32 ++++++++-------- .../v2/Monitors/ChartResponseTime.tsx | 14 ++++--- .../v2/Monitors/HistogramResponseTime.tsx | 28 ++++---------- .../v2/Monitors/HistogramStatus.tsx | 22 ++++++----- client/src/Pages/v2/Uptime/MonitorTable.tsx | 7 +++- client/src/Types/Check.ts | 1 + client/src/Utils/DataUtils.ts | 37 +++++++++++++++++++ 8 files changed, 97 insertions(+), 54 deletions(-) create mode 100644 client/src/Utils/DataUtils.ts diff --git a/client/src/Components/v2/DesignElements/StatusLabel.tsx b/client/src/Components/v2/DesignElements/StatusLabel.tsx index 69b788e96..d601785f9 100644 --- a/client/src/Components/v2/DesignElements/StatusLabel.tsx +++ b/client/src/Components/v2/DesignElements/StatusLabel.tsx @@ -5,7 +5,13 @@ import type { MonitorStatus } from "@/Types/Monitor"; import { getStatusPalette } from "@/Utils/MonitorUtils"; import { useTheme } from "@mui/material/styles"; -export const StatusLabel = ({ status }: { status: MonitorStatus }) => { +export const StatusLabel = ({ + status, + isActive, +}: { + status: MonitorStatus; + isActive: boolean; +}) => { const theme = useTheme(); const palette = getStatusPalette(status); const transformedText = status.charAt(0).toUpperCase() + status.slice(1).toLowerCase(); @@ -29,7 +35,7 @@ export const StatusLabel = ({ status }: { status: MonitorStatus }) => { borderRadius="50%" marginRight="5px" /> - {transformedText} + {isActive === false ? "Paused" : transformedText} ); }; diff --git a/client/src/Components/v2/Layouts/Sidebar/Menu.tsx b/client/src/Components/v2/Layouts/Sidebar/Menu.tsx index 5b3ed705e..9ca555a92 100644 --- a/client/src/Components/v2/Layouts/Sidebar/Menu.tsx +++ b/client/src/Components/v2/Layouts/Sidebar/Menu.tsx @@ -14,25 +14,25 @@ import ChangeLog from "@/assets/icons/changeLog.svg?react"; export const getMenu = (t: Function) => [ { name: t("menu.uptime"), path: "v2/uptime", icon: }, - { name: t("menu.pagespeed"), path: "pagespeed", icon: }, + // { name: t("menu.pagespeed"), path: "pagespeed", icon: }, - { name: t("menu.infrastructure"), path: "infrastructure", icon: }, - { - name: t("menu.notifications"), - path: "notifications", - icon: , - }, - { name: t("menu.incidents"), path: "incidents", icon: }, + // { name: t("menu.infrastructure"), path: "infrastructure", icon: }, + // { + // name: t("menu.notifications"), + // path: "notifications", + // icon: , + // }, + // { name: t("menu.incidents"), path: "incidents", icon: }, - { name: t("menu.statusPages"), path: "status", icon: }, - { name: t("menu.maintenance"), path: "maintenance", icon: }, - { name: t("menu.logs"), path: "logs", icon: }, + // { name: t("menu.statusPages"), path: "status", icon: }, + // { name: t("menu.maintenance"), path: "maintenance", icon: }, + // { name: t("menu.logs"), path: "logs", icon: }, - { - name: t("menu.settings"), - icon: , - path: "settings", - }, + // { + // name: t("menu.settings"), + // icon: , + // path: "settings", + // }, ]; export const getBottomMenu = (t: Function) => [ diff --git a/client/src/Components/v2/Monitors/ChartResponseTime.tsx b/client/src/Components/v2/Monitors/ChartResponseTime.tsx index 97aef314d..efa00488a 100644 --- a/client/src/Components/v2/Monitors/ChartResponseTime.tsx +++ b/client/src/Components/v2/Monitors/ChartResponseTime.tsx @@ -1,6 +1,7 @@ import { BaseChart } from "./HistogramStatus"; import { BaseBox } from "../DesignElements"; import ResponseTimeIcon from "@/assets/icons/response-time-icon.svg?react"; +import { normalizeResponseTimes } from "@/Utils/DataUtils"; import { AreaChart, Area, @@ -18,7 +19,7 @@ import { tooltipDateFormatLookup, } from "@/Utils/TimeUtils"; import { useTheme } from "@mui/material/styles"; -import type { GroupedCheck } from "@/Types/Check"; +import type { Check, GroupedCheck } from "@/Types/Check"; import { useSelector } from "react-redux"; type XTickProps = { @@ -68,7 +69,7 @@ const ResponseTimeToolTip: React.FC = ({ if (!active) return null; const format = tooltipDateFormatLookup(range); - const responseTime = Math.floor(payload?.[0]?.value || 0); + const responseTime = Math.floor(payload?.[0]?.payload?.avgResponseTime || 0); return ( {formatDateWithTz(label, format, uiTimezone)} @@ -86,7 +87,10 @@ export const ChartResponseTime = ({ }) => { const theme = useTheme(); const uiTimezone = useSelector((state: any) => state.ui.timezone); - + const normalized = normalizeResponseTimes( + checks, + "avgResponseTime" + ); return ( } @@ -96,7 +100,7 @@ export const ChartResponseTime = ({ width="100%" height={300} > - + diff --git a/client/src/Components/v2/Monitors/HistogramResponseTime.tsx b/client/src/Components/v2/Monitors/HistogramResponseTime.tsx index d19ddbf53..1925ac93c 100644 --- a/client/src/Components/v2/Monitors/HistogramResponseTime.tsx +++ b/client/src/Components/v2/Monitors/HistogramResponseTime.tsx @@ -3,24 +3,20 @@ import Box from "@mui/material/Box"; import { useTheme } from "@mui/material/styles"; import type { Check } from "@/Types/Check"; import { HistogramResponseTimeTooltip } from "@/Components/v2/Monitors/HistogramResponseTimeTooltip"; +import { normalizeResponseTimes } from "@/Utils/DataUtils"; export const HistogramResponseTime = ({ checks }: { checks: Check[] }) => { + const normalChecks = normalizeResponseTimes(checks, "responseTime"); let data = Array(); - data = checks.map((c) => Math.max(c.responseTime, 1)); - - const logResponses = data.map((r) => Math.log10(r)); - const logMin = Math.min(...logResponses); - const logMax = Math.max(...logResponses); - - if (!checks) { + if (!normalChecks || normalChecks.length === 0) { return null; } - if (checks.length !== 25) { - const placeholders = Array(25 - checks.length).fill("placeholder"); - data = [...checks, ...placeholders]; + if (normalChecks.length !== 25) { + const placeholders = Array(25 - normalChecks.length).fill("placeholder"); + data = [...normalChecks, ...placeholders]; } else { - data = checks; + data = normalChecks; } const theme = useTheme(); @@ -38,14 +34,6 @@ export const HistogramResponseTime = ({ checks }: { checks: Check[] }) => { }} > {data.map((check, index) => { - const safeResponse = Math.max(check.responseTime, 1); - const logValue = Math.log10(safeResponse); - const minHeight = 10; - const barHeight = - logMax === logMin - ? 100 - : Math.max(minHeight, ((logValue - logMin) / (logMax - logMin)) * 100); - if (check === "placeholder") { return ( { position="absolute" bottom={0} width="100%" - height={`${barHeight}%`} + height={`${check.normalResponseTime}%`} bgcolor={ check.status ? theme.palette.success.lowContrast diff --git a/client/src/Components/v2/Monitors/HistogramStatus.tsx b/client/src/Components/v2/Monitors/HistogramStatus.tsx index aeacf31d2..2e10a8042 100644 --- a/client/src/Components/v2/Monitors/HistogramStatus.tsx +++ b/client/src/Components/v2/Monitors/HistogramStatus.tsx @@ -9,6 +9,7 @@ import IncidentsIcon from "@/assets/icons/incidents.svg?react"; import type { GroupedCheck } from "@/Types/Check"; import type { MonitorStatus } from "@/Types/Monitor"; +import { normalizeResponseTimes } from "@/Utils/DataUtils"; import { useState } from "react"; import { formatDateWithTz } from "@/Utils/TimeUtils"; import { useSelector } from "react-redux"; @@ -122,8 +123,9 @@ export const HistogramStatus = ({ const theme = useTheme(); const [idx, setIdx] = useState(null); const dateFormat = range === "1d" || range === "2h" ? "MMM D, h A" : "MMM D"; + const normalChecks = normalizeResponseTimes(checks, "avgResponseTime"); - if (checks.length === 0) { + if (normalChecks.length === 0) { return ( { + const totalChecks = normalChecks.reduce((count, check) => { return count + check.count; }, 0); @@ -161,12 +163,12 @@ export const HistogramStatus = ({ Total checks {idx ? ( - {checks[idx].count} + {normalChecks[idx].count} - {formatDateWithTz(checks[idx]._id, dateFormat, uiTimezone)} + {formatDateWithTz(normalChecks[idx]._id, dateFormat, uiTimezone)} ) : ( @@ -178,26 +180,26 @@ export const HistogramStatus = ({ width="100%" height={155} > - + } /> - {checks?.map((groupedCheck, idx) => { - const fillColor = getResponseTimeColor(groupedCheck.avgResponseTime); + {normalChecks?.map((groupedCheck, idx) => { + const fillColor = getResponseTimeColor(groupedCheck.normalResponseTime); return ( setIdx(idx)} diff --git a/client/src/Pages/v2/Uptime/MonitorTable.tsx b/client/src/Pages/v2/Uptime/MonitorTable.tsx index a7b1d203f..209784309 100644 --- a/client/src/Pages/v2/Uptime/MonitorTable.tsx +++ b/client/src/Pages/v2/Uptime/MonitorTable.tsx @@ -104,7 +104,12 @@ export const MonitorTable = ({ id: "status", content: t("status"), render: (row) => { - return ; + return ( + + ); }, }, { diff --git a/client/src/Types/Check.ts b/client/src/Types/Check.ts index 542507a4b..cccc56f3d 100644 --- a/client/src/Types/Check.ts +++ b/client/src/Types/Check.ts @@ -27,6 +27,7 @@ export interface Check { status: string; message: string; responseTime: number; + normalResponseTime?: number; httpStatusCode: number; ack: boolean; expiry: string; diff --git a/client/src/Utils/DataUtils.ts b/client/src/Utils/DataUtils.ts new file mode 100644 index 000000000..3878d7d6a --- /dev/null +++ b/client/src/Utils/DataUtils.ts @@ -0,0 +1,37 @@ +const MIN_OUT = 10; +const MAX_OUT = 100; + +export const normalizeResponseTimes = , K extends keyof T>( + checks: T[], + key: K +): (T & { normalResponseTime: number })[] => { + if (!Array.isArray(checks) || checks.length === 0) + return checks as (T & { + normalResponseTime: number; + })[]; + + if (checks.length === 1) { + return [ + { + ...checks[0], + normalResponseTime: 50, + }, + ]; + } + + const { min, max } = checks.reduce( + (acc, check) => { + if (check[key] > acc.max) acc.max = check[key]; + if (check[key] < acc.min) acc.min = check[key]; + return acc; + }, + { max: -Infinity, min: Infinity } + ); + + const range = max - min || 1; + + return checks.map((check) => ({ + ...check, + normalResponseTime: MIN_OUT + ((check[key] - min) * (MAX_OUT - MIN_OUT)) / range, + })); +}; From 78b5cc9c6f81367f8935971201737741066eb65e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 9 Oct 2025 13:55:39 -0700 Subject: [PATCH 088/400] ts --- .../Components/v2/DesignElements/StatusLabel.tsx | 2 +- .../src/Components/v2/Layouts/Sidebar/Menu.tsx | 16 ++++++++-------- .../Components/v2/Monitors/ChartResponseTime.tsx | 2 +- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/client/src/Components/v2/DesignElements/StatusLabel.tsx b/client/src/Components/v2/DesignElements/StatusLabel.tsx index d601785f9..a314c09b0 100644 --- a/client/src/Components/v2/DesignElements/StatusLabel.tsx +++ b/client/src/Components/v2/DesignElements/StatusLabel.tsx @@ -10,7 +10,7 @@ export const StatusLabel = ({ isActive, }: { status: MonitorStatus; - isActive: boolean; + isActive?: boolean; }) => { const theme = useTheme(); const palette = getStatusPalette(status); diff --git a/client/src/Components/v2/Layouts/Sidebar/Menu.tsx b/client/src/Components/v2/Layouts/Sidebar/Menu.tsx index 9ca555a92..f0f99f33a 100644 --- a/client/src/Components/v2/Layouts/Sidebar/Menu.tsx +++ b/client/src/Components/v2/Layouts/Sidebar/Menu.tsx @@ -1,12 +1,12 @@ -import Notifications from "@/assets/icons/notifications.svg?react"; +// import Notifications from "@/assets/icons/notifications.svg?react"; import Monitors from "@/assets/icons/monitors.svg?react"; -import PageSpeed from "@/assets/icons/page-speed.svg?react"; -import Integrations from "@/assets/icons/integrations.svg?react"; -import Incidents from "@/assets/icons/incidents.svg?react"; -import StatusPages from "@/assets/icons/status-pages.svg?react"; -import Maintenance from "@/assets/icons/maintenance.svg?react"; -import Logs from "@/assets/icons/logs.svg?react"; -import Settings from "@/assets/icons/settings.svg?react"; +// import PageSpeed from "@/assets/icons/page-speed.svg?react"; +// import Integrations from "@/assets/icons/integrations.svg?react"; +// import Incidents from "@/assets/icons/incidents.svg?react"; +// import StatusPages from "@/assets/icons/status-pages.svg?react"; +// import Maintenance from "@/assets/icons/maintenance.svg?react"; +// import Logs from "@/assets/icons/logs.svg?react"; +// import Settings from "@/assets/icons/settings.svg?react"; import Support from "@/assets/icons/support.svg?react"; import Discussions from "@/assets/icons/discussions.svg?react"; import Docs from "@/assets/icons/docs.svg?react"; diff --git a/client/src/Components/v2/Monitors/ChartResponseTime.tsx b/client/src/Components/v2/Monitors/ChartResponseTime.tsx index efa00488a..28f40f254 100644 --- a/client/src/Components/v2/Monitors/ChartResponseTime.tsx +++ b/client/src/Components/v2/Monitors/ChartResponseTime.tsx @@ -19,7 +19,7 @@ import { tooltipDateFormatLookup, } from "@/Utils/TimeUtils"; import { useTheme } from "@mui/material/styles"; -import type { Check, GroupedCheck } from "@/Types/Check"; +import type { GroupedCheck } from "@/Types/Check"; import { useSelector } from "react-redux"; type XTickProps = { From 5018c56d0ddcdb58ba2b3c0b39e0a4f9bd6514b1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 9 Oct 2025 14:00:04 -0700 Subject: [PATCH 089/400] stikcy sidebar --- client/src/Components/v2/Layouts/RootLayout.tsx | 1 - client/src/Components/v2/Layouts/Sidebar/index.tsx | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/Components/v2/Layouts/RootLayout.tsx b/client/src/Components/v2/Layouts/RootLayout.tsx index 87c186228..4237494b1 100644 --- a/client/src/Components/v2/Layouts/RootLayout.tsx +++ b/client/src/Components/v2/Layouts/RootLayout.tsx @@ -6,7 +6,6 @@ const RootLayout = () => { const theme = useTheme(); return ( diff --git a/client/src/Components/v2/Layouts/Sidebar/index.tsx b/client/src/Components/v2/Layouts/Sidebar/index.tsx index b05716aee..a17b9fa13 100644 --- a/client/src/Components/v2/Layouts/Sidebar/index.tsx +++ b/client/src/Components/v2/Layouts/Sidebar/index.tsx @@ -37,6 +37,9 @@ export const SideBar = () => { Date: Thu, 9 Oct 2025 18:16:25 -0400 Subject: [PATCH 090/400] select component V2 --- client/src/Components/v2/Inputs/Select.tsx | 73 ++++++++++++++++++++-- 1 file changed, 69 insertions(+), 4 deletions(-) diff --git a/client/src/Components/v2/Inputs/Select.tsx b/client/src/Components/v2/Inputs/Select.tsx index fb388b334..cb3e12fcb 100644 --- a/client/src/Components/v2/Inputs/Select.tsx +++ b/client/src/Components/v2/Inputs/Select.tsx @@ -1,6 +1,71 @@ -import Select from "@mui/material/Select"; +import { Typography, Select } from "@mui/material"; +import MenuItem from "@mui/material/MenuItem"; +import { forwardRef } from "react"; import type { SelectProps } from "@mui/material/Select"; - -export const SelectInput: React.FC = ({ ...props }) => { - return { + if (!selected) { + return ( + + {placeholder ?? ""} + + ); + } + const selectedItem = items.find((item) => item._id === selected); + const displayName = selectedItem ? selectedItem.name : placeholder; + return ( + + {displayName} + + ); + }} + {...props} + > + {items.map((item) => ( + + {item.name} + + ))} + + ); + } +); +SelectInput.displayName = "SelectInput"; From 27d37ece7decd358cca955af8ab02c03bd2cb657 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Sun, 12 Oct 2025 21:11:20 -0400 Subject: [PATCH 091/400] feat: Add monitor grouping functionality to backend MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add optional 'group' field to Monitor schema with trimming and validation - Add group validation to Joi schemas for create and edit monitor endpoints - Add GET /api/v1/monitors/team/groups endpoint to fetch unique groups - Implement case-insensitive group filtering in database layer - Support for organizing monitors into collapsible groups 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- server/src/controllers/v1/monitorController.js | 18 ++++++++++++++++++ server/src/db/v1/models/Monitor.js | 9 +++++++++ server/src/db/v1/modules/monitorModule.js | 15 +++++++++++++++ server/src/routes/v1/monitorRoute.js | 1 + .../src/service/v1/business/monitorService.js | 5 +++++ server/src/validation/joi.js | 2 ++ 6 files changed, 50 insertions(+) diff --git a/server/src/controllers/v1/monitorController.js b/server/src/controllers/v1/monitorController.js index 462d5a278..5ac44fa94 100644 --- a/server/src/controllers/v1/monitorController.js +++ b/server/src/controllers/v1/monitorController.js @@ -452,6 +452,24 @@ class MonitorController extends BaseController { SERVICE_NAME, "getAllGames" ); + + getGroupsByTeamId = this.asyncHandler( + async (req, res) => { + const teamId = req?.user?.teamId; + if (!teamId) { + throw this.errorService.createBadRequestError("Team ID is required"); + } + + const groups = await this.monitorService.getGroupsByTeamId({ teamId }); + + return res.success({ + msg: "OK", + data: groups, + }); + }, + SERVICE_NAME, + "getGroupsByTeamId" + ); } export default MonitorController; diff --git a/server/src/db/v1/models/Monitor.js b/server/src/db/v1/models/Monitor.js index 0e12d3c26..7d499dcce 100755 --- a/server/src/db/v1/models/Monitor.js +++ b/server/src/db/v1/models/Monitor.js @@ -128,6 +128,15 @@ const MonitorSchema = mongoose.Schema( gameId: { type: String, }, + group: { + type: String, + trim: true, + maxLength: 50, + default: null, + set: function(value) { + return value && value.trim() ? value.trim() : null; + } + }, }, { timestamps: true, diff --git a/server/src/db/v1/modules/monitorModule.js b/server/src/db/v1/modules/monitorModule.js index f795a7f2e..83aa29dfe 100755 --- a/server/src/db/v1/modules/monitorModule.js +++ b/server/src/db/v1/modules/monitorModule.js @@ -573,6 +573,21 @@ class MonitorModule { throw error; } }; + + getGroupsByTeamId = async ({ teamId }) => { + try { + const groups = await this.Monitor.distinct("group", { + teamId: new this.ObjectId(teamId), + group: { $ne: null, $ne: "" } + }); + + return groups.filter(Boolean).sort(); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getGroupsByTeamId"; + throw error; + } + }; } export default MonitorModule; diff --git a/server/src/routes/v1/monitorRoute.js b/server/src/routes/v1/monitorRoute.js index 107964f6b..dc0dd694b 100755 --- a/server/src/routes/v1/monitorRoute.js +++ b/server/src/routes/v1/monitorRoute.js @@ -18,6 +18,7 @@ class MonitorRoutes { this.router.get("/team", this.monitorController.getMonitorsByTeamId); this.router.get("/team/with-checks", this.monitorController.getMonitorsWithChecksByTeamId); this.router.get("/team/summary", this.monitorController.getMonitorsAndSummaryByTeamId); + this.router.get("/team/groups", this.monitorController.getGroupsByTeamId); // Uptime routes this.router.get("/uptime/details/:monitorId", this.monitorController.getUptimeDetailsById); diff --git a/server/src/service/v1/business/monitorService.js b/server/src/service/v1/business/monitorService.js index 061234102..2b643ee64 100644 --- a/server/src/service/v1/business/monitorService.js +++ b/server/src/service/v1/business/monitorService.js @@ -267,6 +267,11 @@ class MonitorService { getAllGames = () => { return this.games; }; + + getGroupsByTeamId = async ({ teamId }) => { + const groups = await this.db.monitorModule.getGroupsByTeamId({ teamId }); + return groups; + }; } export default MonitorService; diff --git a/server/src/validation/joi.js b/server/src/validation/joi.js index 1cd5cd5d6..87d2b2a8c 100755 --- a/server/src/validation/joi.js +++ b/server/src/validation/joi.js @@ -174,6 +174,7 @@ const createMonitorBodyValidation = joi.object({ expectedValue: joi.string().allow(""), matchMethod: joi.string(), gameId: joi.string().allow(""), + group: joi.string().max(50).trim().allow(null, "").optional(), }); const createMonitorsBodyValidation = joi.array().items( @@ -203,6 +204,7 @@ const editMonitorBodyValidation = joi.object({ usage_temperature: joi.number(), }), gameId: joi.string(), + group: joi.string().max(50).trim().allow(null, "").optional(), }); const pauseMonitorParamValidation = joi.object({ From 6bdf9891e898f76ee07e298c3e25aa29c5042a07 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Sun, 12 Oct 2025 21:51:06 -0400 Subject: [PATCH 092/400] Fix server formatting issues MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- server/src/db/v1/models/Monitor.js | 4 ++-- server/src/db/v1/modules/monitorModule.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/db/v1/models/Monitor.js b/server/src/db/v1/models/Monitor.js index 7d499dcce..e6e1a0024 100755 --- a/server/src/db/v1/models/Monitor.js +++ b/server/src/db/v1/models/Monitor.js @@ -133,9 +133,9 @@ const MonitorSchema = mongoose.Schema( trim: true, maxLength: 50, default: null, - set: function(value) { + set: function (value) { return value && value.trim() ? value.trim() : null; - } + }, }, }, { diff --git a/server/src/db/v1/modules/monitorModule.js b/server/src/db/v1/modules/monitorModule.js index 83aa29dfe..39168f0b2 100755 --- a/server/src/db/v1/modules/monitorModule.js +++ b/server/src/db/v1/modules/monitorModule.js @@ -578,7 +578,7 @@ class MonitorModule { try { const groups = await this.Monitor.distinct("group", { teamId: new this.ObjectId(teamId), - group: { $ne: null, $ne: "" } + group: { $ne: null, $ne: "" }, }); return groups.filter(Boolean).sort(); From 104cf194ac52c1e456284f390892a8d48bd93699 Mon Sep 17 00:00:00 2001 From: karenvicent Date: Tue, 14 Oct 2025 12:22:20 -0400 Subject: [PATCH 093/400] fix create more specific component --- client/src/Components/v2/Inputs/Select.tsx | 125 +++++++++++---------- 1 file changed, 66 insertions(+), 59 deletions(-) diff --git a/client/src/Components/v2/Inputs/Select.tsx b/client/src/Components/v2/Inputs/Select.tsx index 6aa2498bf..54aacf68a 100644 --- a/client/src/Components/v2/Inputs/Select.tsx +++ b/client/src/Components/v2/Inputs/Select.tsx @@ -1,81 +1,88 @@ import { Typography, Select } from "@mui/material"; import MenuItem from "@mui/material/MenuItem"; -import { forwardRef } from "react"; import type { SelectProps } from "@mui/material/Select"; import { useTheme } from "@mui/material/styles"; import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; -type ItemId = string | number; + +export const SelectInput: React.FC = ({ ...props }) => { + const theme = useTheme(); + return ( + { - if (!selected) { - return ( - - {placeholder ?? ""} - - ); - } - const selectedItem = items.find((item) => item._id === selected); - const displayName = selectedItem ? selectedItem.name : placeholder; +export const SelectFromItems: React.FC = ({ + items, + placeholder, + isHidden = false, + hasError = false, + ...props +}) => { + return ( + { + if (!selected) { return ( - {displayName} + {placeholder ?? ""} ); - }} - {...props} - > - {items.map((item) => ( - item._id === selected); + const displayName = selectedItem ? selectedItem.name : placeholder; + return ( + - {item.name} - - ))} - - ); - } -); + {displayName} + + ); + }} + {...props} + > + {items.map((item) => ( + + {item.name} + + ))} + + ); +}; + SelectInput.displayName = "SelectInput"; +SelectFromItems.displayName = "SelectFromItems"; From e18e2e26da9ad83b14104c55712d5394342366b7 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 14 Oct 2025 12:45:11 -0700 Subject: [PATCH 094/400] move v2 utils to v2 dir --- .../v2/DesignElements/StatusLabel.tsx | 2 +- .../v2/Monitors/ChartAvgResponse.tsx | 2 +- .../v2/Monitors/ChartResponseTime.tsx | 4 +- .../v2/Monitors/HistogramResponseTime.tsx | 2 +- .../Monitors/HistogramResponseTimeTooltip.tsx | 2 +- .../v2/Monitors/HistogramStatus.tsx | 6 +- .../Components/v2/Monitors/MonitorStatus.tsx | 2 +- client/src/Hooks/v2/UseApi.tsx | 2 +- client/src/Hooks/v2/useInitMonitorForm.tsx | 17 ++ client/src/Pages/v2/Uptime/CheckTable.tsx | 2 +- client/src/Pages/v2/Uptime/Configure.tsx | 43 +++ client/src/Pages/v2/Uptime/Details.tsx | 2 +- client/src/Pages/v2/Uptime/UptimeForm.tsx | 247 ++++++++++++++++++ client/src/Utils/{ => v2}/ApiClient.ts | 0 client/src/Utils/{ => v2}/DataUtils.ts | 0 client/src/Utils/{ => v2}/MonitorUtils.ts | 2 +- client/src/Utils/{ => v2}/TimeUtils.ts | 0 17 files changed, 321 insertions(+), 14 deletions(-) create mode 100644 client/src/Hooks/v2/useInitMonitorForm.tsx create mode 100644 client/src/Pages/v2/Uptime/Configure.tsx create mode 100644 client/src/Pages/v2/Uptime/UptimeForm.tsx rename client/src/Utils/{ => v2}/ApiClient.ts (100%) rename client/src/Utils/{ => v2}/DataUtils.ts (100%) rename client/src/Utils/{ => v2}/MonitorUtils.ts (94%) rename client/src/Utils/{ => v2}/TimeUtils.ts (100%) diff --git a/client/src/Components/v2/DesignElements/StatusLabel.tsx b/client/src/Components/v2/DesignElements/StatusLabel.tsx index a314c09b0..e4d288e63 100644 --- a/client/src/Components/v2/DesignElements/StatusLabel.tsx +++ b/client/src/Components/v2/DesignElements/StatusLabel.tsx @@ -2,7 +2,7 @@ import Box from "@mui/material/Box"; import { BaseBox } from "@/Components/v2/DesignElements"; import type { MonitorStatus } from "@/Types/Monitor"; -import { getStatusPalette } from "@/Utils/MonitorUtils"; +import { getStatusPalette } from "@/Utils/v2/MonitorUtils"; import { useTheme } from "@mui/material/styles"; export const StatusLabel = ({ diff --git a/client/src/Components/v2/Monitors/ChartAvgResponse.tsx b/client/src/Components/v2/Monitors/ChartAvgResponse.tsx index 10938f359..e9b85cb43 100644 --- a/client/src/Components/v2/Monitors/ChartAvgResponse.tsx +++ b/client/src/Components/v2/Monitors/ChartAvgResponse.tsx @@ -4,7 +4,7 @@ import Typography from "@mui/material/Typography"; import AverageResponseIcon from "@/assets/icons/average-response-icon.svg?react"; import { Cell, RadialBarChart, RadialBar, ResponsiveContainer } from "recharts"; -import { getResponseTimeColor } from "@/Utils/MonitorUtils"; +import { getResponseTimeColor } from "@/Utils/v2/MonitorUtils"; import { useTheme } from "@mui/material/styles"; export const ChartAvgResponse = ({ avg, max }: { avg: number; max: number }) => { diff --git a/client/src/Components/v2/Monitors/ChartResponseTime.tsx b/client/src/Components/v2/Monitors/ChartResponseTime.tsx index 28f40f254..3c1c93f01 100644 --- a/client/src/Components/v2/Monitors/ChartResponseTime.tsx +++ b/client/src/Components/v2/Monitors/ChartResponseTime.tsx @@ -1,7 +1,7 @@ import { BaseChart } from "./HistogramStatus"; import { BaseBox } from "../DesignElements"; import ResponseTimeIcon from "@/assets/icons/response-time-icon.svg?react"; -import { normalizeResponseTimes } from "@/Utils/DataUtils"; +import { normalizeResponseTimes } from "@/Utils/v2/DataUtils"; import { AreaChart, Area, @@ -17,7 +17,7 @@ import { formatDateWithTz, tickDateFormatLookup, tooltipDateFormatLookup, -} from "@/Utils/TimeUtils"; +} from "@/Utils/v2/TimeUtils"; import { useTheme } from "@mui/material/styles"; import type { GroupedCheck } from "@/Types/Check"; import { useSelector } from "react-redux"; diff --git a/client/src/Components/v2/Monitors/HistogramResponseTime.tsx b/client/src/Components/v2/Monitors/HistogramResponseTime.tsx index 1925ac93c..aa1996e2a 100644 --- a/client/src/Components/v2/Monitors/HistogramResponseTime.tsx +++ b/client/src/Components/v2/Monitors/HistogramResponseTime.tsx @@ -3,7 +3,7 @@ import Box from "@mui/material/Box"; import { useTheme } from "@mui/material/styles"; import type { Check } from "@/Types/Check"; import { HistogramResponseTimeTooltip } from "@/Components/v2/Monitors/HistogramResponseTimeTooltip"; -import { normalizeResponseTimes } from "@/Utils/DataUtils"; +import { normalizeResponseTimes } from "@/Utils/v2/DataUtils"; export const HistogramResponseTime = ({ checks }: { checks: Check[] }) => { const normalChecks = normalizeResponseTimes(checks, "responseTime"); diff --git a/client/src/Components/v2/Monitors/HistogramResponseTimeTooltip.tsx b/client/src/Components/v2/Monitors/HistogramResponseTimeTooltip.tsx index d704cf5d4..d8c0263f0 100644 --- a/client/src/Components/v2/Monitors/HistogramResponseTimeTooltip.tsx +++ b/client/src/Components/v2/Monitors/HistogramResponseTimeTooltip.tsx @@ -1,7 +1,7 @@ import Stack from "@mui/material/Stack"; import Tooltip from "@mui/material/Tooltip"; import Typography from "@mui/material/Typography"; -import { formatDateWithTz } from "@/Utils/TimeUtils"; +import { formatDateWithTz } from "@/Utils/v2/TimeUtils"; import { useSelector } from "react-redux"; import type { LatestCheck } from "@/Types/Check"; diff --git a/client/src/Components/v2/Monitors/HistogramStatus.tsx b/client/src/Components/v2/Monitors/HistogramStatus.tsx index 2e10a8042..bc6675bc3 100644 --- a/client/src/Components/v2/Monitors/HistogramStatus.tsx +++ b/client/src/Components/v2/Monitors/HistogramStatus.tsx @@ -9,12 +9,12 @@ import IncidentsIcon from "@/assets/icons/incidents.svg?react"; import type { GroupedCheck } from "@/Types/Check"; import type { MonitorStatus } from "@/Types/Monitor"; -import { normalizeResponseTimes } from "@/Utils/DataUtils"; +import { normalizeResponseTimes } from "@/Utils/v2/DataUtils"; import { useState } from "react"; -import { formatDateWithTz } from "@/Utils/TimeUtils"; +import { formatDateWithTz } from "@/Utils/v2/TimeUtils"; import { useSelector } from "react-redux"; import { useTheme } from "@mui/material/styles"; -import { getResponseTimeColor } from "@/Utils/MonitorUtils"; +import { getResponseTimeColor } from "@/Utils/v2/MonitorUtils"; const XLabel = ({ p1, diff --git a/client/src/Components/v2/Monitors/MonitorStatus.tsx b/client/src/Components/v2/Monitors/MonitorStatus.tsx index ff4d83352..f329e6bb8 100644 --- a/client/src/Components/v2/Monitors/MonitorStatus.tsx +++ b/client/src/Components/v2/Monitors/MonitorStatus.tsx @@ -3,7 +3,7 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import { PulseDot } from "@/Components/v2/DesignElements/PulseDot"; import { Dot } from "@/Components/v2/DesignElements/Dot"; -import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils"; +import { getStatusColor, formatUrl } from "@/Utils/v2/MonitorUtils"; import { useTheme } from "@mui/material/styles"; import prettyMilliseconds from "pretty-ms"; import { typographyLevels } from "@/Utils/Theme/v2/palette"; diff --git a/client/src/Hooks/v2/UseApi.tsx b/client/src/Hooks/v2/UseApi.tsx index d3001c87b..2ee37c0da 100644 --- a/client/src/Hooks/v2/UseApi.tsx +++ b/client/src/Hooks/v2/UseApi.tsx @@ -2,7 +2,7 @@ import { useState } from "react"; import useSWR from "swr"; import type { SWRConfiguration } from "swr"; import type { AxiosRequestConfig } from "axios"; -import { get, post, patch } from "@/Utils/ApiClient"; // your axios wrapper +import { get, post, patch } from "@/Utils/v2/ApiClient"; // your axios wrapper export type ApiResponse = { message: string; diff --git a/client/src/Hooks/v2/useInitMonitorForm.tsx b/client/src/Hooks/v2/useInitMonitorForm.tsx new file mode 100644 index 000000000..2e98b3753 --- /dev/null +++ b/client/src/Hooks/v2/useInitMonitorForm.tsx @@ -0,0 +1,17 @@ +import { monitorSchema } from "@/Validation/v2/zod"; +import { z } from "zod"; +export const useInitForm = ({ + initialData, +}: { + initialData: Partial> | undefined; +}) => { + const defaults: z.infer = { + type: initialData?.type || "http", + url: initialData?.url || "", + n: initialData?.n || 3, + notificationChannels: initialData?.notificationChannels || [], + name: initialData?.name || "", + interval: initialData?.interval || "1 minute", + }; + return { defaults }; +}; diff --git a/client/src/Pages/v2/Uptime/CheckTable.tsx b/client/src/Pages/v2/Uptime/CheckTable.tsx index 42da6da12..c2811c453 100644 --- a/client/src/Pages/v2/Uptime/CheckTable.tsx +++ b/client/src/Pages/v2/Uptime/CheckTable.tsx @@ -10,7 +10,7 @@ import type { MonitorStatus } from "@/Types/Monitor"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useGet } from "@/Hooks/v2/UseApi"; -import { formatDateWithTz } from "@/Utils/TimeUtils"; +import { formatDateWithTz } from "@/Utils/v2/TimeUtils"; import { useSelector } from "react-redux"; const getHeaders = (t: Function, uiTimezone: string) => { const headers: Header[] = [ diff --git a/client/src/Pages/v2/Uptime/Configure.tsx b/client/src/Pages/v2/Uptime/Configure.tsx new file mode 100644 index 000000000..dc014669b --- /dev/null +++ b/client/src/Pages/v2/Uptime/Configure.tsx @@ -0,0 +1,43 @@ +import { monitorSchema } from "@/Validation/v2/zod"; +import { useGet, usePost } from "@/Hooks/v2/UseApi"; +import { UptimeForm } from "@/Pages/v2/Uptime/UptimeForm"; + +import { useParams } from "react-router"; +import type { ApiResponse } from "@/Hooks/v2/UseApi"; +import humanInterval from "human-interval"; +import { z } from "zod"; + +export const UptimeConfigurePage = () => { + type FormValues = z.infer; + type SubmitValues = Omit & { interval: number | undefined }; + + const { id } = useParams(); + const { response } = useGet("/notification-channels"); + const { response: monitorResponse } = useGet(`/monitors/${id}`); + const monitor = monitorResponse?.data || null; + const notificationOptions = response?.data ?? []; + + const { post, loading, error } = usePost(); + + const onSubmit = async (data: FormValues) => { + let interval = humanInterval(data.interval); + if (!interval) interval = 60000; + const submitData = { ...data, interval }; + const result = await post("/monitors", submitData); + if (result) { + console.log(result); + } else { + console.error(error); + } + }; + return ( + + ); +}; diff --git a/client/src/Pages/v2/Uptime/Details.tsx b/client/src/Pages/v2/Uptime/Details.tsx index c2069c7e4..5515e7b32 100644 --- a/client/src/Pages/v2/Uptime/Details.tsx +++ b/client/src/Pages/v2/Uptime/Details.tsx @@ -14,7 +14,7 @@ import { useTheme } from "@mui/material/styles"; import { useParams } from "react-router"; import { useGet, usePatch, type ApiResponse } from "@/Hooks/v2/UseApi"; import { useState } from "react"; -import { getStatusPalette } from "@/Utils/MonitorUtils"; +import { getStatusPalette } from "@/Utils/v2/MonitorUtils"; import prettyMilliseconds from "pretty-ms"; const UptimeDetailsPage = () => { diff --git a/client/src/Pages/v2/Uptime/UptimeForm.tsx b/client/src/Pages/v2/Uptime/UptimeForm.tsx new file mode 100644 index 000000000..e9636e5dd --- /dev/null +++ b/client/src/Pages/v2/Uptime/UptimeForm.tsx @@ -0,0 +1,247 @@ +import Stack from "@mui/material/Stack"; +import { TextInput } from "@/Components/v2/Inputs/TextInput"; +import { AutoCompleteInput } from "@/Components/v2/Inputs/AutoComplete"; +import { ConfigBox, BasePage } from "@/Components/v2/DesignElements"; +import RadioGroup from "@mui/material/RadioGroup"; +import FormControl from "@mui/material/FormControl"; +import { RadioWithDescription } from "@/Components/v2/Inputs/RadioInput"; +import { Button } from "@/Components/v2/Inputs"; +import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded"; +import { Typography } from "@mui/material"; + +import { useTranslation } from "react-i18next"; +import { monitorSchema } from "@/Validation/v2/zod"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm, Controller, useWatch, type SubmitHandler } from "react-hook-form"; +import { useTheme } from "@mui/material/styles"; +import { useInitForm } from "@/Hooks/v2/useInitMonitorForm"; + +type FormValues = z.infer; + +export const UptimeForm = ({ + initialData, + onSubmit, + notificationOptions, + loading, +}: { + initialData?: Partial; + onSubmit: SubmitHandler; + notificationOptions: any[]; + loading: boolean; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const { defaults } = useInitForm({ initialData: initialData }); + const { + handleSubmit, + control, + setValue, + formState: { errors }, + } = useForm({ + resolver: zodResolver(monitorSchema) as any, + defaultValues: defaults, + mode: "onChange", + }); + + const selectedType = useWatch({ + control, + name: "type", + }); + const notificationChannels = useWatch({ + control, + name: "notificationChannels", + }); + + return ( + + ( + + + + + + + + )} + /> + } + /> + + ( + + )} + /> + ( + + )} + /> + + } + /> + ( + { + const target = e.target as HTMLInputElement; + field.onChange(target.valueAsNumber); + }} + /> + )} + /> + } + /> + + ( + option.name} + value={notificationOptions.filter((o: any) => + (field.value || []).includes(o._id) + )} + onChange={(_, newValue) => { + field.onChange(newValue.map((o: any) => o._id)); + }} + /> + )} + /> + + {notificationChannels.map((notificationId) => { + const option = notificationOptions.find( + (o: any) => o._id === notificationId + ); + if (!option) return null; + return ( + + {option.name} + { + const updated = notificationChannels.filter( + (id) => id !== notificationId + ); + setValue("notificationChannels", updated); + }} + sx={{ cursor: "pointer" }} + /> + + ); + })} + + + } + /> + ( + + )} + /> + } + /> + + + + + ); +}; diff --git a/client/src/Utils/ApiClient.ts b/client/src/Utils/v2/ApiClient.ts similarity index 100% rename from client/src/Utils/ApiClient.ts rename to client/src/Utils/v2/ApiClient.ts diff --git a/client/src/Utils/DataUtils.ts b/client/src/Utils/v2/DataUtils.ts similarity index 100% rename from client/src/Utils/DataUtils.ts rename to client/src/Utils/v2/DataUtils.ts diff --git a/client/src/Utils/MonitorUtils.ts b/client/src/Utils/v2/MonitorUtils.ts similarity index 94% rename from client/src/Utils/MonitorUtils.ts rename to client/src/Utils/v2/MonitorUtils.ts index dcd202361..c47b7a82e 100644 --- a/client/src/Utils/MonitorUtils.ts +++ b/client/src/Utils/v2/MonitorUtils.ts @@ -1,5 +1,5 @@ import type { MonitorStatus } from "@/Types/Monitor"; -import type { PaletteKey } from "./Theme/v2/theme"; +import type { PaletteKey } from "@/Utils/Theme/v2/theme"; export const getStatusPalette = (status: MonitorStatus): PaletteKey => { const paletteMap: Record = { up: "success", diff --git a/client/src/Utils/TimeUtils.ts b/client/src/Utils/v2/TimeUtils.ts similarity index 100% rename from client/src/Utils/TimeUtils.ts rename to client/src/Utils/v2/TimeUtils.ts From 06ebb7694841573e1a74101718be7221e2d94ed6 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Sun, 19 Oct 2025 23:18:50 -0400 Subject: [PATCH 095/400] chore: remove 13 unused CSS variables from index.css MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed the following unused CSS variables that were leftovers from when the project switched to the MUI theme system: - --env-var-radius-1 - --env-var-width-1 - --env-var-width-3 - --env-var-height-1 - --env-var-spacing-3 - --env-var-spacing-4 - --env-var-spacing-5 - --env-var-img-width-1 - --env-var-img-width-2 - --env-var-img-width-3 - --env-var-default-1 - --env-var-default-2 - --env-var-shadow-1 These variables are not referenced anywhere in the codebase. The project now uses MUI theme configuration (theme.spacing(), theme.palette.*) instead of CSS variables for styling. Kept 19 CSS variables that are still actively used during the migration to pure MUI theme. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- client/src/index.css | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/client/src/index.css b/client/src/index.css index a5d305d46..d58a7e872 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -15,15 +15,11 @@ html { -moz-osx-font-smoothing: grayscale; /* Generalized Stylings */ - --env-var-radius-1: 4px; --env-var-radius-2: 8px; - --env-var-width-1: 100vw; --env-var-width-2: 360px; - --env-var-width-3: 250px; --env-var-width-4: 100px; - --env-var-height-1: 100vh; --env-var-height-2: 34px; --env-var-nav-bar-height: 70px; @@ -35,9 +31,6 @@ html { --env-var-spacing-1-plus: 16px; --env-var-spacing-1-minus: 10px; --env-var-spacing-2: 24px; - --env-var-spacing-3: 32px; - --env-var-spacing-4: 40px; - --env-var-spacing-5: 65px; --env-var-font-size-small: 11px; --env-var-font-size-small-plus: 12px; @@ -46,16 +39,6 @@ html { --env-var-font-size-large: 16px; --env-var-font-size-large-plus: 22px; --env-var-font-size-xlarge: 30px; - - --env-var-img-width-1: 20px; - --env-var-img-width-2: 16px; - --env-var-img-width-3: 12px; - - --env-var-default-1: 24px; - --env-var-default-2: 40px; - - --env-var-shadow-1: - 0px 4px 24px -4px rgba(16, 24, 40, 0.08), 0px 3px 3px -3px rgba(16, 24, 40, 0.03); } .MuiInputBase-root.Mui-disabled input { From 9ae83cc34dee175d49a1ce03dbe838a41f9eafa4 Mon Sep 17 00:00:00 2001 From: LinPr <314573849@qq.com> Date: Mon, 20 Oct 2025 14:22:20 +0800 Subject: [PATCH 096/400] fix/v1: remove unnecessary import --- server/src/routes/v1/maintenanceWindowRoute.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/routes/v1/maintenanceWindowRoute.js b/server/src/routes/v1/maintenanceWindowRoute.js index 3df040994..345f2c13c 100755 --- a/server/src/routes/v1/maintenanceWindowRoute.js +++ b/server/src/routes/v1/maintenanceWindowRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import MaintenanceWindow from "../../db/v1/models/MaintenanceWindow.js"; + class MaintenanceWindowRoutes { constructor(maintenanceWindowController) { this.router = Router(); From 0baa7d989187a40744b16dfb535e8d63e89c7190 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 20 Oct 2025 17:36:14 +0000 Subject: [PATCH 097/400] feat: translations updated from POEditor --- client/src/locales/ar.json | 17 +- client/src/locales/cs.json | 17 +- client/src/locales/de.json | 177 ++++---- client/src/locales/en.json | 8 +- client/src/locales/es.json | 83 ++-- client/src/locales/fi.json | 17 +- client/src/locales/fr.json | 17 +- client/src/locales/ja.json | 17 +- client/src/locales/pt-BR.json | 17 +- client/src/locales/ru.json | 738 ++++++++++++++++++---------------- client/src/locales/th.json | 17 +- client/src/locales/tr.json | 17 +- client/src/locales/zh-CN.json | 17 +- client/src/locales/zh-TW.json | 17 +- 14 files changed, 672 insertions(+), 504 deletions(-) diff --git a/client/src/locales/ar.json b/client/src/locales/ar.json index d8019d5de..6cec253d8 100644 --- a/client/src/locales/ar.json +++ b/client/src/locales/ar.json @@ -421,7 +421,13 @@ } } }, - "role": "" + "role": "", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "", @@ -483,7 +489,8 @@ "ping": "", "docker": "", "port": "", - "game": "" + "game": "", + "https": "" }, "common": { "appName": "", @@ -1071,7 +1078,11 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigTitle": "", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/cs.json b/client/src/locales/cs.json index 71c7e24b9..837b49803 100644 --- a/client/src/locales/cs.json +++ b/client/src/locales/cs.json @@ -421,7 +421,13 @@ } } }, - "role": "" + "role": "", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "", @@ -483,7 +489,8 @@ "ping": "", "docker": "", "port": "", - "game": "" + "game": "", + "https": "" }, "common": { "appName": "Checkmate", @@ -1071,7 +1078,11 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigTitle": "", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/de.json b/client/src/locales/de.json index ef8df5df4..da5a3ee61 100644 --- a/client/src/locales/de.json +++ b/client/src/locales/de.json @@ -382,50 +382,56 @@ "created": "Angelegt" }, "addTeamMember": { - "addMemberMenu": "", - "title": "", - "description": "", - "addButton": "" + "addMemberMenu": "Teammitglied hinzufügen", + "title": "Neues Teammitglied registrieren", + "description": "Einen neuen Benutzer anlegen und Zugangsdaten mit ihm teilen. Diese Methode gibt dem Mitglied direkt Zugriff auf alle Überwachungen.", + "addButton": "Mitglied hinzufügen" }, - "register": "", + "register": "Teammitglied registrieren", "registerToast": { - "success": "", - "dbUserExists": "", - "unknownError": "" + "success": "Benutzer erstellt, Zugangsdaten auf sichere Weise weitergeben.", + "dbUserExists": "Der Benutzer existiert bereits.", + "unknownError": "Unbekannter Fehler aufgetreten." }, "registerTeamMember": { - "title": "", + "title": "Teammitglied registrieren", "auth": { "common": { "inputs": { "firstName": { "errors": { - "empty": "", - "pattern": "" + "empty": "Bitte einen Namen eingeben", + "pattern": "Der Name darf nur Buchstaben, Leerzeichen, Apostrophe oder Bindestriche enthalten" } }, "lastName": { "errors": { - "empty": "", - "pattern": "" + "empty": "Bitte einen Vornamen eingeben", + "pattern": "Der Vorname darf nur Buchstaben, Leerzeichen, Apostrophe oder Bindestriche enthalten" } }, "email": { "errors": { - "empty": "", - "invalid": "" + "empty": "Zum Fortsetzen bitte eine E-Mail-Adresse eingeben", + "invalid": "Bitte die Gültigkeit der E-Mail-Adresse erneut prüfen" } }, "role": { "errors": { - "empty": "" + "empty": "Eine Rolle wird benötigt" } } } } } }, - "role": "" + "role": "Rolle", + "changeTeamPassword": { + "changePasswordMenu": "Passwort zurücksetzen", + "title": "Passwort des Teammitgliedes zurücksetzen", + "description": "Neues Passwort für dieses Teammitglied erstellen. Es muss auf einem Sicheren weg zur Verfügung gestellt werden.", + "success": "Passwort erfolgreich zurückgesetzt. Bitte sicherstellen, dass die Zugangsdaten auf sichere Weise weitergegeben werden." + } }, "monitorState": { "paused": "Pausiert", @@ -487,7 +493,8 @@ "ping": "Geben Sie die IP-Adresse oder den Hostnamen ein, der angepingt werden soll (z. B. 192.168.1.100 oder example.com), und fügen Sie einen eindeutigen Anzeigenamen hinzu, der auf dem Dashboard angezeigt wird.", "docker": "Geben Sie die Docker-ID Ihres Containers ein. Docker-IDs müssen die vollständige 64-Zeichen-ID sein. Sie können docker inspect ausführen, um die vollständige Container-ID abzurufen.", "port": "Geben Sie die URL oder IP des Servers, die Portnummer und einen eindeutigen Anzeigenamen ein, der auf dem Dashboard angezeigt wird.", - "game": "" + "game": "IP Adresse oder Hostnamen und Port für Ping eingeben (z.B. 192.168.1.100 oder beispiel.de) und Spieletyp auswählen.", + "https": "Zu überwachende Adresse oder IP (z.B. https://beispiel.de/ oder 192.168.1.100) eingeben und einen klaren Anzeigenamen, der im Dashboard erscheint." }, "common": { "appName": "Checkmate", @@ -607,7 +614,7 @@ "forgotPassword": "Passwort vergessen?", "register": "Noch keinen Account?", "forgotPasswordLink": "Passwort zurücksetzen", - "registerLink": "Hier resistrieren" + "registerLink": "Hier registrieren" }, "toasts": { "success": "Willkommen zurück. Erfolgreich angemeldet.", @@ -618,7 +625,7 @@ "incorrect": "Das Passwort stimmt nicht mit dem gespeicherten überein" } }, - "welcome": "" + "welcome": "Willkommen zurück bei Checkmate!" }, "registration": { "heading": { @@ -645,7 +652,7 @@ "toasts": { "success": "Willkommen! Benutzer erfolgreich angelegt" }, - "welcome": "" + "welcome": "Willkommen bei Checkmate!" }, "forgotPassword": { "heading": "Passwort vergessen?", @@ -746,8 +753,8 @@ "monitorStatus": { "checkingEvery": "Überprüfung alle {{interval}}", "withCaptureAgent": "mit Capture-Agent {{version}}", - "up": "hoch", - "down": "runter", + "up": "Online", + "down": "Offline", "paused": "pausiert" }, "advancedMatching": "Erweiterter Abgleich", @@ -792,7 +799,7 @@ "lastRunHeader": "Letzter Lauf um", "lastFinishedAtHeader": "Zuletzt abgeschlossen um", "lastRunTookHeader": "Letzter Lauf dauerte", - "intervalHeader": "" + "intervalHeader": "Invervall" }, "metricsTable": { "title": "Warteschlangenmetriken", @@ -900,8 +907,8 @@ "title": "Überwachen Sie IP/URL auf der Statusseite" }, "globalThresholds": { - "title": "", - "description": "" + "title": "Globale Schwellenwerte", + "description": "Globale CPU-, Speicher-, Festplatten- und Temperaturschwellenwerte festlegen. Wenn ein Wert angegeben ist, wird er automatisch für die Überwachung genutzt." } }, "statusPageCreate": { @@ -975,11 +982,11 @@ "oneHour": "1 Stunde", "oneDay": "1 Tag", "oneWeek": "1 Woche", - "fourMinutes": "", - "oneMinute": "", - "twoMinutes": "", - "fifteenSeconds": "", - "thirtySeconds": "" + "fourMinutes": "4 Minuten", + "oneMinute": "Eine Minute", + "twoMinutes": "Zwei Minuten", + "fifteenSeconds": "15 Sekunden", + "thirtySeconds": "30 Sekunden" }, "general": { "noOptionsFound": "Keine {{unit}} gefunden" @@ -1050,73 +1057,77 @@ "incidentsPageActionResolveMonitor": "Monitor-Vorfälle beheben", "incidentsPageActionResolveAll": "Alle Vorfälle lösen", "matchMethodOptions": { - "equal": "", - "equalPlaceholder": "", - "include": "", - "includePlaceholder": "", - "regex": "", - "regexPlaceholder": "", - "text": "" + "equal": "Gleich", + "equalPlaceholder": "Erfolg", + "include": "Enthält", + "includePlaceholder": "OK", + "regex": "Regex", + "regexPlaceholder": "^(success|ok)$", + "text": "Übereinstummungsmethode" }, "monitorType": { "docker": { - "label": "", - "namePlaceholder": "", - "placeholder": "" + "label": "Container Name/ID", + "namePlaceholder": "Mein Container", + "placeholder": "Meine-App" }, "http": { - "label": "", - "namePlaceholder": "", - "placeholder": "" + "label": "Zu überwachende URL", + "namePlaceholder": "Google", + "placeholder": "google.de" }, "ping": { - "label": "", - "namePlaceholder": "", - "placeholder": "" + "label": "Zu überwachende IP-Adresse", + "namePlaceholder": "Google", + "placeholder": "1.1.1.1" }, "port": { - "label": "", - "namePlaceholder": "", - "placeholder": "" + "label": "Zu überwachende URL", + "namePlaceholder": "localhost:5173", + "placeholder": "localhost" }, "game": { - "label": "", - "namePlaceholder": "", - "placeholder": "" + "label": "Zu überwachende URL", + "namePlaceholder": "localhost:5173", + "placeholder": "localhost" } }, "uptimeAdvancedMatching": { - "jsonPath": "" + "jsonPath": "JSON Pfad" }, - "bytesPerSecond": "", - "bytesReceived": "", - "bytesSent": "", - "chooseGame": "", + "bytesPerSecond": "Bytes pro Sekunde", + "bytesReceived": "Empfangene Bytes", + "bytesSent": "Gesendete Bytes", + "chooseGame": "Spiel auswählen", "createMonitorPage": { - "incidentConfigDescription": "", - "incidentConfigStatusWindowLabel": "", - "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigDescription": "Mithilfe eines gleitenden Fensters wird ermittelt, wann ein Monitor ausfällt. Der Status eines Monitors ändert sich nur, wenn der Prozentsatz der Prüfungen im gleitenden Fenster den angegebenen Wert erreicht.", + "incidentConfigStatusWindowLabel": "Wie viele Prüfungen sollen im gleitenden Fenster ausgeführt werden?", + "incidentConfigStatusWindowThresholdLabel": "Wie viel Prozent der Prüfungen im gleitenden Fenster sollen fehlschlagen/erfolgreich sein, bevor sich der Status ändert?", + "incidentConfigTitle": "Vorfälle", + "incidentConfigDescriptionV2": "Anzahl der aufeinanderfolgenden Prüfungen, die zum Ändern des Monitorstatus erforderlich sind. Maximal 25", + "incidentConfigStatusCheckNumber": "Anzahl der Prüfungen bevor sich er Status ändert", + "intervalTitle": "Prüfintervall", + "intervalDescription": "Wie oft soll die Überwachung geprüft werden." }, - "dataRate": "", - "dataReceived": "", - "dataSent": "", - "details": "", - "drops": "", - "errors": "", - "errorsIn": "", - "errorsOut": "", - "gameServerMonitoring": "", - "gameServerMonitoringDescription": "", - "network": "", - "networkDrops": "", - "networkErrors": "", - "networkInterface": "", - "noNetworkStatsAvailable": "", - "packetsPerSecond": "", - "packetsReceived": "", - "packetsReceivedRate": "", - "packetsSent": "", - "rate": "", - "selectInterface": "" + "dataRate": "Datenrate", + "dataReceived": "Empfangene Daten", + "dataSent": "Gesendete Daten", + "details": "Details", + "drops": "verloren", + "errors": "Fehler", + "errorsIn": "Fehler eingehend", + "errorsOut": "Fehler ausgehend", + "gameServerMonitoring": "Spiele Server Überwachung", + "gameServerMonitoringDescription": "Prüfen ob der Spiele-Server läuft oder nicht", + "network": "Netzwerk", + "networkDrops": "Netzwerkabbrüche", + "networkErrors": "Netzwerkfehler", + "networkInterface": "Netzwerkschnittstelle", + "noNetworkStatsAvailable": "Keine Netzwerkstatistiken verfügbar.", + "packetsPerSecond": "Pakete pro Sekunde", + "packetsReceived": "Empfangene Pakete", + "packetsReceivedRate": "Rate empfangener Pakete", + "packetsSent": "Gesendete Pakete", + "rate": "Rate", + "selectInterface": "Schnittstelle auswählen" } diff --git a/client/src/locales/en.json b/client/src/locales/en.json index da19c38da..13d4a7731 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -490,11 +490,11 @@ }, "uptimeGeneralInstructions": { "http": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.", - "https": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.", "ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.", "docker": "Enter the Docker container name or ID. You can use either the container name (e.g., my-app) or the container ID (full 64-char ID or short ID).", "port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard.", - "game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type." + "game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.", + "https": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard." }, "common": { "appName": "Checkmate", @@ -1101,11 +1101,11 @@ "chooseGame": "Choose game", "createMonitorPage": { "incidentConfigDescription": "A sliding window is used to determine when a monitor goes down. The status of a monitor will only change when the percentage of checks in the sliding window meet the specified value.", - "incidentConfigDescriptionV2": "Number of consecutive checks needed to change monitor status. Maximum 25", "incidentConfigStatusWindowLabel": "How many checks should be in the sliding window?", - "incidentConfigStatusCheckNumber": "How many checks before status change?", "incidentConfigStatusWindowThresholdLabel": "What percentage of checks in the sliding window fail/succeed before monitor status changes?", "incidentConfigTitle": "Incidents", + "incidentConfigDescriptionV2": "Number of consecutive checks needed to change monitor status. Maximum 25", + "incidentConfigStatusCheckNumber": "How many checks before status change?", "intervalTitle": "Check interval", "intervalDescription": "How often the monitor should be checked." }, diff --git a/client/src/locales/es.json b/client/src/locales/es.json index 440d40027..ed6a98c5d 100644 --- a/client/src/locales/es.json +++ b/client/src/locales/es.json @@ -129,32 +129,32 @@ "distributedUptimeStatusPublishedLabel": "", "distributedUptimeStatusCompanyNameLabel": "", "distributedUptimeStatusPageAddressLabel": "", - "distributedUptimeStatus30Days": "", - "distributedUptimeStatus60Days": "", - "distributedUptimeStatus90Days": "", + "distributedUptimeStatus30Days": "30 dias", + "distributedUptimeStatus60Days": "60 días", + "distributedUptimeStatus90Days": "90 dias", "distributedUptimeStatusPageNotSetUp": "", "distributedUptimeStatusContactAdmin": "", "distributedUptimeStatusPageNotPublic": "", "distributedUptimeStatusPageDeleteDialog": "", "distributedUptimeStatusPageDeleteConfirm": "", "distributedUptimeStatusPageDeleteDescription": "", - "distributedUptimeStatusDevices": "", + "distributedUptimeStatusDevices": "Dispositivos", "distributedUptimeStatusUpt": "", "distributedUptimeStatusUptBurned": "", "distributedUptimeStatusUptLogo": "", "incidentsTableNoIncidents": "", - "incidentsTablePaginationLabel": "", + "incidentsTablePaginationLabel": "Incidentes", "incidentsTableMonitorName": "", - "incidentsTableStatus": "", + "incidentsTableStatus": "Estado", "incidentsTableDateTime": "", "incidentsTableStatusCode": "", - "incidentsTableMessage": "", + "incidentsTableMessage": "Mensaje", "incidentsOptionsHeader": "", "incidentsOptionsHeaderFilterBy": "", - "incidentsOptionsHeaderFilterAll": "", + "incidentsOptionsHeaderFilterAll": "Todo", "incidentsOptionsHeaderFilterDown": "", "incidentsOptionsHeaderFilterCannotResolve": "", - "incidentsOptionsHeaderShow": "", + "incidentsOptionsHeaderShow": "Mostrar:", "incidentsOptionsHeaderLastHour": "", "incidentsOptionsHeaderLastDay": "", "incidentsOptionsHeaderLastWeek": "", @@ -165,22 +165,22 @@ "infrastructureCustomizeAlerts": "", "infrastructureAlertNotificationDescription": "", "infrastructureCreateMonitor": "", - "infrastructureProtocol": "", + "infrastructureProtocol": "Protocolo", "infrastructureServerUrlLabel": "", "infrastructureDisplayNameLabel": "", "infrastructureAuthorizationSecretLabel": "", - "gb": "", - "mb": "", - "mem": "", + "gb": "GB", + "mb": "MB", + "mem": "Mem", "memoryUsage": "", - "cpu": "", + "cpu": "CPU", "cpuUsage": "", "cpuTemperature": "", "diskUsage": "", "used": "", - "total": "", - "cores": "", - "frequency": "", + "total": "Total", + "cores": "Nucleos", + "frequency": "Frecuencia", "status": "", "cpuPhysical": "", "cpuLogical": "", @@ -189,25 +189,25 @@ "memory": "", "disk": "", "uptime": "", - "os": "", + "os": "SO", "host": "", "actions": "", "integrations": "", "integrationsPrism": "", - "integrationsSlack": "", + "integrationsSlack": "Slack", "integrationsSlackInfo": "", - "integrationsDiscord": "", + "integrationsDiscord": "Discord", "integrationsDiscordInfo": "", - "integrationsZapier": "", + "integrationsZapier": "Zapier", "integrationsZapierInfo": "", - "commonSave": "", + "commonSave": "Guardar", "createYour": "", "createMonitor": "", - "pause": "", - "resume": "", - "editing": "", - "url": "", - "access": "", + "pause": "Pausar", + "resume": "Resumir", + "editing": "Editando...", + "url": "URL", + "access": "Acceso", "timezone": "", "features": "", "administrator": "", @@ -231,13 +231,13 @@ "friendlyNamePlaceholder": "", "maintenanceRepeat": "", "maintenance": "", - "duration": "", + "duration": "Duración", "addMonitors": "", - "window": "", - "cancel": "", - "message": "", - "low": "", - "high": "", + "window": "ventana", + "cancel": "Cancelar", + "message": "Mensaje", + "low": "bajo", + "high": "alto", "statusCode": "", "date&Time": "", "type": "", @@ -421,7 +421,13 @@ } } }, - "role": "" + "role": "", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "", @@ -483,7 +489,8 @@ "ping": "", "docker": "", "port": "", - "game": "" + "game": "", + "https": "" }, "common": { "appName": "", @@ -1071,7 +1078,11 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigTitle": "", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/fi.json b/client/src/locales/fi.json index d98ece35b..4949817c7 100644 --- a/client/src/locales/fi.json +++ b/client/src/locales/fi.json @@ -421,7 +421,13 @@ } } }, - "role": "" + "role": "", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "Tauota", @@ -483,7 +489,8 @@ "ping": "", "docker": "", "port": "", - "game": "" + "game": "", + "https": "" }, "common": { "appName": "", @@ -1071,7 +1078,11 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigTitle": "", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/fr.json b/client/src/locales/fr.json index bfce12843..1ebc8bad4 100644 --- a/client/src/locales/fr.json +++ b/client/src/locales/fr.json @@ -425,7 +425,13 @@ } } }, - "role": "" + "role": "", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "Pause", @@ -487,7 +493,8 @@ "ping": "Entrez l'adresse IP ou le nom d'hôte à tester (par exemple, 192.168.1.100 ou exemple.fr) et ajoutez un nom familier qui apparaîtra sur le tableau de bord.", "docker": "Entrer l'ID Docker du container. Les identifiants Docker doivent être les 64 caractères de l'ID Docker. Vous pouvez utiliser la commande docker inspect pour avoir l'ID complet.", "port": "Entrez l'URL ou l'adresse IP du serveur, le numéro de port et un nom d'affichage familier qui apparaîtra sur le tableau de bord.", - "game": "" + "game": "", + "https": "" }, "common": { "appName": "Checkmate", @@ -1096,7 +1103,11 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigTitle": "", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/ja.json b/client/src/locales/ja.json index 6107c19d0..f458b8ed0 100644 --- a/client/src/locales/ja.json +++ b/client/src/locales/ja.json @@ -425,7 +425,13 @@ } } }, - "role": "" + "role": "", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "一時停止", @@ -487,7 +493,8 @@ "ping": "pingするIPアドレスまたはホスト名を入力(例: 192.168.1.100 または example.com)し、ダッシュボードに表示される明確な表示名を追加。", "docker": "コンテナのDocker IDを入力。Docker IDは64文字のフルDocker IDである必要があります。docker inspect を実行してフルコンテナIDを取得できます。", "port": "サーバーのURLまたはIP、ポート番号、ダッシュボードに表示される明確な表示名を入力。", - "game": "" + "game": "", + "https": "" }, "common": { "appName": "Checkmate", @@ -1075,7 +1082,11 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigTitle": "", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/pt-BR.json b/client/src/locales/pt-BR.json index 9e1fa6bc0..1a6d9a62f 100644 --- a/client/src/locales/pt-BR.json +++ b/client/src/locales/pt-BR.json @@ -425,7 +425,13 @@ } } }, - "role": "" + "role": "", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "Pausado", @@ -487,7 +493,8 @@ "ping": "Insira o endereço IP ou nome de domínio para ping (ex. 192.168.1.100 ou exemplo.com.br) e adicione uma descrição que aparecerá na dashboard.", "docker": "Insira o Docker Id do seu container. Docker Ids devem ser todos os 64 caracteres. Você pode executar docker inspect para descobrir o Id completo.", "port": "Insira a URL ou o IP do servidor, aporta e uma descrição que aparecerá na dashboard.", - "game": "" + "game": "", + "https": "" }, "common": { "appName": "Checkmate", @@ -1096,7 +1103,11 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigTitle": "", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/ru.json b/client/src/locales/ru.json index aa54de393..cdb4d235d 100644 --- a/client/src/locales/ru.json +++ b/client/src/locales/ru.json @@ -10,7 +10,7 @@ "settingsFailedToClearStats": "Не удалось очистить статистику", "settingsMonitorsDeleted": "Успешно удалены все мониторы", "settingsFailedToDeleteMonitors": "Не удалось удалить все мониторы", - "starPromptTitle": "", + "starPromptTitle": "Запустить Checkmate.", "starPromptDescription": "Ознакомьтесь с последними релизами и помогите развить сообщество на GitHub", "https": "HTTPS", "http": "HTTP", @@ -84,31 +84,35 @@ "unsupportedType": "Неподдерживаемый тип уведомления", "networkError": "Произошла сетевая ошибка", "fallback": { - "title": "", - "checks": [""], - "actionButton": "" + "title": "Канал уведомлений используется для:", + "checks": [ + "Дайте знать команде, если сервер ложится или что-то виснет", + "Сообщите инженерам, когда появляется какая-то проблема", + "Уведомляйте админов, если в системе что-то поменялось" + ], + "actionButton": "Давайте создадим ваш первый канал уведомлений!" }, - "createButton": "", - "createTitle": "", + "createButton": "Создать канал уведомлений", + "createTitle": "Канал уведомлений", "create": { - "success": "", - "failed": "" + "success": "Уведомление успешно создано", + "failed": "Не удалось создать уведомление." }, "fetch": { - "success": "", - "failed": "" + "success": "Уведомления успешно получены", + "failed": "Не удалось получить уведомления." }, "delete": { - "success": "", - "failed": "" + "success": "Уведомление успешно удалено", + "failed": "Не удалось удалить уведомление." }, "edit": { - "success": "", - "failed": "" + "success": "Уведомление успешно обновлено", + "failed": "Не удалось обновить уведомление." }, "test": { - "success": "", - "failed": "" + "success": "Тестовое уведомление успешно отправлено", + "failed": "Не удалось отправить тестовое уведомление." } }, "testLocale": "testLocale", @@ -332,8 +336,8 @@ "validationFailed": "Не удалось выполнить проверку", "noFileSelected": "Файл не выбран", "fallbackPage": "Импортируйте файл для массовой загрузки списка серверов", - "invalidFileType": "", - "uploadFailed": "" + "invalidFileType": "Неверный тип файла", + "uploadFailed": "Загрузка не удалась" }, "DeleteAccountTitle": "Удалить аккаунт", "DeleteAccountButton": "Удалить аккаунт", @@ -378,50 +382,56 @@ "created": "Создано" }, "addTeamMember": { - "addMemberMenu": "", - "title": "", - "description": "", - "addButton": "" + "addMemberMenu": "Добавить члена команды", + "title": "Зарегистрируйте нового члена команды", + "description": "Создайте нового пользователя и передайте ему учётные данные. Этот метод даёт участнику мгновенный доступ ко всем мониторам.", + "addButton": "Добавить участника" }, - "register": "", + "register": "Зарегистрируйте члена команды", "registerToast": { - "success": "", - "dbUserExists": "", - "unknownError": "" + "success": "Созданный пользователем, безопасный обмен учетными данными с участником.", + "dbUserExists": "Пользователь уже существует.", + "unknownError": "Произошла неизвестная ошибка." }, "registerTeamMember": { - "title": "", + "title": "Зарегистрировать члена команды", "auth": { "common": { "inputs": { "firstName": { "errors": { - "empty": "", - "pattern": "" + "empty": "Пожалуйста, введите имя", + "pattern": "Имя должно содержать только буквы, пробелы, апострофы или дефисы." } }, "lastName": { "errors": { - "empty": "", - "pattern": "" + "empty": "Пожалуйста, введите фамилию", + "pattern": "Фамилия должна содержать только буквы, пробелы, апострофы или дефисы." } }, "email": { "errors": { - "empty": "", - "invalid": "" + "empty": "Чтобы продолжить, введите адрес электронной почты.", + "invalid": "Пожалуйста, перепроверьте правильность введенного адреса электронной почты." } }, "role": { "errors": { - "empty": "" + "empty": "Требуется роль" } } } } } }, - "role": "" + "role": "Роль", + "changeTeamPassword": { + "changePasswordMenu": "Сбросить пароль", + "title": "Сбросить пароль члена команды", + "description": "Создайте новый пароль для этого члена команды. Вам необходимо будет сообщить ему пароль по секретному адресу.", + "success": "Пароль успешно сброшен. Убедитесь, что вы предоставили учётные данные участнику безопасным способом." + } }, "monitorState": { "paused": "Остановлено", @@ -445,8 +455,8 @@ "password": "Пароль", "team": "Команда", "logOut": "Выйти", - "notifications": "", - "logs": "" + "notifications": "Уведомления", + "logs": "Логи" }, "settingsEmailUser": "Email пользователь", "state": "Состояние", @@ -474,8 +484,8 @@ "settingsTestEmailUnknownError": "Неизвестная ошибка", "statusMsg": { "paused": "Мониторинг приостановлен", - "up": "", - "down": "", + "up": "Ваш сайт запущен.", + "down": "Сайт оффлайн.", "pending": "Ожидание..." }, "uptimeGeneralInstructions": { @@ -483,13 +493,14 @@ "ping": "Ведите IP-адрес или имя хоста для пинга (например, 192.168.1.100 или example.com) и добавьте понятное имя, которое будет отображаться на панели инструментов.", "docker": "Введите Docker ID вашего контейнера. Docker ID должен быть полным 64-символьным Docker ID. Вы можете запустить docker inspect , чтобы получить полный ID контейнера.", "port": "Введите URL-адрес или IP-адрес сервера, номер порта и понятное имя, которое будет отображаться на панели инструментов.", - "game": "" + "game": "Введите IP-адрес или имя хоста и номер порта для пинга (например, 192.168.1.100 или example.com) и выберите тип игры.", + "https": "Введите URL-адрес или IP-адрес для мониторинга (например, https://example.com/ или 192.168.1.100) и добавьте понятное отображаемое имя, которое будет отображаться на панели управления." }, "common": { - "appName": "", - "monitoringAgentName": "", + "appName": "Checkmate", + "monitoringAgentName": "Захватить", "buttons": { - "toggleTheme": "" + "toggleTheme": "Переключить тему" }, "toasts": { "networkError": "Ошибка сети", @@ -506,7 +517,7 @@ "inputs": { "email": { "label": "Электронная почта", - "placeholder": "", + "placeholder": "jordan.ellis@domain.com", "errors": { "empty": "Для продолжения введите свой адрес электронной почты", "invalid": "Пожалуйста, проверьте правильность введённого адреса электронной почты" @@ -560,7 +571,7 @@ }, "firstName": { "label": "Имя", - "placeholder": "", + "placeholder": "Jordan", "errors": { "empty": "Пожалуйста, введите свое имя", "length": "Имя должно быть короче 50 символов", @@ -569,7 +580,7 @@ }, "lastName": { "label": "Фамилия", - "placeholder": "", + "placeholder": "Ellis", "errors": { "empty": "Пожалуйста, введите свою фамилию", "length": "Фамилия должна быть короче 50 символов", @@ -583,12 +594,12 @@ "fields": { "password": { "errors": { - "incorrect": "" + "incorrect": "Введённый вами пароль неверный" } }, "role": { "errors": { - "min": "" + "min": "Требуется хотя бы одна роль" } } } @@ -602,8 +613,8 @@ "links": { "forgotPassword": "Забыли пароль?", "register": "У вас нет учетной записи?", - "forgotPasswordLink": "", - "registerLink": "" + "forgotPasswordLink": "Сбросить пароль", + "registerLink": "Зарегистрируйтесь здесь" }, "toasts": { "success": "С возвращением! Вы успешно вошли в систему.", @@ -611,10 +622,10 @@ }, "errors": { "password": { - "incorrect": "" + "incorrect": "Предоставленный вами пароль не соответствует нашим записям." } }, - "welcome": "" + "welcome": "Добро пожаловать обратно в Checkmate!" }, "registration": { "heading": { @@ -641,7 +652,7 @@ "toasts": { "success": "Добро пожаловать! Ваша учетная запись была создана успешно." }, - "welcome": "" + "welcome": "Добро пожаловать в Checkmate!" }, "forgotPassword": { "heading": "Забыли пароль?", @@ -652,446 +663,471 @@ "stepFour": "Ваш пароль был успешно сброшен. Нажмите ниже, чтобы волшебным образом войти в систему." }, "buttons": { - "openEmail": "", + "openEmail": "Открыть Gmail", "resetPassword": "Сбросить пароль" }, "imageAlts": { - "passwordKey": "", - "email": "", - "lock": "", - "passwordConfirm": "" + "passwordKey": "Password key icon", + "email": "Email icon", + "lock": "Lock icon", + "passwordConfirm": "Password confirm icon" }, "links": { - "login": "", - "resend": "" + "login": "Вернуться к Входу", + "resend": "Не получили письмо? Нажмите, чтобы отправить повторно" }, "toasts": { - "sent": "", - "emailNotFound": "", - "redirect": "", - "success": "", - "error": "" + "sent": "Инструкция отправлена на .", + "emailNotFound": "Email не найден.", + "redirect": "Перенаправление через ...", + "success": "Ваш пароль успешно сброшен.", + "error": "Не удалось сбросить пароль. Повторите попытку позже или обратитесь в службу поддержки." } } }, "errorPages": { "serverUnreachable": { "toasts": { - "reconnected": "", - "stillUnreachable": "" + "reconnected": "Успешное повторное подключение к серверу.", + "stillUnreachable": "Сервер по-прежнему недоступен. Повторите попытку позже." }, - "alertBox": "", - "description": "", + "alertBox": "Ошибка подключения к серверу", + "description": "Не удаётся подключиться к серверу. Проверьте подключение к Интернету или конфигурацию развёртывания, если проблема не устранена.", "retryButton": { - "default": "", - "processing": "" + "default": "Повторите попытку подключения", + "processing": "Подключение..." } } }, "createNotifications": { - "title": "", + "title": "Создать канал уведомлений", "nameSettings": { - "title": "", - "description": "", - "nameLabel": "", - "namePlaceholder": "" + "title": "Имя", + "description": "Описательное имя для вашей интеграции.", + "nameLabel": "Имя", + "namePlaceholder": "например, уведомления Slack" }, "typeSettings": { - "title": "", - "description": "", - "typeLabel": "" + "title": "Тип", + "description": "Выберите тип канала уведомлений, который вы хотите создать.", + "typeLabel": "Тип" }, "emailSettings": { - "title": "", - "description": "", - "emailLabel": "", - "emailPlaceholder": "" + "title": "Электронная почта", + "description": "Адреса электронной почты получателей.", + "emailLabel": "Адрес электронной почты", + "emailPlaceholder": "например, john@example.com" }, "slackSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" + "title": "Slack", + "description": "Настройте свой вебхук Slack здесь", + "webhookLabel": "Slack вебхук URL", + "webhookPlaceholder": "https://hooks.slack.com/services/..." }, "pagerdutySettings": { - "title": "", - "description": "", - "integrationKeyLabel": "", - "integrationKeyPlaceholder": "" + "title": "PagerDuty", + "description": "Настройте интеграцию PagerDuty здесь", + "integrationKeyLabel": "Интеграционный ключ", + "integrationKeyPlaceholder": "1234567890" }, "discordSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" + "title": "Discord", + "description": "Настройте вебхук Discord здесь", + "webhookLabel": "Discord вебхук URL", + "webhookPlaceholder": "https://your-server.com/webhook" }, "webhookSettings": { - "title": "", - "description": "", - "webhookLabel": "", - "webhookPlaceholder": "" + "title": "Вебхук", + "description": "Настройте свой вебхук здесь", + "webhookLabel": "URL-адрес веб-перехватчика", + "webhookPlaceholder": "https://your-server.com/webhook" }, - "testNotification": "", - "dialogDeleteTitle": "", - "dialogDeleteConfirm": "" + "testNotification": "Тестовое уведомление", + "dialogDeleteTitle": "Вы уверены, что хотите удалить это уведомление?", + "dialogDeleteConfirm": "Удалить" }, "notificationConfig": { - "title": "", - "description": "" + "title": "Уведомления", + "description": "Выберите каналы уведомлений, которые вы хотите использовать" }, "monitorStatus": { - "checkingEvery": "", - "withCaptureAgent": "", - "up": "", - "down": "", - "paused": "" + "checkingEvery": "Проверка каждый {{interval}}", + "withCaptureAgent": "с агентом захвата {{version}}", + "up": "вверх", + "down": "вниз", + "paused": "приостановлено" }, - "advancedMatching": "", - "sendTestNotifications": "", - "selectAll": "", - "showAdminLoginLink": "", + "advancedMatching": "Расширенное сопоставление", + "sendTestNotifications": "Отправить тестовое уведомление", + "selectAll": "Выбрать все", + "showAdminLoginLink": "Показывать ссылку «Администратор? Войти здесь» на странице статуса", "logsPage": { - "title": "", - "description": "", + "title": "Логи", + "description": "Системные логи - последние 1000 строк", "tabs": { - "queue": "", - "logs": "", - "diagnostics": "" + "queue": "Очередь заданий", + "logs": "Серверные логи", + "diagnostics": "Диагностика" }, "toast": { - "fetchLogsSuccess": "" + "fetchLogsSuccess": "Логи успешно получены" }, "logLevelSelect": { - "title": "", + "title": "Уровень логов", "values": { - "all": "", - "info": "", - "warn": "", - "error": "", - "debug": "" + "all": "Все", + "info": "Информация", + "warn": "Предупреждение", + "error": "Ошибка", + "debug": "Дебаг" } } }, "queuePage": { - "title": "", - "refreshButton": "", - "flushButton": "", + "title": "Очередь", + "refreshButton": "Обновить", + "flushButton": "Очистить очередь", "jobTable": { - "title": "", - "idHeader": "", - "urlHeader": "", - "typeHeader": "", - "activeHeader": "", - "lockedAtHeader": "", - "runCountHeader": "", - "failCountHeader": "", - "lastRunHeader": "", - "lastFinishedAtHeader": "", - "lastRunTookHeader": "", - "intervalHeader": "" + "title": "Задачи в настоящее время в очереди", + "idHeader": "ID монитора", + "urlHeader": "URL", + "typeHeader": "Тип", + "activeHeader": "Активный", + "lockedAtHeader": "Заблокировано на", + "runCountHeader": "Количество запусков", + "failCountHeader": "Количество неудач", + "lastRunHeader": "Последний запуск в", + "lastFinishedAtHeader": "Последний раз закончил в", + "lastRunTookHeader": "Последний забег занял", + "intervalHeader": "Интервал" }, "metricsTable": { - "title": "", - "metricHeader": "", - "valueHeader": "" + "title": "Метрики очереди", + "metricHeader": "Метрическая", + "valueHeader": "Ценить" }, "failedJobTable": { - "title": "", - "monitorIdHeader": "", - "monitorUrlHeader": "", - "failCountHeader": "", - "failedAtHeader": "", - "failReasonHeader": "" + "title": "Неудачные задачи", + "monitorIdHeader": "ID монитора", + "monitorUrlHeader": "URL монитора", + "failCountHeader": "Количество падений", + "failedAtHeader": "Последнее падение в", + "failReasonHeader": "Причина падения" } }, "export": { - "title": "", - "success": "", - "failed": "" + "title": "Экспорт мониторов", + "success": "Мониторы успешно экспортированы!", + "failed": "Не удалось экспортировать мониторы" }, "monitorActions": { - "title": "", - "import": "", - "export": "", - "deleteSuccess": "", - "deleteFailed": "", - "details": "" + "title": "Экспорт/Импорт", + "import": "Импортные мониторы", + "export": "Экспорт мониторов", + "deleteSuccess": "Монитор успешно удален", + "deleteFailed": "Не удалось удалить монитор", + "details": "Подробности" }, "settingsPage": { "aboutSettings": { - "labelDevelopedBy": "", - "labelVersion": "", - "title": "" + "labelDevelopedBy": "Разработано Bluewave Labs", + "labelVersion": "Версия", + "title": "Об" }, "demoMonitorsSettings": { - "buttonAddMonitors": "", - "description": "", - "title": "" + "buttonAddMonitors": "Добавить демо мониторы", + "description": "Добавьте образцы мониторов для демонстрации.", + "title": "Демо мониторы" }, "emailSettings": { - "buttonSendTestEmail": "", - "description": "", - "descriptionTransport": "", - "labelAddress": "", - "labelConnectionHost": "", - "labelHost": "", - "labelIgnoreTLS": "", - "labelPassword": "", - "labelPasswordSet": "", - "labelPool": "", - "labelPort": "", - "labelRejectUnauthorized": "", - "labelRequireTLS": "", - "labelSecure": "", - "labelTLSServername": "", - "labelUser": "", - "linkTransport": "", - "placeholderUser": "", - "title": "", - "toastEmailRequiredFieldsError": "" + "buttonSendTestEmail": "Отправить тестовое письмо", + "description": "Настройте параметры электронной почты в вашей системе. Она используется для отправки уведомлений и оповещений.", + "descriptionTransport": "Это создает SMTP-транспорт для NodeMailer.", + "labelAddress": "Адрес электронной почты — используется для аутентификации.", + "labelConnectionHost": "Хост подключения электронной почты — имя хоста для использования в приветствии HELO/EHLO.", + "labelHost": "Хост электронной почты — имя хоста или IP-адрес для подключения.", + "labelIgnoreTLS": "Отключить STARTTLS: не использовать TLS, даже если сервер его поддерживает.", + "labelPassword": "Пароль электронной почты - Пароль для аутентификации", + "labelPasswordSet": "Пароль установлен. Нажмите «Сброс», чтобы изменить его.", + "labelPool": "Включить пул соединений: повторно использовать существующие соединения для повышения производительности.", + "labelPort": "Порт электронной почты — порт для подключения", + "labelRejectUnauthorized": "Отклонять недействительные сертификаты: отклонять соединения с самоподписанными или ненадежными сертификатами.", + "labelRequireTLS": "Принудительный STARTTLS: требуется обновление TLS, выдается ошибка, если не поддерживается", + "labelSecure": "Использовать SSL (рекомендуется): зашифровать соединение с помощью SSL/TLS.", + "labelTLSServername": "Имя сервера TLS — необязательное имя хоста для проверки TLS, если хост — это IP-адрес.", + "labelUser": "Пользователь электронной почты — имя пользователя для аутентификации, переопределяет адрес электронной почты, если указан", + "linkTransport": "Технические характеристики смотрите здесь.", + "placeholderUser": "Оставьте пустым, если не требуется", + "title": "Электронная почта", + "toastEmailRequiredFieldsError": "Требуется адрес электронной почты, хост, порт и пароль" }, "pageSpeedSettings": { - "description": "", - "labelApiKeySet": "", - "labelApiKey": "", - "title": "" + "description": "Введите ключ API Google PageSpeed, чтобы включить мониторинг Google PageSpeed. Нажмите «Сбросить», чтобы обновить ключ.", + "labelApiKeySet": "API-ключ установлен. Нажмите «Сброс», чтобы изменить его.", + "labelApiKey": "API-ключ PageSpeed", + "title": "API-ключ Google PageSpeed" }, - "saveButtonLabel": "", + "saveButtonLabel": "Сохранить", "statsSettings": { - "clearAllStatsButton": "", - "clearAllStatsDescription": "", - "clearAllStatsDialogConfirm": "", - "clearAllStatsDialogDescription": "", - "clearAllStatsDialogTitle": "", - "description": "", - "labelTTL": "", - "labelTTLOptional": "", - "title": "" + "clearAllStatsButton": "Очистить всю статистику", + "clearAllStatsDescription": "Очистить всю статистику. Это необратимо.", + "clearAllStatsDialogConfirm": "Да, очистить всю статистику", + "clearAllStatsDialogDescription": "После удаления история и статистика мониторинга не могут быть восстановлены.", + "clearAllStatsDialogTitle": "Вы хотите очистить всю статистику?", + "description": "Укажите, как долго вы хотите хранить данные. Вы также можете удалить все существующие данные.", + "labelTTL": "Дни, за которые вы хотите посмотреть историю.", + "labelTTLOptional": "0 для бесконечности", + "title": "История монитора" }, "systemResetSettings": { - "buttonRemoveAllMonitors": "", - "description": "", - "dialogConfirm": "", - "dialogDescription": "", - "dialogTitle": "", - "title": "" + "buttonRemoveAllMonitors": "Удалить все мониторы", + "description": "Удалите все мониторы из вашей системы.", + "dialogConfirm": "Да, удалить все мониторы", + "dialogDescription": "После удаления, мониторы нельзя будет воостановить.", + "dialogTitle": "Вы хотите удалить все мониторы?", + "title": "Сброс системы" }, "timezoneSettings": { - "description": "", - "label": "", - "title": "" + "description": "Выберите часовой пояс, используемый для отображения даты и времени в приложении.", + "label": "Отображение часового пояса", + "title": "Отображение часового пояса" }, - "title": "", + "title": "Настройки", "uiSettings": { - "description": "", - "labelLanguage": "", - "labelTheme": "", - "title": "" + "description": "Переключайтесь между светлым и темным режимами или меняйте язык пользовательского интерфейса.", + "labelLanguage": "Язык", + "labelTheme": "Режим темы", + "title": "Появление" }, "urlSettings": { - "description": "", - "label": "", - "selectDisabled": "", - "selectEnabled": "", - "title": "" + "description": "Отображение IP-адреса или URL-адреса монитора на общедоступной странице состояния. Если эта опция отключена, для защиты конфиденциальной информации будет отображаться только имя монитора.", + "label": "Отображать IP/URL на странице статуса", + "selectDisabled": "Выключено", + "selectEnabled": "Включено", + "title": "Мониторинг IP/URL на странице статуса" }, "globalThresholds": { - "title": "", - "description": "" + "title": "Глобальные пороги", + "description": "Настройте глобальные пороговые значения для процессора, памяти, диска и температуры. Если значение указано, оно будет автоматически включено для мониторинга." } }, "statusPageCreate": { - "buttonSave": "" + "buttonSave": "Сохранить" }, - "incidentsOptionsHeaderFilterResolved": "", - "settingsSave": "", - "statusPageCreateAppearanceTitle": "", - "confirmPassword": "", + "incidentsOptionsHeaderFilterResolved": "Решено", + "settingsSave": "Сохранить", + "statusPageCreateAppearanceTitle": "Внешний вид", + "confirmPassword": "Подтвердите пароль", "monitorHooks": { - "failureAddDemoMonitors": "", - "successAddDemoMonitors": "" + "failureAddDemoMonitors": "Не удалось добавить демонстрационные мониторы.", + "successAddDemoMonitors": "Демонстрационные мониторы успешно добавлены" }, - "settingsAppearance": "", - "settingsDisplayTimezone": "", - "settingsGeneralSettings": "", - "incidentsOptionsHeaderTotalIncidents": "", + "settingsAppearance": "Внешний вид", + "settingsDisplayTimezone": "Отображение часового пояса", + "settingsGeneralSettings": "Общие настройки", + "incidentsOptionsHeaderTotalIncidents": "Всего инцидентов", "statusPage": { - "deleteSuccess": "", - "deleteFailed": "", - "createSuccess": "", - "updateSuccess": "", - "generalSettings": "", - "contents": "", + "deleteSuccess": "Страница статуса успешно удалена", + "deleteFailed": "Не удалось удалить страницу статуса", + "createSuccess": "Страница статуса успешно создана", + "updateSuccess": "Страница статуса успешно обновлена", + "generalSettings": "Общие настройки", + "contents": "Содержание", "fallback": { - "checks": [""], - "title": "", - "actionButton": "" + "checks": [ + "Мониторинг и отображение состояния ваших сервисов в режиме реального времени", + "Отслеживайте состояние нескольких сервисов и делитесь их статусом", + "Дайте пользователям информацию о сбоях и производительности" + ], + "title": "Страница статуса используется для:", + "actionButton": "Давайте создадим вашу первую страницу статуса!" } }, - "testNotificationsDisabled": "", - "incidentsTableResolvedAt": "", - "incidentsTableActionResolve": "", + "testNotificationsDisabled": "Для этого монитора не настроены уведомления. Вам нужно добавить их, нажав кнопку «Настроить».", + "incidentsTableResolvedAt": "Решено в", + "incidentsTableActionResolve": "Решено", "checkHooks": { - "failureResolveOne": "", - "failureResolveAll": "", - "failureResolveMonitor": "" + "failureResolveOne": "Не удалось разрешить инцидент.", + "failureResolveAll": "Не удалось разрешить все инциденты.", + "failureResolveMonitor": "Не удалось устранить инциденты с монитором." }, - "checkFormError": "", + "checkFormError": "Пожалуйста, проверьте форму на наличие ошибок.", "diagnosticsPage": { - "diagnosticDescription": "", - "statsDescription": "", + "diagnosticDescription": "Системная диагностика", + "statsDescription": "Системная статистика", "gauges": { - "heapAllocationTitle": "", - "heapAllocationSubtitle": "", - "heapUsageTitle": "", - "heapUsageSubtitle": "", - "heapUtilizationTitle": "", - "heapUtilizationSubtitle": "", - "instantCpuUsageTitle": "", - "instantCpuUsageSubtitle": "" + "heapAllocationTitle": "Использовано кучи", + "heapAllocationSubtitle": "% доступной памяти", + "heapUsageTitle": "Выделено памяти в куче", + "heapUsageSubtitle": "% доступно памяти", + "heapUtilizationTitle": "Использовано кучи", + "heapUtilizationSubtitle": "% от доступного", + "instantCpuUsageTitle": "Загруженность CPU", + "instantCpuUsageSubtitle": "% загрузка CPU за 1s" }, "stats": { - "eventLoopDelayTitle": "", - "uptimeTitle": "", - "usedHeapSizeTitle": "", - "totalHeapSizeTitle": "", - "osMemoryLimitTitle": "" + "eventLoopDelayTitle": "Задержка цикла событий", + "uptimeTitle": "Аптайм", + "usedHeapSizeTitle": "Размер используемой кучи", + "totalHeapSizeTitle": "Общий размер кучи", + "osMemoryLimitTitle": "Ограничение памяти ОS" } }, - "pageSpeedLighthouseAPI": "", + "pageSpeedLighthouseAPI": "Используйте API Lighthouse PageSpeed для мониторинга вашего сайта", "time": { - "threeMinutes": "", - "fiveMinutes": "", - "tenMinutes": "", - "twentyMinutes": "", - "oneHour": "", - "oneDay": "", - "oneWeek": "", - "fourMinutes": "", - "oneMinute": "", - "twoMinutes": "", - "fifteenSeconds": "", - "thirtySeconds": "" + "threeMinutes": "3 минуты", + "fiveMinutes": "5 минут", + "tenMinutes": "10 минут", + "twentyMinutes": "20 минут", + "oneHour": "1 час", + "oneDay": "1 день", + "oneWeek": "1 неделя", + "fourMinutes": "4 минуты", + "oneMinute": "1 минута", + "twoMinutes": "2 минуты", + "fifteenSeconds": "15 секунд", + "thirtySeconds": "30 секунд" }, "general": { - "noOptionsFound": "" + "noOptionsFound": "{{unit}} не найдено" }, "infrastructureMonitor": { "fallback": { - "checks": [""], - "title": "", - "actionButton": "" + "checks": [ + "Отслеживайте производительность серверов", + "Выявляйте узкие места и оптимизируйте использование", + "Обеспечивайте надежность с помощью мониторинга в режиме реального времени" + ], + "title": "Монитор инфраструктуры используется для:", + "actionButton": "Давайте создадим ваш первый монитор инфраструктуры!" } }, "maintenanceWindow": { "fallback": { - "checks": [""], - "title": "", - "actionButton": "" + "checks": [ + "Отметьте периоды технического обслуживания.", + "Устраните любые недопонимания.", + "Прекратите отправку оповещений в периоды технического обслуживания." + ], + "title": "Период технического обслуживания используется для:", + "actionButton": "Давайте создадим ваше первое окно технического обслуживания!" } }, "pageSpeed": { "fallback": { - "checks": [""], - "title": "", - "actionButton": "" + "checks": [ + "Отчет об удобстве использования страницы", + "Помощь в анализе скорости загрузки веб-страницы", + "Предложение о том, как можно улучшить страницу" + ], + "title": "Монитор PageSpeed используется для:", + "actionButton": "Давайте создадим ваш первый монитор PageSpeed!" } }, "uptimeMonitor": { "fallback": { - "checks": [""], - "title": "", - "actionButton": "" + "checks": [ + "Проверьте, доступны ли веб-сайты или серверы и отвечают ли они", + "Оповещайте команды о сбоях или проблемах с производительностью", + "Мониторинг конечных точек HTTP, пингов, контейнеров и портов", + "Отслеживайте динамику времени безотказной работы и надежности" + ], + "title": "Монитор работоспособности используется для:", + "actionButton": "Давайте создадим ваш первый монитор времени безотказной работы!" } }, "editUserPage": { "form": { - "email": "", - "firstName": "", - "lastName": "", - "role": "", - "save": "" + "email": "Электронная почта", + "firstName": "Имя", + "lastName": "Фамилия", + "role": "Роли", + "save": "Сохранить" }, "table": { - "actionHeader": "", - "roleHeader": "" + "actionHeader": "Действие", + "roleHeader": "Роль" }, - "title": "", + "title": "Редактировать пользователя", "toast": { - "successUserUpdate": "", - "validationErrors": "" + "successUserUpdate": "Пользователь успешно обновлен", + "validationErrors": "Ошибки проверки" } }, - "incidentsPageActionResolveMonitor": "", - "incidentsPageActionResolveAll": "", + "incidentsPageActionResolveMonitor": "Разрешение инцидентов мониторинга", + "incidentsPageActionResolveAll": "Решить все инциденты", "matchMethodOptions": { - "equal": "", - "equalPlaceholder": "", - "include": "", - "includePlaceholder": "", - "regex": "", - "regexPlaceholder": "", - "text": "" + "equal": "Равный", + "equalPlaceholder": "успех", + "include": "Включить", + "includePlaceholder": "хорошо", + "regex": "Regex", + "regexPlaceholder": "^(success|ok)$", + "text": "Метод сопоставления" }, "monitorType": { "docker": { - "label": "", - "namePlaceholder": "", - "placeholder": "" + "label": "Имя/ID контейнера", + "namePlaceholder": "Мой контейнер", + "placeholder": "my-app или abcd1234" }, "http": { - "label": "", - "namePlaceholder": "", - "placeholder": "" + "label": "URL для мониторинга", + "namePlaceholder": "Google", + "placeholder": "google.com" }, "ping": { - "label": "", - "namePlaceholder": "", - "placeholder": "" + "label": "IP-адрес для мониторинга", + "namePlaceholder": "Google", + "placeholder": "1.1.1.1" }, "port": { - "label": "", - "namePlaceholder": "", - "placeholder": "" + "label": "URL для мониторинга", + "namePlaceholder": "Локальный хост:5173", + "placeholder": "локальный хост" }, "game": { - "label": "", - "namePlaceholder": "", - "placeholder": "" + "label": "URL для мониторинга", + "namePlaceholder": "localhost:5173", + "placeholder": "localhost" } }, "uptimeAdvancedMatching": { - "jsonPath": "" + "jsonPath": "путь к JSON" }, - "bytesPerSecond": "", - "bytesReceived": "", - "bytesSent": "", - "chooseGame": "", + "bytesPerSecond": "Байты в секунду", + "bytesReceived": "Получено байтов", + "bytesSent": "Отправлено байтов", + "chooseGame": "Выбрать игру", "createMonitorPage": { - "incidentConfigDescription": "", - "incidentConfigStatusWindowLabel": "", - "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigDescription": "Мониторинг использует скользящее окно, чтобы понять, когда ресурс недоступен. Статус изменится, если нужный процент проверок покажет сбой.", + "incidentConfigStatusWindowLabel": "Сколько проверок должно быть в скользящем окне?", + "incidentConfigStatusWindowThresholdLabel": "Какой процент проверок в скользящем окне завершается неудачей/успешностью до изменения статуса монитора?", + "incidentConfigTitle": "Инциденты", + "incidentConfigDescriptionV2": "Количество последовательных проверок, необходимых для изменения статуса монитора. Максимум 25.", + "incidentConfigStatusCheckNumber": "Сколько проверок перед сменой статуса?", + "intervalTitle": "Интервал проверки", + "intervalDescription": "Как часто следует проверять монитор." }, - "dataRate": "", - "dataReceived": "", - "dataSent": "", - "details": "", - "drops": "", - "errors": "", - "errorsIn": "", - "errorsOut": "", - "gameServerMonitoring": "", - "gameServerMonitoringDescription": "", - "network": "", - "networkDrops": "", - "networkErrors": "", - "networkInterface": "", - "noNetworkStatsAvailable": "", - "packetsPerSecond": "", - "packetsReceived": "", - "packetsReceivedRate": "", - "packetsSent": "", - "rate": "", - "selectInterface": "" + "dataRate": "Скорость передачи данных", + "dataReceived": "Данные получены", + "dataSent": "Данные отправлены", + "details": "Подробности", + "drops": "Падений", + "errors": "Ошибки", + "errorsIn": "Ошибки в", + "errorsOut": "Ошибки вне", + "gameServerMonitoring": "Мониторинг игрового сервера", + "gameServerMonitoringDescription": "Проверьте, запущен ли ваш игровой сервер.", + "network": "Сеть", + "networkDrops": "Сетевые падения", + "networkErrors": "Ошибки сети", + "networkInterface": "Сетевой интерфейс", + "noNetworkStatsAvailable": "Статистика сети отсутствует.", + "packetsPerSecond": "Пакетов в секунду", + "packetsReceived": "Получено пакетов", + "packetsReceivedRate": "Скорость получения пакетов", + "packetsSent": "Отправлено пакетов", + "rate": "коэффициент", + "selectInterface": "Выберите интерфейс" } diff --git a/client/src/locales/th.json b/client/src/locales/th.json index c3f571b6b..c32849afe 100644 --- a/client/src/locales/th.json +++ b/client/src/locales/th.json @@ -425,7 +425,13 @@ } } }, - "role": "บทบาท" + "role": "บทบาท", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "หยุดชั่วคราว", @@ -487,7 +493,8 @@ "ping": "กรอกที่อยู่ IP หรือชื่อโฮสต์ที่ต้องการ ping (เช่น 192.168.1.100 หรือ example.com) และตั้งชื่อที่ชัดเจนให้แสดงบนแดชบอร์ด", "docker": "กรอกชื่อหรือ ID ของคอนเทนเนอร์ Docker คุณสามารถใช้ชื่อคอนเทนเนอร์ (เช่น my-app) หรือ ID ของคอนเทนเนอร์ (ID เต็ม 64 ตัวอักษร หรือ ID สั้น)", "port": "กรอก URL หรือ IP ของเซิร์ฟเวอร์ หมายเลขพอร์ต และตั้งชื่อที่ชัดเจนให้แสดงบนแดชบอร์ด", - "game": "กรอกที่อยู่ IP หรือชื่อโฮสต์และหมายเลขพอร์ตเพื่อทำการ ping (เช่น 192.168.1.100 หรือ example.com) และเลือกประเภทเกม" + "game": "กรอกที่อยู่ IP หรือชื่อโฮสต์และหมายเลขพอร์ตเพื่อทำการ ping (เช่น 192.168.1.100 หรือ example.com) และเลือกประเภทเกม", + "https": "" }, "common": { "appName": "Checkmate", @@ -1096,7 +1103,11 @@ "incidentConfigDescription": "การใช้หน้าต่างเลื่อนจะช่วยกำหนดเวลาที่ตัวตรวจสอบล้มเหลว สถานะของตัวตรวจสอบจะเปลี่ยนก็ต่อเมื่อเปอร์เซ็นต์ของการตรวจสอบในหน้าต่างเลื่อนตรงตามค่าที่กำหนด", "incidentConfigStatusWindowLabel": "ต้องมีการตรวจสอบกี่ครั้งในหน้าต่างเลื่อน?", "incidentConfigStatusWindowThresholdLabel": "ต้องการให้กี่เปอร์เซ็นต์ของการตรวจสอบในหน้าต่างเลื่อนล้มเหลว/สำเร็จ ก่อนที่จะเปลี่ยนสถานะของตัวตรวจสอบ?", - "incidentConfigTitle": "เหตุการณ์" + "incidentConfigTitle": "เหตุการณ์", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "อัตราการส่งข้อมูล", "dataReceived": "ข้อมูลที่รับ", diff --git a/client/src/locales/tr.json b/client/src/locales/tr.json index 0ab45ebe9..59f294533 100644 --- a/client/src/locales/tr.json +++ b/client/src/locales/tr.json @@ -421,7 +421,13 @@ } } }, - "role": "" + "role": "", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "Durduruldu", @@ -483,7 +489,8 @@ "ping": "", "docker": "", "port": "", - "game": "" + "game": "", + "https": "" }, "common": { "appName": "Checkmate", @@ -1071,7 +1078,11 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigTitle": "", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "", "dataReceived": "", diff --git a/client/src/locales/zh-CN.json b/client/src/locales/zh-CN.json index c2d1aa6fc..305cefe87 100644 --- a/client/src/locales/zh-CN.json +++ b/client/src/locales/zh-CN.json @@ -425,7 +425,13 @@ } } }, - "role": "角色" + "role": "角色", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "暂停", @@ -487,7 +493,8 @@ "ping": "输入要 ping 的 IP 地址或主机名(例如.. 192.168.1.100 或 example.com),并添加显示在仪表板上的清晰显示名称。", "docker": "码输入 Docker 容器名称或 ID。您可以使用容器名称(例如.. my-app)或容器 ID(完整的 64 字符 ID 或短 ID)。", "port": "输入服务器的 URL 或 IP、端口号和仪表板上显示的清晰显示名称。", - "game": "输入要 ping 的 IP 地址或主机名和端口号(例如.. 192.168.1.100 或 example.com)并选择游戏类型。" + "game": "输入要 ping 的 IP 地址或主机名和端口号(例如.. 192.168.1.100 或 example.com)并选择游戏类型。", + "https": "" }, "common": { "appName": "Checkmate", @@ -1088,7 +1095,11 @@ "incidentConfigDescription": "滑动窗口用于确定监视器何时发生故障。仅当滑动窗口中的检查百分比满足指定值时,监视器的状态才会改变。", "incidentConfigStatusWindowLabel": "在滑动窗口中应包含多少次检测?", "incidentConfigStatusWindowThresholdLabel": "在监视器状态改变之前,滑动窗口中的检查失败/成功的百分比是多少?", - "incidentConfigTitle": "事件" + "incidentConfigTitle": "事件", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "数据速率", "dataReceived": "已接收数据", diff --git a/client/src/locales/zh-TW.json b/client/src/locales/zh-TW.json index e57ebd487..e5ee58e58 100644 --- a/client/src/locales/zh-TW.json +++ b/client/src/locales/zh-TW.json @@ -425,7 +425,13 @@ } } }, - "role": "" + "role": "", + "changeTeamPassword": { + "changePasswordMenu": "", + "title": "", + "description": "", + "success": "" + } }, "monitorState": { "paused": "", @@ -487,7 +493,8 @@ "ping": "", "docker": "", "port": "", - "game": "" + "game": "", + "https": "" }, "common": { "appName": "Checkmate", @@ -1075,7 +1082,11 @@ "incidentConfigDescription": "", "incidentConfigStatusWindowLabel": "", "incidentConfigStatusWindowThresholdLabel": "", - "incidentConfigTitle": "" + "incidentConfigTitle": "", + "incidentConfigDescriptionV2": "", + "incidentConfigStatusCheckNumber": "", + "intervalTitle": "", + "intervalDescription": "" }, "dataRate": "", "dataReceived": "", From ce025df7b7fa24f8c95de7f518bc5466357256b9 Mon Sep 17 00:00:00 2001 From: Tudor Gradinaru Date: Thu, 23 Oct 2025 15:28:20 +0300 Subject: [PATCH 098/400] feat: Add monitor name to notification titles --- server/src/locales/en.json | 4 ++-- server/src/service/v1/infrastructure/notificationUtils.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/server/src/locales/en.json b/server/src/locales/en.json index 1345f84a8..373911a55 100755 --- a/server/src/locales/en.json +++ b/server/src/locales/en.json @@ -175,7 +175,7 @@ "checkmate": "Checkmate", "url": "URL", "unknown": "Unknown", - "uptimeAlert": "Uptime Alert: One of your monitors is back online", - "downtimeAlert": "Downtime Alert: One of your monitors went offline" + "uptimeAlert": "Uptime Alert: Your monitor {monitorName} is back online", + "downtimeAlert": "Downtime Alert: Your monitor {monitorName} went offline" } } diff --git a/server/src/service/v1/infrastructure/notificationUtils.js b/server/src/service/v1/infrastructure/notificationUtils.js index d6b92cc0e..879ad927c 100644 --- a/server/src/service/v1/infrastructure/notificationUtils.js +++ b/server/src/service/v1/infrastructure/notificationUtils.js @@ -74,7 +74,9 @@ class NotificationUtils { let discordMessageText = { embeds: [ { - title: status ? dn.uptimeAlert : dn.downtimeAlert, + title: status + ? dn.uptimeAlert.replace("{monitorName}", monitor?.name ?? dn.unknown) + : dn.downtimeAlert.replace("{monitorName}", monitor?.name ?? dn.unknown), color: status ? 5763719 : 15548997, fields: [ From 1fad85d48d3d27c1e238ab14ca11a930e0c1cd68 Mon Sep 17 00:00:00 2001 From: Tudor Gradinaru Date: Sat, 25 Oct 2025 18:31:01 +0300 Subject: [PATCH 099/400] feat: Add Matrix notification support Fixes #3034 --- .../Pages/v1/Notifications/create/index.jsx | 58 +++++++++++++++---- client/src/Pages/v1/Notifications/utils.js | 5 ++ client/src/Validation/validation.js | 34 ++++++++++- client/src/locales/en.json | 10 ++++ server/src/db/v1/models/Notification.js | 12 +++- .../v1/infrastructure/networkService.js | 36 ++++++++++++ .../notificationProviders/matrix.js | 34 +++++++++++ .../v1/infrastructure/notificationService.js | 9 +++ server/src/validation/joi.js | 36 +++++++++++- 9 files changed, 219 insertions(+), 15 deletions(-) create mode 100644 server/src/service/v1/infrastructure/notificationProviders/matrix.js diff --git a/client/src/Pages/v1/Notifications/create/index.jsx b/client/src/Pages/v1/Notifications/create/index.jsx index eb2b04aa6..b1c50493c 100644 --- a/client/src/Pages/v1/Notifications/create/index.jsx +++ b/client/src/Pages/v1/Notifications/create/index.jsx @@ -223,18 +223,54 @@ const CreateNotifications = () => { {t(DESCRIPTION_MAP[type])} - + {type === "matrix" ? ( + <> + + + + + ) : ( + + )} - - + {" "} Date: Mon, 27 Oct 2025 09:42:27 -0700 Subject: [PATCH 100/400] bump-scheduler --- server/package-lock.json | 2316 +++++++++++++++++++++----------------- server/package.json | 2 +- 2 files changed, 1268 insertions(+), 1050 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 3ea4269eb..8de5cfc0c 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -39,7 +39,7 @@ "ping": "0.4.4", "sharp": "0.33.5", "ssl-checker": "2.0.10", - "super-simple-scheduler": "1.4.1", + "super-simple-scheduler": "1.4.4", "swagger-ui-express": "5.0.1", "winston": "^3.13.0" }, @@ -60,6 +60,9 @@ "sinon": "19.0.2", "tsx": "4.20.5", "typescript": "5.9.2" + }, + "engines": { + "node": ">=20" } }, "node_modules/@asamuzakjp/css-color": { @@ -75,6 +78,34 @@ "lru-cache": "^10.4.3" } }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-6.7.3.tgz", + "integrity": "sha512-kiGFeY+Hxf5KbPpjRLf+ffWbkos1aGo8MBfd91oxS3O57RgU3XhZrt/6UzoVF9VMpWbC3v87SRc9jxGrc9qHtQ==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.1.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.2.2" + } + }, + "node_modules/@asamuzakjp/dom-selector/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "license": "MIT" + }, "node_modules/@aws-crypto/sha256-browser": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", @@ -211,51 +242,51 @@ } }, "node_modules/@aws-sdk/client-sesv2": { - "version": "3.895.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.895.0.tgz", - "integrity": "sha512-tYfgwoG4n8LEc0NHVriNNS+YqeDMJHHcUzJxJJ7Nols5Y3EJzv76QRvu2rdLa9XoxvTSFGXJSuqrd8s0ElbCQQ==", + "version": "3.917.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sesv2/-/client-sesv2-3.917.0.tgz", + "integrity": "sha512-Jwz+3rO9fc2H8JyF4ainL5x06tiQ0n4ewKIpdUeGIauKdndMItYP1aeM0Y8wJQPTF1XEpWFC36cTSDgk/eypxA==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.894.0", - "@aws-sdk/credential-provider-node": "3.895.0", - "@aws-sdk/middleware-host-header": "3.893.0", - "@aws-sdk/middleware-logger": "3.893.0", - "@aws-sdk/middleware-recursion-detection": "3.893.0", - "@aws-sdk/middleware-user-agent": "3.895.0", - "@aws-sdk/region-config-resolver": "3.893.0", - "@aws-sdk/signature-v4-multi-region": "3.894.0", - "@aws-sdk/types": "3.893.0", - "@aws-sdk/util-endpoints": "3.895.0", - "@aws-sdk/util-user-agent-browser": "3.893.0", - "@aws-sdk/util-user-agent-node": "3.895.0", - "@smithy/config-resolver": "^4.2.2", - "@smithy/core": "^3.11.1", - "@smithy/fetch-http-handler": "^5.2.1", - "@smithy/hash-node": "^4.1.1", - "@smithy/invalid-dependency": "^4.1.1", - "@smithy/middleware-content-length": "^4.1.1", - "@smithy/middleware-endpoint": "^4.2.3", - "@smithy/middleware-retry": "^4.2.4", - "@smithy/middleware-serde": "^4.1.1", - "@smithy/middleware-stack": "^4.1.1", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/node-http-handler": "^4.2.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/smithy-client": "^4.6.3", - "@smithy/types": "^4.5.0", - "@smithy/url-parser": "^4.1.1", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-body-length-browser": "^4.1.0", - "@smithy/util-body-length-node": "^4.1.0", - "@smithy/util-defaults-mode-browser": "^4.1.3", - "@smithy/util-defaults-mode-node": "^4.1.3", - "@smithy/util-endpoints": "^3.1.2", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-retry": "^4.1.2", - "@smithy/util-utf8": "^4.1.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/credential-provider-node": "3.917.0", + "@aws-sdk/middleware-host-header": "3.914.0", + "@aws-sdk/middleware-logger": "3.914.0", + "@aws-sdk/middleware-recursion-detection": "3.914.0", + "@aws-sdk/middleware-user-agent": "3.916.0", + "@aws-sdk/region-config-resolver": "3.914.0", + "@aws-sdk/signature-v4-multi-region": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-endpoints": "3.916.0", + "@aws-sdk/util-user-agent-browser": "3.914.0", + "@aws-sdk/util-user-agent-node": "3.916.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/core": "^3.17.1", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/hash-node": "^4.2.3", + "@smithy/invalid-dependency": "^4.2.3", + "@smithy/middleware-content-length": "^4.2.3", + "@smithy/middleware-endpoint": "^4.3.5", + "@smithy/middleware-retry": "^4.4.5", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/middleware-stack": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.4", + "@smithy/util-defaults-mode-node": "^4.2.6", + "@smithy/util-endpoints": "^3.2.3", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-retry": "^4.2.3", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -263,49 +294,49 @@ } }, "node_modules/@aws-sdk/client-sso": { - "version": "3.895.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.895.0.tgz", - "integrity": "sha512-AQHk6iJrwce/NwZa5/Njy0ZGoHdxWCajkgufhXk53L0kRiC3vUPPWEV1m1F3etQWhaUsatcO2xtRuKvLpe4zgA==", + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.916.0.tgz", + "integrity": "sha512-Eu4PtEUL1MyRvboQnoq5YKg0Z9vAni3ccebykJy615xokVZUdA3di2YxHM/hykDQX7lcUC62q9fVIvh0+UNk/w==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.894.0", - "@aws-sdk/middleware-host-header": "3.893.0", - "@aws-sdk/middleware-logger": "3.893.0", - "@aws-sdk/middleware-recursion-detection": "3.893.0", - "@aws-sdk/middleware-user-agent": "3.895.0", - "@aws-sdk/region-config-resolver": "3.893.0", - "@aws-sdk/types": "3.893.0", - "@aws-sdk/util-endpoints": "3.895.0", - "@aws-sdk/util-user-agent-browser": "3.893.0", - "@aws-sdk/util-user-agent-node": "3.895.0", - "@smithy/config-resolver": "^4.2.2", - "@smithy/core": "^3.11.1", - "@smithy/fetch-http-handler": "^5.2.1", - "@smithy/hash-node": "^4.1.1", - "@smithy/invalid-dependency": "^4.1.1", - "@smithy/middleware-content-length": "^4.1.1", - "@smithy/middleware-endpoint": "^4.2.3", - "@smithy/middleware-retry": "^4.2.4", - "@smithy/middleware-serde": "^4.1.1", - "@smithy/middleware-stack": "^4.1.1", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/node-http-handler": "^4.2.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/smithy-client": "^4.6.3", - "@smithy/types": "^4.5.0", - "@smithy/url-parser": "^4.1.1", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-body-length-browser": "^4.1.0", - "@smithy/util-body-length-node": "^4.1.0", - "@smithy/util-defaults-mode-browser": "^4.1.3", - "@smithy/util-defaults-mode-node": "^4.1.3", - "@smithy/util-endpoints": "^3.1.2", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-retry": "^4.1.2", - "@smithy/util-utf8": "^4.1.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/middleware-host-header": "3.914.0", + "@aws-sdk/middleware-logger": "3.914.0", + "@aws-sdk/middleware-recursion-detection": "3.914.0", + "@aws-sdk/middleware-user-agent": "3.916.0", + "@aws-sdk/region-config-resolver": "3.914.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-endpoints": "3.916.0", + "@aws-sdk/util-user-agent-browser": "3.914.0", + "@aws-sdk/util-user-agent-node": "3.916.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/core": "^3.17.1", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/hash-node": "^4.2.3", + "@smithy/invalid-dependency": "^4.2.3", + "@smithy/middleware-content-length": "^4.2.3", + "@smithy/middleware-endpoint": "^4.3.5", + "@smithy/middleware-retry": "^4.4.5", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/middleware-stack": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.4", + "@smithy/util-defaults-mode-node": "^4.2.6", + "@smithy/util-endpoints": "^3.2.3", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-retry": "^4.2.3", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -313,25 +344,24 @@ } }, "node_modules/@aws-sdk/core": { - "version": "3.894.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.894.0.tgz", - "integrity": "sha512-7zbO31NV2FaocmMtWOg/fuTk3PC2Ji2AC0Fi2KqrppEDIcwLlTTuT9w/rdu/93Pz+wyUhCxWnDc0tPbwtCLs+A==", + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.916.0.tgz", + "integrity": "sha512-1JHE5s6MD5PKGovmx/F1e01hUbds/1y3X8rD+Gvi/gWVfdg5noO7ZCerpRsWgfzgvCMZC9VicopBqNHCKLykZA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.893.0", - "@aws-sdk/xml-builder": "3.894.0", - "@smithy/core": "^3.11.1", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/property-provider": "^4.1.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/signature-v4": "^5.2.1", - "@smithy/smithy-client": "^4.6.3", - "@smithy/types": "^4.5.0", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-body-length-browser": "^4.1.0", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-utf8": "^4.1.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/xml-builder": "3.914.0", + "@smithy/core": "^3.17.1", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/signature-v4": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -339,16 +369,16 @@ } }, "node_modules/@aws-sdk/credential-provider-env": { - "version": "3.894.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.894.0.tgz", - "integrity": "sha512-2aiQJIRWOuROPPISKgzQnH/HqSfucdk5z5VMemVH3Mm2EYOrzBwmmiiFpmSMN3ST+sE8c7gusqycUchP+KfALQ==", + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.916.0.tgz", + "integrity": "sha512-3gDeqOXcBRXGHScc6xb7358Lyf64NRG2P08g6Bu5mv1Vbg9PKDyCAZvhKLkG7hkdfAM8Yc6UJNhbFxr1ud/tCQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.894.0", - "@aws-sdk/types": "3.893.0", - "@smithy/property-provider": "^4.1.1", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -356,21 +386,21 @@ } }, "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.894.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.894.0.tgz", - "integrity": "sha512-Z5QQpqFRflszrT+lUq6+ORuu4jRDcpgCUSoTtlhczidMqfdOSckKmK3chZEfmUUJPSwoFQZ7EiVTsX3c886fBg==", + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.916.0.tgz", + "integrity": "sha512-NmooA5Z4/kPFJdsyoJgDxuqXC1C6oPMmreJjbOPqcwo6E/h2jxaG8utlQFgXe5F9FeJsMx668dtxVxSYnAAqHQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.894.0", - "@aws-sdk/types": "3.893.0", - "@smithy/fetch-http-handler": "^5.2.1", - "@smithy/node-http-handler": "^4.2.1", - "@smithy/property-provider": "^4.1.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/smithy-client": "^4.6.3", - "@smithy/types": "^4.5.0", - "@smithy/util-stream": "^4.3.2", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/util-stream": "^4.5.4", "tslib": "^2.6.2" }, "engines": { @@ -378,24 +408,24 @@ } }, "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.895.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.895.0.tgz", - "integrity": "sha512-uIh7N4IN/yIk+qYMAkVpVkjhB90SGKSfaXEVcnmxzBDG6e5304HKT0esqoCVZvtFfLKasjm2TOpalM5l3fi/dA==", + "version": "3.917.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.917.0.tgz", + "integrity": "sha512-rvQ0QamLySRq+Okc0ZqFHZ3Fbvj3tYuWNIlzyEKklNmw5X5PM1idYKlOJflY2dvUGkIqY3lUC9SC2WL+1s7KIw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.894.0", - "@aws-sdk/credential-provider-env": "3.894.0", - "@aws-sdk/credential-provider-http": "3.894.0", - "@aws-sdk/credential-provider-process": "3.894.0", - "@aws-sdk/credential-provider-sso": "3.895.0", - "@aws-sdk/credential-provider-web-identity": "3.895.0", - "@aws-sdk/nested-clients": "3.895.0", - "@aws-sdk/types": "3.893.0", - "@smithy/credential-provider-imds": "^4.1.2", - "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.2.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/credential-provider-env": "3.916.0", + "@aws-sdk/credential-provider-http": "3.916.0", + "@aws-sdk/credential-provider-process": "3.916.0", + "@aws-sdk/credential-provider-sso": "3.916.0", + "@aws-sdk/credential-provider-web-identity": "3.917.0", + "@aws-sdk/nested-clients": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/credential-provider-imds": "^4.2.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -403,23 +433,23 @@ } }, "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.895.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.895.0.tgz", - "integrity": "sha512-7xsBCmkBUz+2sNqNsDJ1uyQsBvwhNFzwFt8wX39WrFJTpTQh3uNQ5g8QH21BbkKqIFKCLdvgHgwt3Ub5RGVuPA==", + "version": "3.917.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.917.0.tgz", + "integrity": "sha512-n7HUJ+TgU9wV/Z46yR1rqD9hUjfG50AKi+b5UXTlaDlVD8bckg40i77ROCllp53h32xQj/7H0yBIYyphwzLtmg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/credential-provider-env": "3.894.0", - "@aws-sdk/credential-provider-http": "3.894.0", - "@aws-sdk/credential-provider-ini": "3.895.0", - "@aws-sdk/credential-provider-process": "3.894.0", - "@aws-sdk/credential-provider-sso": "3.895.0", - "@aws-sdk/credential-provider-web-identity": "3.895.0", - "@aws-sdk/types": "3.893.0", - "@smithy/credential-provider-imds": "^4.1.2", - "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.2.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/credential-provider-env": "3.916.0", + "@aws-sdk/credential-provider-http": "3.916.0", + "@aws-sdk/credential-provider-ini": "3.917.0", + "@aws-sdk/credential-provider-process": "3.916.0", + "@aws-sdk/credential-provider-sso": "3.916.0", + "@aws-sdk/credential-provider-web-identity": "3.917.0", + "@aws-sdk/types": "3.914.0", + "@smithy/credential-provider-imds": "^4.2.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -427,17 +457,17 @@ } }, "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.894.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.894.0.tgz", - "integrity": "sha512-VU74GNsj+SsO+pl4d+JimlQ7+AcderZaC6bFndQssQdFZ5NRad8yFNz5Xbec8CPJr+z/VAwHib6431F5nYF46g==", + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.916.0.tgz", + "integrity": "sha512-SXDyDvpJ1+WbotZDLJW1lqP6gYGaXfZJrgFSXIuZjHb75fKeNRgPkQX/wZDdUvCwdrscvxmtyJorp2sVYkMcvA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.894.0", - "@aws-sdk/types": "3.893.0", - "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.2.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -445,19 +475,19 @@ } }, "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.895.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.895.0.tgz", - "integrity": "sha512-bZCcHUZGz+XlCaK0KEOHGHkMtlwIvnpxJvlZtSCVaBdX/IgouxaB42fxChflxSMRWF45ygdezfky4i17f6vC4w==", + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.916.0.tgz", + "integrity": "sha512-gu9D+c+U/Dp1AKBcVxYHNNoZF9uD4wjAKYCjgSN37j4tDsazwMEylbbZLuRNuxfbXtizbo4/TiaxBXDbWM7AkQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/client-sso": "3.895.0", - "@aws-sdk/core": "3.894.0", - "@aws-sdk/token-providers": "3.895.0", - "@aws-sdk/types": "3.893.0", - "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.2.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/client-sso": "3.916.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/token-providers": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -465,18 +495,18 @@ } }, "node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.895.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.895.0.tgz", - "integrity": "sha512-tKbXbOp2xrL02fxKvB7ko1E4Uvyy5TF9qi5pT2MVWNnfSsBlUM80aJ6tyUPKWXdUTdAlPrU3XcwgQl/DnnRa9A==", + "version": "3.917.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.917.0.tgz", + "integrity": "sha512-pZncQhFbwW04pB0jcD5OFv3x2gAddDYCVxyJVixgyhSw7bKCYxqu6ramfq1NxyVpmm+qsw+ijwi/3cCmhUHF/A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.894.0", - "@aws-sdk/nested-clients": "3.895.0", - "@aws-sdk/types": "3.893.0", - "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.2.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/nested-clients": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -484,15 +514,15 @@ } }, "node_modules/@aws-sdk/middleware-host-header": { - "version": "3.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.893.0.tgz", - "integrity": "sha512-qL5xYRt80ahDfj9nDYLhpCNkDinEXvjLe/Qen/Y/u12+djrR2MB4DRa6mzBCkLkdXDtf0WAoW2EZsNCfGrmOEQ==", + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.914.0.tgz", + "integrity": "sha512-7r9ToySQ15+iIgXMF/h616PcQStByylVkCshmQqcdeynD/lCn2l667ynckxW4+ql0Q+Bo/URljuhJRxVJzydNA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.893.0", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", + "@aws-sdk/types": "3.914.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -500,14 +530,14 @@ } }, "node_modules/@aws-sdk/middleware-logger": { - "version": "3.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.893.0.tgz", - "integrity": "sha512-ZqzMecjju5zkBquSIfVfCORI/3Mge21nUY4nWaGQy+NUXehqCGG4W7AiVpiHGOcY2cGJa7xeEkYcr2E2U9U0AA==", + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.914.0.tgz", + "integrity": "sha512-/gaW2VENS5vKvJbcE1umV4Ag3NuiVzpsANxtrqISxT3ovyro29o1RezW/Avz/6oJqjnmgz8soe9J1t65jJdiNg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.893.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -515,16 +545,16 @@ } }, "node_modules/@aws-sdk/middleware-recursion-detection": { - "version": "3.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.893.0.tgz", - "integrity": "sha512-H7Zotd9zUHQAr/wr3bcWHULYhEeoQrF54artgsoUGIf/9emv6LzY89QUccKIxYd6oHKNTrTyXm9F0ZZrzXNxlg==", + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.914.0.tgz", + "integrity": "sha512-yiAjQKs5S2JKYc+GrkvGMwkUvhepXDigEXpSJqUseR/IrqHhvGNuOxDxq+8LbDhM4ajEW81wkiBbU+Jl9G82yQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.893.0", + "@aws-sdk/types": "3.914.0", "@aws/lambda-invoke-store": "^0.0.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -532,25 +562,25 @@ } }, "node_modules/@aws-sdk/middleware-sdk-s3": { - "version": "3.894.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.894.0.tgz", - "integrity": "sha512-0C3lTVdTuv5CkJ4LulpA7FmGFSKrGUKxnFZ6+qGjYjNzbdiHXfq0TyEBiDmVqDkoV2k4AT2H/m0Xw//rTkcNEQ==", + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.916.0.tgz", + "integrity": "sha512-pjmzzjkEkpJObzmTthqJPq/P13KoNFuEi/x5PISlzJtHofCNcyXeVAQ90yvY2dQ6UXHf511Rh1/ytiKy2A8M0g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.894.0", - "@aws-sdk/types": "3.893.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", "@aws-sdk/util-arn-parser": "3.893.0", - "@smithy/core": "^3.11.1", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/protocol-http": "^5.2.1", - "@smithy/signature-v4": "^5.2.1", - "@smithy/smithy-client": "^4.6.3", - "@smithy/types": "^4.5.0", - "@smithy/util-config-provider": "^4.1.0", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-stream": "^4.3.2", - "@smithy/util-utf8": "^4.1.0", + "@smithy/core": "^3.17.1", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/signature-v4": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-stream": "^4.5.4", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -558,18 +588,18 @@ } }, "node_modules/@aws-sdk/middleware-user-agent": { - "version": "3.895.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.895.0.tgz", - "integrity": "sha512-JUqQW2RPp4I95wZ/Im9fTiaX3DF55oJgeoiNlLdHkQZPSNNS/pT1WMWMReSvJdcfSNU3xSUaLtI+h4mQjQUDbQ==", + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.916.0.tgz", + "integrity": "sha512-mzF5AdrpQXc2SOmAoaQeHpDFsK2GE6EGcEACeNuoESluPI2uYMpuuNMYrUufdnIAIyqgKlis0NVxiahA5jG42w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.894.0", - "@aws-sdk/types": "3.893.0", - "@aws-sdk/util-endpoints": "3.895.0", - "@smithy/core": "^3.11.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-endpoints": "3.916.0", + "@smithy/core": "^3.17.1", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -577,49 +607,49 @@ } }, "node_modules/@aws-sdk/nested-clients": { - "version": "3.895.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.895.0.tgz", - "integrity": "sha512-8w1ihfYgvds6kfal/qJXQQrHRsKYh2nujSyzWMo2TMKMze9WPZA93G4mRbRtKtbSuQ66mVWePH8Cksq35ABu2Q==", + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.916.0.tgz", + "integrity": "sha512-tgg8e8AnVAer0rcgeWucFJ/uNN67TbTiDHfD+zIOPKep0Z61mrHEoeT/X8WxGIOkEn4W6nMpmS4ii8P42rNtnA==", "dev": true, "license": "Apache-2.0", "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "3.894.0", - "@aws-sdk/middleware-host-header": "3.893.0", - "@aws-sdk/middleware-logger": "3.893.0", - "@aws-sdk/middleware-recursion-detection": "3.893.0", - "@aws-sdk/middleware-user-agent": "3.895.0", - "@aws-sdk/region-config-resolver": "3.893.0", - "@aws-sdk/types": "3.893.0", - "@aws-sdk/util-endpoints": "3.895.0", - "@aws-sdk/util-user-agent-browser": "3.893.0", - "@aws-sdk/util-user-agent-node": "3.895.0", - "@smithy/config-resolver": "^4.2.2", - "@smithy/core": "^3.11.1", - "@smithy/fetch-http-handler": "^5.2.1", - "@smithy/hash-node": "^4.1.1", - "@smithy/invalid-dependency": "^4.1.1", - "@smithy/middleware-content-length": "^4.1.1", - "@smithy/middleware-endpoint": "^4.2.3", - "@smithy/middleware-retry": "^4.2.4", - "@smithy/middleware-serde": "^4.1.1", - "@smithy/middleware-stack": "^4.1.1", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/node-http-handler": "^4.2.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/smithy-client": "^4.6.3", - "@smithy/types": "^4.5.0", - "@smithy/url-parser": "^4.1.1", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-body-length-browser": "^4.1.0", - "@smithy/util-body-length-node": "^4.1.0", - "@smithy/util-defaults-mode-browser": "^4.1.3", - "@smithy/util-defaults-mode-node": "^4.1.3", - "@smithy/util-endpoints": "^3.1.2", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-retry": "^4.1.2", - "@smithy/util-utf8": "^4.1.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/middleware-host-header": "3.914.0", + "@aws-sdk/middleware-logger": "3.914.0", + "@aws-sdk/middleware-recursion-detection": "3.914.0", + "@aws-sdk/middleware-user-agent": "3.916.0", + "@aws-sdk/region-config-resolver": "3.914.0", + "@aws-sdk/types": "3.914.0", + "@aws-sdk/util-endpoints": "3.916.0", + "@aws-sdk/util-user-agent-browser": "3.914.0", + "@aws-sdk/util-user-agent-node": "3.916.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/core": "^3.17.1", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/hash-node": "^4.2.3", + "@smithy/invalid-dependency": "^4.2.3", + "@smithy/middleware-content-length": "^4.2.3", + "@smithy/middleware-endpoint": "^4.3.5", + "@smithy/middleware-retry": "^4.4.5", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/middleware-stack": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-body-length-node": "^4.2.1", + "@smithy/util-defaults-mode-browser": "^4.3.4", + "@smithy/util-defaults-mode-node": "^4.2.6", + "@smithy/util-endpoints": "^3.2.3", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-retry": "^4.2.3", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -627,17 +657,15 @@ } }, "node_modules/@aws-sdk/region-config-resolver": { - "version": "3.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.893.0.tgz", - "integrity": "sha512-/cJvh3Zsa+Of0Zbg7vl9wp/kZtdb40yk/2+XcroAMVPO9hPvmS9r/UOm6tO7FeX4TtkRFwWaQJiTZTgSdsPY+Q==", + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.914.0.tgz", + "integrity": "sha512-KlmHhRbn1qdwXUdsdrJ7S/MAkkC1jLpQ11n+XvxUUUCGAJd1gjC7AjxPZUM7ieQ2zcb8bfEzIU7al+Q3ZT0u7Q==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.893.0", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/types": "^4.5.0", - "@smithy/util-config-provider": "^4.1.0", - "@smithy/util-middleware": "^4.1.1", + "@aws-sdk/types": "3.914.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -645,17 +673,17 @@ } }, "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.894.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.894.0.tgz", - "integrity": "sha512-Te5b3fSbatkZrh3eYNmpOadZFKsCLNSwiolQKQeEeKHxdnqORwYXa+0ypcTHle6ukic+tFRRd9n3NuMVo9uiVg==", + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.916.0.tgz", + "integrity": "sha512-fuzUMo6xU7e0NBzBA6TQ4FUf1gqNbg4woBSvYfxRRsIfKmSMn9/elXXn4sAE5UKvlwVQmYnb6p7dpVRPyFvnQA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-sdk-s3": "3.894.0", - "@aws-sdk/types": "3.893.0", - "@smithy/protocol-http": "^5.2.1", - "@smithy/signature-v4": "^5.2.1", - "@smithy/types": "^4.5.0", + "@aws-sdk/middleware-sdk-s3": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/signature-v4": "^5.3.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -663,18 +691,18 @@ } }, "node_modules/@aws-sdk/token-providers": { - "version": "3.895.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.895.0.tgz", - "integrity": "sha512-vJqrEHFFGRZ3ok5T+jII00sa2DQ3HdVkTBIfM0DcrcPssqDV18VKdA767qiBdIEN/cygjdBg8Ri/cuq6ER9BeQ==", + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.916.0.tgz", + "integrity": "sha512-13GGOEgq5etbXulFCmYqhWtpcEQ6WI6U53dvXbheW0guut8fDFJZmEv7tKMTJgiybxh7JHd0rWcL9JQND8DwoQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/core": "3.894.0", - "@aws-sdk/nested-clients": "3.895.0", - "@aws-sdk/types": "3.893.0", - "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.2.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/core": "3.916.0", + "@aws-sdk/nested-clients": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -682,13 +710,13 @@ } }, "node_modules/@aws-sdk/types": { - "version": "3.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.893.0.tgz", - "integrity": "sha512-Aht1nn5SnA0N+Tjv0dzhAY7CQbxVtmq1bBR6xI0MhG7p2XYVh1wXuKTzrldEvQWwA3odOYunAfT9aBiKZx9qIg==", + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.914.0.tgz", + "integrity": "sha512-kQWPsRDmom4yvAfyG6L1lMmlwnTzm1XwMHOU+G5IFlsP4YEaMtXidDzW/wiivY0QFrhfCz/4TVmu0a2aPU57ug==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -709,16 +737,16 @@ } }, "node_modules/@aws-sdk/util-endpoints": { - "version": "3.895.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.895.0.tgz", - "integrity": "sha512-MhxBvWbwxmKknuggO2NeMwOVkHOYL98pZ+1ZRI5YwckoCL3AvISMnPJgfN60ww6AIXHGpkp+HhpFdKOe8RHSEg==", + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.916.0.tgz", + "integrity": "sha512-bAgUQwvixdsiGNcuZSDAOWbyHlnPtg8G8TyHD6DTfTmKTHUW6tAn+af/ZYJPXEzXhhpwgJqi58vWnsiDhmr7NQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.893.0", - "@smithy/types": "^4.5.0", - "@smithy/url-parser": "^4.1.1", - "@smithy/util-endpoints": "^3.1.2", + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@smithy/util-endpoints": "^3.2.3", "tslib": "^2.6.2" }, "engines": { @@ -739,29 +767,29 @@ } }, "node_modules/@aws-sdk/util-user-agent-browser": { - "version": "3.893.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.893.0.tgz", - "integrity": "sha512-PE9NtbDBW6Kgl1bG6A5fF3EPo168tnkj8TgMcT0sg4xYBWsBpq0bpJZRh+Jm5Bkwiw9IgTCLjEU7mR6xWaMB9w==", + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.914.0.tgz", + "integrity": "sha512-rMQUrM1ECH4kmIwlGl9UB0BtbHy6ZuKdWFrIknu8yGTRI/saAucqNTh5EI1vWBxZ0ElhK5+g7zOnUuhSmVQYUA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/types": "3.893.0", - "@smithy/types": "^4.5.0", + "@aws-sdk/types": "3.914.0", + "@smithy/types": "^4.8.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "node_modules/@aws-sdk/util-user-agent-node": { - "version": "3.895.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.895.0.tgz", - "integrity": "sha512-lLRC7BAFOPtJk4cZC0Q0MZBMCGF109QpGnug3L3n/2TJW02Sinz9lzA0ykBpYXe9j60LjIYSENCg+F4DZE5vxg==", + "version": "3.916.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.916.0.tgz", + "integrity": "sha512-CwfWV2ch6UdjuSV75ZU99N03seEUb31FIUrXBnwa6oONqj/xqXwrxtlUMLx6WH3OJEE4zI3zt5PjlTdGcVwf4g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@aws-sdk/middleware-user-agent": "3.895.0", - "@aws-sdk/types": "3.893.0", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/types": "^4.5.0", + "@aws-sdk/middleware-user-agent": "3.916.0", + "@aws-sdk/types": "3.914.0", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -777,13 +805,13 @@ } }, "node_modules/@aws-sdk/xml-builder": { - "version": "3.894.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.894.0.tgz", - "integrity": "sha512-E6EAMc9dT1a2DOdo4zyOf3fp5+NJ2wI+mcm7RaW1baFIWDwcb99PpvWoV7YEiK7oaBDshuOEGWKUSYXdW+JYgA==", + "version": "3.914.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.914.0.tgz", + "integrity": "sha512-k75evsBD5TcIjedycYS7QXQ98AmOtbnxRJOPtCo0IwYRmy7UvqgS/gBL5SmrIqeV6FDSYRQMgdBxSMp6MLmdew==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.0", "fast-xml-parser": "5.2.5", "tslib": "^2.6.2" }, @@ -791,25 +819,6 @@ "node": ">=18.0.0" } }, - "node_modules/@aws-sdk/xml-builder/node_modules/fast-xml-parser": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", - "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/NaturalIntelligence" - } - ], - "license": "MIT", - "dependencies": { - "strnum": "^2.1.0" - }, - "bin": { - "fxparser": "src/cli/cli.js" - } - }, "node_modules/@aws/lambda-invoke-store": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.0.1.tgz", @@ -835,18 +844,18 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", - "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -968,6 +977,28 @@ "@csstools/css-tokenizer": "^3.0.4" } }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.14.tgz", + "integrity": "sha512-zSlIxa20WvMojjpCSy8WrNpcZ61RqfTfX3XTaOeVlGJrt/8HF3YbzgFZa01yTbT4GWQLwfTcC3EB8i3XnB647Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss": "^8.4" + } + }, "node_modules/@csstools/css-tokenizer": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", @@ -988,20 +1019,20 @@ } }, "node_modules/@dabh/diagnostics": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.3.tgz", - "integrity": "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA==", + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@dabh/diagnostics/-/diagnostics-2.0.8.tgz", + "integrity": "sha512-R4MSXTVnuMzGD7bzHdW2ZhhdPC/igELENcq5IjEverBvq5hn1SXCWcsi6eSsdWP0/Ur+SItRRjAktmdoX/8R/Q==", "license": "MIT", "dependencies": { - "colorspace": "1.1.x", + "@so-ric/colorspace": "^1.1.6", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", - "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz", + "integrity": "sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==", "license": "MIT", "optional": true, "dependencies": { @@ -1009,9 +1040,9 @@ } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.10.tgz", - "integrity": "sha512-0NFWnA+7l41irNuaSVlLfgNT12caWJVLzp5eAVhZ0z1qpxbockccEt3s+149rE64VUI3Ml2zt8Nv5JVc4QXTsw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", + "integrity": "sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==", "cpu": [ "ppc64" ], @@ -1026,9 +1057,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.10.tgz", - "integrity": "sha512-dQAxF1dW1C3zpeCDc5KqIYuZ1tgAdRXNoZP7vkBIRtKZPYe2xVr/d3SkirklCHudW1B45tGiUlz2pUWDfbDD4w==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.11.tgz", + "integrity": "sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==", "cpu": [ "arm" ], @@ -1043,9 +1074,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.10.tgz", - "integrity": "sha512-LSQa7eDahypv/VO6WKohZGPSJDq5OVOo3UoFR1E4t4Gj1W7zEQMUhI+lo81H+DtB+kP+tDgBp+M4oNCwp6kffg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz", + "integrity": "sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==", "cpu": [ "arm64" ], @@ -1060,9 +1091,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.10.tgz", - "integrity": "sha512-MiC9CWdPrfhibcXwr39p9ha1x0lZJ9KaVfvzA0Wxwz9ETX4v5CHfF09bx935nHlhi+MxhA63dKRRQLiVgSUtEg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.11.tgz", + "integrity": "sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==", "cpu": [ "x64" ], @@ -1077,9 +1108,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.10.tgz", - "integrity": "sha512-JC74bdXcQEpW9KkV326WpZZjLguSZ3DfS8wrrvPMHgQOIEIG/sPXEN/V8IssoJhbefLRcRqw6RQH2NnpdprtMA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz", + "integrity": "sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==", "cpu": [ "arm64" ], @@ -1094,9 +1125,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.10.tgz", - "integrity": "sha512-tguWg1olF6DGqzws97pKZ8G2L7Ig1vjDmGTwcTuYHbuU6TTjJe5FXbgs5C1BBzHbJ2bo1m3WkQDbWO2PvamRcg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz", + "integrity": "sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==", "cpu": [ "x64" ], @@ -1111,9 +1142,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.10.tgz", - "integrity": "sha512-3ZioSQSg1HT2N05YxeJWYR+Libe3bREVSdWhEEgExWaDtyFbbXWb49QgPvFH8u03vUPX10JhJPcz7s9t9+boWg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz", + "integrity": "sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==", "cpu": [ "arm64" ], @@ -1128,9 +1159,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.10.tgz", - "integrity": "sha512-LLgJfHJk014Aa4anGDbh8bmI5Lk+QidDmGzuC2D+vP7mv/GeSN+H39zOf7pN5N8p059FcOfs2bVlrRr4SK9WxA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz", + "integrity": "sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==", "cpu": [ "x64" ], @@ -1145,9 +1176,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.10.tgz", - "integrity": "sha512-oR31GtBTFYCqEBALI9r6WxoU/ZofZl962pouZRTEYECvNF/dtXKku8YXcJkhgK/beU+zedXfIzHijSRapJY3vg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz", + "integrity": "sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==", "cpu": [ "arm" ], @@ -1162,9 +1193,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.10.tgz", - "integrity": "sha512-5luJWN6YKBsawd5f9i4+c+geYiVEw20FVW5x0v1kEMWNq8UctFjDiMATBxLvmmHA4bf7F6hTRaJgtghFr9iziQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz", + "integrity": "sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==", "cpu": [ "arm64" ], @@ -1179,9 +1210,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.10.tgz", - "integrity": "sha512-NrSCx2Kim3EnnWgS4Txn0QGt0Xipoumb6z6sUtl5bOEZIVKhzfyp/Lyw4C1DIYvzeW/5mWYPBFJU3a/8Yr75DQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz", + "integrity": "sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==", "cpu": [ "ia32" ], @@ -1196,9 +1227,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.10.tgz", - "integrity": "sha512-xoSphrd4AZda8+rUDDfD9J6FUMjrkTz8itpTITM4/xgerAZZcFW7Dv+sun7333IfKxGG8gAq+3NbfEMJfiY+Eg==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz", + "integrity": "sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==", "cpu": [ "loong64" ], @@ -1213,9 +1244,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.10.tgz", - "integrity": "sha512-ab6eiuCwoMmYDyTnyptoKkVS3k8fy/1Uvq7Dj5czXI6DF2GqD2ToInBI0SHOp5/X1BdZ26RKc5+qjQNGRBelRA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz", + "integrity": "sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==", "cpu": [ "mips64el" ], @@ -1230,9 +1261,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.10.tgz", - "integrity": "sha512-NLinzzOgZQsGpsTkEbdJTCanwA5/wozN9dSgEl12haXJBzMTpssebuXR42bthOF3z7zXFWH1AmvWunUCkBE4EA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz", + "integrity": "sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==", "cpu": [ "ppc64" ], @@ -1247,9 +1278,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.10.tgz", - "integrity": "sha512-FE557XdZDrtX8NMIeA8LBJX3dC2M8VGXwfrQWU7LB5SLOajfJIxmSdyL/gU1m64Zs9CBKvm4UAuBp5aJ8OgnrA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz", + "integrity": "sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==", "cpu": [ "riscv64" ], @@ -1264,9 +1295,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.10.tgz", - "integrity": "sha512-3BBSbgzuB9ajLoVZk0mGu+EHlBwkusRmeNYdqmznmMc9zGASFjSsxgkNsqmXugpPk00gJ0JNKh/97nxmjctdew==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz", + "integrity": "sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==", "cpu": [ "s390x" ], @@ -1281,9 +1312,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.10.tgz", - "integrity": "sha512-QSX81KhFoZGwenVyPoberggdW1nrQZSvfVDAIUXr3WqLRZGZqWk/P4T8p2SP+de2Sr5HPcvjhcJzEiulKgnxtA==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz", + "integrity": "sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==", "cpu": [ "x64" ], @@ -1298,9 +1329,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.10.tgz", - "integrity": "sha512-AKQM3gfYfSW8XRk8DdMCzaLUFB15dTrZfnX8WXQoOUpUBQ+NaAFCP1kPS/ykbbGYz7rxn0WS48/81l9hFl3u4A==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz", + "integrity": "sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==", "cpu": [ "arm64" ], @@ -1315,9 +1346,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.10.tgz", - "integrity": "sha512-7RTytDPGU6fek/hWuN9qQpeGPBZFfB4zZgcz2VK2Z5VpdUxEI8JKYsg3JfO0n/Z1E/6l05n0unDCNc4HnhQGig==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz", + "integrity": "sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==", "cpu": [ "x64" ], @@ -1332,9 +1363,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.10.tgz", - "integrity": "sha512-5Se0VM9Wtq797YFn+dLimf2Zx6McttsH2olUBsDml+lm0GOCRVebRWUvDtkY4BWYv/3NgzS8b/UM3jQNh5hYyw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz", + "integrity": "sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==", "cpu": [ "arm64" ], @@ -1349,9 +1380,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.10.tgz", - "integrity": "sha512-XkA4frq1TLj4bEMB+2HnI0+4RnjbuGZfet2gs/LNs5Hc7D89ZQBHQ0gL2ND6Lzu1+QVkjp3x1gIcPKzRNP8bXw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz", + "integrity": "sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==", "cpu": [ "x64" ], @@ -1366,9 +1397,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.10.tgz", - "integrity": "sha512-AVTSBhTX8Y/Fz6OmIVBip9tJzZEUcY8WLh7I59+upa5/GPhh2/aM6bvOMQySspnCCHvFi79kMtdJS1w0DXAeag==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz", + "integrity": "sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==", "cpu": [ "arm64" ], @@ -1383,9 +1414,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.10.tgz", - "integrity": "sha512-fswk3XT0Uf2pGJmOpDB7yknqhVkJQkAQOcW/ccVOtfx05LkbWOaRAtn5SaqXypeKQra1QaEa841PgrSL9ubSPQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz", + "integrity": "sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==", "cpu": [ "x64" ], @@ -1400,9 +1431,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.10.tgz", - "integrity": "sha512-ah+9b59KDTSfpaCg6VdJoOQvKjI33nTaQr4UluQwW7aEwZQsbMCfTmfEO4VyewOxx4RaDT/xCy9ra2GPWmO7Kw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz", + "integrity": "sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==", "cpu": [ "arm64" ], @@ -1417,9 +1448,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.10.tgz", - "integrity": "sha512-QHPDbKkrGO8/cz9LKVnJU22HOi4pxZnZhhA2HYHez5Pz4JeffhDjf85E57Oyco163GnzNCVkZK0b/n4Y0UHcSw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz", + "integrity": "sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==", "cpu": [ "ia32" ], @@ -1434,9 +1465,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.10.tgz", - "integrity": "sha512-9KpxSVFCu0iK1owoez6aC/s/EdUQLDN3adTxGCqxMVhrPDj6bt5dbrHDXUuq+Bs2vATFBBrQS5vdQ/Ed2P+nbw==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz", + "integrity": "sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==", "cpu": [ "x64" ], @@ -1451,9 +1482,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.8.0.tgz", - "integrity": "sha512-MJQFqrZgcW0UNYLGOuQpey/oTN59vyWwplvCGZztn1cKz9agZPPYpJB7h2OMmuu7VLqkvEjN8feFZJmxNF9D+Q==", + "version": "4.9.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", + "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", "dev": true, "license": "MIT", "dependencies": { @@ -1483,9 +1514,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -1493,13 +1524,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", - "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "version": "0.21.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz", + "integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^2.1.6", + "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.2" }, @@ -1508,19 +1539,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", - "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.1.tgz", + "integrity": "sha512-csZAzkNhsgwb0I/UAV6/RGFTbiakPCf0ZrGmrIxQpYvGZ00PhTkSnyKNolphgIvmnJeGw6rcGVEXfTzUnFuEvw==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.16.0" + }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@eslint/core": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", - "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.16.0.tgz", + "integrity": "sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -1568,9 +1602,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", - "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.38.0.tgz", + "integrity": "sha512-UZ1VpFvXf9J06YG9xQBdnzU+kthors6KjhMAl6f4gH4usHyh31rUf2DLGInT8RFYIReYXNSydgPY0V2LuWgl7A==", "dev": true, "license": "MIT", "engines": { @@ -1581,9 +1615,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", - "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", "dev": true, "license": "Apache-2.0", "engines": { @@ -1591,13 +1625,13 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", - "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz", + "integrity": "sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^0.15.2", + "@eslint/core": "^0.16.0", "levn": "^0.4.1" }, "engines": { @@ -1605,18 +1639,36 @@ } }, "node_modules/@grpc/grpc-js": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.13.4.tgz", - "integrity": "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg==", + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.0.tgz", + "integrity": "sha512-N8Jx6PaYzcTRNzirReJCtADVoq4z7+1KQ4E70jTg/koQiMoUSN1kbNjPOqpPbhMFhfU1/l7ixspPl8dNY+FoUg==", "license": "Apache-2.0", "dependencies": { - "@grpc/proto-loader": "^0.7.13", + "@grpc/proto-loader": "^0.8.0", "@js-sdsl/ordered-map": "^4.4.2" }, "engines": { "node": ">=12.10.0" } }, + "node_modules/@grpc/grpc-js/node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@grpc/proto-loader": { "version": "0.7.15", "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.15.tgz", @@ -2064,9 +2116,9 @@ } }, "node_modules/@ioredis/commands": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.3.1.tgz", - "integrity": "sha512-bYtU8avhGIcje3IhvF9aSjsa5URMZBHnwKtOvXsT4sfYy9gppW11gLPT/9oNqlJZD47yPKveQFTAFWpHjKvUoQ==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.4.0.tgz", + "integrity": "sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==", "license": "MIT" }, "node_modules/@isaacs/cliui": { @@ -2114,9 +2166,9 @@ "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.30", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", - "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "license": "MIT", "dependencies": { @@ -2135,9 +2187,9 @@ } }, "node_modules/@mongodb-js/saslprep": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.0.tgz", - "integrity": "sha512-zlayKCsIjYb7/IdfqxorK5+xUMyi4vOKcFy10wKJYc63NSdKI8mNME+uJqfatkPmOSMMUiojrL58IePKBm3gvQ==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.3.2.tgz", + "integrity": "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg==", "license": "MIT", "dependencies": { "sparse-bitfield": "^3.0.3" @@ -2344,9 +2396,9 @@ "license": "BSD-3-Clause" }, "node_modules/@sindresorhus/is": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.2.tgz", - "integrity": "sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.1.0.tgz", + "integrity": "sha512-7F/yz2IphV39hiS2zB4QYVkivrptHHh0K8qJJd9HhuWSdvf8AN7NpebW3CcDZDBQsUPMoDKWsY2WWgW7bqOcfA==", "license": "MIT", "engines": { "node": ">=18" @@ -2404,13 +2456,13 @@ "license": "(Unlicense OR Apache-2.0)" }, "node_modules/@smithy/abort-controller": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.1.1.tgz", - "integrity": "sha512-vkzula+IwRvPR6oKQhMYioM3A/oX/lFCZiwuxkQbRhqJS2S4YRY2k7k/SyR2jMf3607HLtbEwlRxi0ndXHMjRg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.3.tgz", + "integrity": "sha512-xWL9Mf8b7tIFuAlpjKtRPnHrR8XVrwTj5NPYO/QwZPtc0SDLsPxb56V5tzi5yspSMytISHybifez+4jlrx0vkQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -2418,16 +2470,17 @@ } }, "node_modules/@smithy/config-resolver": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.2.2.tgz", - "integrity": "sha512-IT6MatgBWagLybZl1xQcURXRICvqz1z3APSCAI9IqdvfCkrA7RaQIEfgC6G/KvfxnDfQUDqFV+ZlixcuFznGBQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.0.tgz", + "integrity": "sha512-Kkmz3Mup2PGp/HNJxhCWkLNdlajJORLSjwkcfrj0E7nu6STAEdcMR1ir5P9/xOmncx8xXfru0fbUYLlZog/cFg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.2.2", - "@smithy/types": "^4.5.0", - "@smithy/util-config-provider": "^4.1.0", - "@smithy/util-middleware": "^4.1.1", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/types": "^4.8.0", + "@smithy/util-config-provider": "^4.2.0", + "@smithy/util-endpoints": "^3.2.3", + "@smithy/util-middleware": "^4.2.3", "tslib": "^2.6.2" }, "engines": { @@ -2435,21 +2488,21 @@ } }, "node_modules/@smithy/core": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.12.0.tgz", - "integrity": "sha512-zJeAgogZfbwlPGL93y4Z/XNeIN37YCreRUd6YMIRvaq+6RnBK8PPYYIQ85Is/GglPh3kNImD5riDCXbVSDpCiQ==", + "version": "3.17.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.17.1.tgz", + "integrity": "sha512-V4Qc2CIb5McABYfaGiIYLTmo/vwNIK7WXI5aGveBd9UcdhbOMwcvIMxIw/DJj1S9QgOMa/7FBkarMdIC0EOTEQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/middleware-serde": "^4.1.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-body-length-browser": "^4.1.0", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-stream": "^4.3.2", - "@smithy/util-utf8": "^4.1.0", - "@smithy/uuid": "^1.0.0", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-body-length-browser": "^4.2.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-stream": "^4.5.4", + "@smithy/util-utf8": "^4.2.0", + "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" }, "engines": { @@ -2457,16 +2510,16 @@ } }, "node_modules/@smithy/credential-provider-imds": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.1.2.tgz", - "integrity": "sha512-JlYNq8TShnqCLg0h+afqe2wLAwZpuoSgOyzhYvTgbiKBWRov+uUve+vrZEQO6lkdLOWPh7gK5dtb9dS+KGendg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.3.tgz", + "integrity": "sha512-hA1MQ/WAHly4SYltJKitEsIDVsNmXcQfYBRv2e+q04fnqtAX5qXaybxy/fhUeAMCnQIdAjaGDb04fMHQefWRhw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.2.2", - "@smithy/property-provider": "^4.1.1", - "@smithy/types": "^4.5.0", - "@smithy/url-parser": "^4.1.1", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", "tslib": "^2.6.2" }, "engines": { @@ -2474,16 +2527,16 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.2.1.tgz", - "integrity": "sha512-5/3wxKNtV3wO/hk1is+CZUhL8a1yy/U+9u9LKQ9kZTkMsHaQjJhc3stFfiujtMnkITjzWfndGA2f7g9Uh9vKng==", + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.4.tgz", + "integrity": "sha512-bwigPylvivpRLCm+YK9I5wRIYjFESSVwl8JQ1vVx/XhCw0PtCi558NwTnT2DaVCl5pYlImGuQTSwMsZ+pIavRw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.2.1", - "@smithy/querystring-builder": "^4.1.1", - "@smithy/types": "^4.5.0", - "@smithy/util-base64": "^4.1.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/querystring-builder": "^4.2.3", + "@smithy/types": "^4.8.0", + "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" }, "engines": { @@ -2491,15 +2544,15 @@ } }, "node_modules/@smithy/hash-node": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.1.1.tgz", - "integrity": "sha512-H9DIU9WBLhYrvPs9v4sYvnZ1PiAI0oc8CgNQUJ1rpN3pP7QADbTOUjchI2FB764Ub0DstH5xbTqcMJu1pnVqxA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.3.tgz", + "integrity": "sha512-6+NOdZDbfuU6s1ISp3UOk5Rg953RJ2aBLNLLBEcamLjHAg1Po9Ha7QIB5ZWhdRUVuOUrT8BVFR+O2KIPmw027g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", - "@smithy/util-buffer-from": "^4.1.0", - "@smithy/util-utf8": "^4.1.0", + "@smithy/types": "^4.8.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -2507,13 +2560,13 @@ } }, "node_modules/@smithy/invalid-dependency": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.1.1.tgz", - "integrity": "sha512-1AqLyFlfrrDkyES8uhINRlJXmHA2FkG+3DY8X+rmLSqmFwk3DJnvhyGzyByPyewh2jbmV+TYQBEfngQax8IFGg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.3.tgz", + "integrity": "sha512-Cc9W5DwDuebXEDMpOpl4iERo8I0KFjTnomK2RMdhhR87GwrSmUmwMxS4P5JdRf+LsjOdIqumcerwRgYMr/tZ9Q==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -2521,9 +2574,9 @@ } }, "node_modules/@smithy/is-array-buffer": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.1.0.tgz", - "integrity": "sha512-ePTYUOV54wMogio+he4pBybe8fwg4sDvEVDBU8ZlHOZXbXK3/C0XfJgUCu6qAZcawv05ZhZzODGUerFBPsPUDQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.0.tgz", + "integrity": "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2534,14 +2587,14 @@ } }, "node_modules/@smithy/middleware-content-length": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.1.1.tgz", - "integrity": "sha512-9wlfBBgTsRvC2JxLJxv4xDGNBrZuio3AgSl0lSFX7fneW2cGskXTYpFxCdRYD2+5yzmsiTuaAJD1Wp7gWt9y9w==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.3.tgz", + "integrity": "sha512-/atXLsT88GwKtfp5Jr0Ks1CSa4+lB+IgRnkNrrYP0h1wL4swHNb0YONEvTceNKNdZGJsye+W2HH8W7olbcPUeA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -2549,19 +2602,19 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.2.4.tgz", - "integrity": "sha512-FZ4hzupOmthm8Q8ujYrd0I+/MHwVMuSTdkDtIQE0xVuvJt9pLT6Q+b0p4/t+slDyrpcf+Wj7SN+ZqT5OryaaZg==", + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.3.5.tgz", + "integrity": "sha512-SIzKVTvEudFWJbxAaq7f2GvP3jh2FHDpIFI6/VAf4FOWGFZy0vnYMPSRj8PGYI8Hjt29mvmwSRgKuO3bK4ixDw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.12.0", - "@smithy/middleware-serde": "^4.1.1", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/shared-ini-file-loader": "^4.2.0", - "@smithy/types": "^4.5.0", - "@smithy/url-parser": "^4.1.1", - "@smithy/util-middleware": "^4.1.1", + "@smithy/core": "^3.17.1", + "@smithy/middleware-serde": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", + "@smithy/url-parser": "^4.2.3", + "@smithy/util-middleware": "^4.2.3", "tslib": "^2.6.2" }, "engines": { @@ -2569,20 +2622,20 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.3.0.tgz", - "integrity": "sha512-qhEX9745fAxZvtLM4bQJAVC98elWjiMO2OiHl1s6p7hUzS4QfZO1gXUYNwEK8m0J6NoCD5W52ggWxbIDHI0XSg==", + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.4.5.tgz", + "integrity": "sha512-DCaXbQqcZ4tONMvvdz+zccDE21sLcbwWoNqzPLFlZaxt1lDtOE2tlVpRSwcTOJrjJSUThdgEYn7HrX5oLGlK9A==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.2.2", - "@smithy/protocol-http": "^5.2.1", - "@smithy/service-error-classification": "^4.1.2", - "@smithy/smithy-client": "^4.6.4", - "@smithy/types": "^4.5.0", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-retry": "^4.1.2", - "@smithy/uuid": "^1.0.0", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/service-error-classification": "^4.2.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-retry": "^4.2.3", + "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" }, "engines": { @@ -2590,14 +2643,14 @@ } }, "node_modules/@smithy/middleware-serde": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.1.1.tgz", - "integrity": "sha512-lh48uQdbCoj619kRouev5XbWhCwRKLmphAif16c4J6JgJ4uXjub1PI6RL38d3BLliUvSso6klyB/LTNpWSNIyg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.3.tgz", + "integrity": "sha512-8g4NuUINpYccxiCXM5s1/V+uLtts8NcX4+sPEbvYQDZk4XoJfDpq5y2FQxfmUL89syoldpzNzA0R9nhzdtdKnQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -2605,13 +2658,13 @@ } }, "node_modules/@smithy/middleware-stack": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.1.1.tgz", - "integrity": "sha512-ygRnniqNcDhHzs6QAPIdia26M7e7z9gpkIMUe/pK0RsrQ7i5MblwxY8078/QCnGq6AmlUUWgljK2HlelsKIb/A==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.3.tgz", + "integrity": "sha512-iGuOJkH71faPNgOj/gWuEGS6xvQashpLwWB1HjHq1lNNiVfbiJLpZVbhddPuDbx9l4Cgl0vPLq5ltRfSaHfspA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -2619,15 +2672,15 @@ } }, "node_modules/@smithy/node-config-provider": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.2.2.tgz", - "integrity": "sha512-SYGTKyPvyCfEzIN5rD8q/bYaOPZprYUPD2f5g9M7OjaYupWOoQFYJ5ho+0wvxIRf471i2SR4GoiZ2r94Jq9h6A==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.3.tgz", + "integrity": "sha512-NzI1eBpBSViOav8NVy1fqOlSfkLgkUjUTlohUSgAEhHaFWA3XJiLditvavIP7OpvTjDp5u2LhtlBhkBlEisMwA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.1.1", - "@smithy/shared-ini-file-loader": "^4.2.0", - "@smithy/types": "^4.5.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/shared-ini-file-loader": "^4.3.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -2635,16 +2688,16 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.2.1.tgz", - "integrity": "sha512-REyybygHlxo3TJICPF89N2pMQSf+p+tBJqpVe1+77Cfi9HBPReNjTgtZ1Vg73exq24vkqJskKDpfF74reXjxfw==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.4.3.tgz", + "integrity": "sha512-MAwltrDB0lZB/H6/2M5PIsISSwdI5yIh6DaBB9r0Flo9nx3y0dzl/qTMJPd7tJvPdsx6Ks/cwVzheGNYzXyNbQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/abort-controller": "^4.1.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/querystring-builder": "^4.1.1", - "@smithy/types": "^4.5.0", + "@smithy/abort-controller": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/querystring-builder": "^4.2.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -2652,13 +2705,13 @@ } }, "node_modules/@smithy/property-provider": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.1.1.tgz", - "integrity": "sha512-gm3ZS7DHxUbzC2wr8MUCsAabyiXY0gaj3ROWnhSx/9sPMc6eYLMM4rX81w1zsMaObj2Lq3PZtNCC1J6lpEY7zg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.3.tgz", + "integrity": "sha512-+1EZ+Y+njiefCohjlhyOcy1UNYjT+1PwGFHCxA/gYctjg3DQWAU19WigOXAco/Ql8hZokNehpzLd0/+3uCreqQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -2666,13 +2719,13 @@ } }, "node_modules/@smithy/protocol-http": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.2.1.tgz", - "integrity": "sha512-T8SlkLYCwfT/6m33SIU/JOVGNwoelkrvGjFKDSDtVvAXj/9gOT78JVJEas5a+ETjOu4SVvpCstKgd0PxSu/aHw==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.3.tgz", + "integrity": "sha512-Mn7f/1aN2/jecywDcRDvWWWJF4uwg/A0XjFMJtj72DsgHTByfjRltSqcT9NyE9RTdBSN6X1RSXrhn/YWQl8xlw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -2680,14 +2733,14 @@ } }, "node_modules/@smithy/querystring-builder": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.1.1.tgz", - "integrity": "sha512-J9b55bfimP4z/Jg1gNo+AT84hr90p716/nvxDkPGCD4W70MPms0h8KF50RDRgBGZeL83/u59DWNqJv6tEP/DHA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.3.tgz", + "integrity": "sha512-LOVCGCmwMahYUM/P0YnU/AlDQFjcu+gWbFJooC417QRB/lDJlWSn8qmPSDp+s4YVAHOgtgbNG4sR+SxF/VOcJQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", - "@smithy/util-uri-escape": "^4.1.0", + "@smithy/types": "^4.8.0", + "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -2695,13 +2748,13 @@ } }, "node_modules/@smithy/querystring-parser": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.1.1.tgz", - "integrity": "sha512-63TEp92YFz0oQ7Pj9IuI3IgnprP92LrZtRAkE3c6wLWJxfy/yOPRt39IOKerVr0JS770olzl0kGafXlAXZ1vng==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.3.tgz", + "integrity": "sha512-cYlSNHcTAX/wc1rpblli3aUlLMGgKZ/Oqn8hhjFASXMCXjIqeuQBei0cnq2JR8t4RtU9FpG6uyl6PxyArTiwKA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -2709,26 +2762,26 @@ } }, "node_modules/@smithy/service-error-classification": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.1.2.tgz", - "integrity": "sha512-Kqd8wyfmBWHZNppZSMfrQFpc3M9Y/kjyN8n8P4DqJJtuwgK1H914R471HTw7+RL+T7+kI1f1gOnL7Vb5z9+NgQ==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.3.tgz", + "integrity": "sha512-NkxsAxFWwsPsQiwFG2MzJ/T7uIR6AQNh1SzcxSUnmmIqIQMlLRQDKhc17M7IYjiuBXhrQRjQTo3CxX+DobS93g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0" + "@smithy/types": "^4.8.0" }, "engines": { "node": ">=18.0.0" } }, "node_modules/@smithy/shared-ini-file-loader": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.2.0.tgz", - "integrity": "sha512-OQTfmIEp2LLuWdxa8nEEPhZmiOREO6bcB6pjs0AySf4yiZhl6kMOfqmcwcY8BaBPX+0Tb+tG7/Ia/6mwpoZ7Pw==", + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.3.3.tgz", + "integrity": "sha512-9f9Ixej0hFhroOK2TxZfUUDR13WVa8tQzhSzPDgXe5jGL3KmaM9s8XN7RQwqtEypI82q9KHnKS71CJ+q/1xLtQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -2736,19 +2789,19 @@ } }, "node_modules/@smithy/signature-v4": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.2.1.tgz", - "integrity": "sha512-M9rZhWQLjlQVCCR37cSjHfhriGRN+FQ8UfgrYNufv66TJgk+acaggShl3KS5U/ssxivvZLlnj7QH2CUOKlxPyA==", + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.3.tgz", + "integrity": "sha512-CmSlUy+eEYbIEYN5N3vvQTRfqt0lJlQkaQUIf+oizu7BbDut0pozfDjBGecfcfWf7c62Yis4JIEgqQ/TCfodaA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.1.0", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", - "@smithy/util-hex-encoding": "^4.1.0", - "@smithy/util-middleware": "^4.1.1", - "@smithy/util-uri-escape": "^4.1.0", - "@smithy/util-utf8": "^4.1.0", + "@smithy/is-array-buffer": "^4.2.0", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-middleware": "^4.2.3", + "@smithy/util-uri-escape": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -2756,18 +2809,18 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.6.4", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.6.4.tgz", - "integrity": "sha512-qL7O3VDyfzCSN9r+sdbQXGhaHtrfSJL30En6Jboj0I3bobf2g1/T0eP2L4qxqrEW26gWhJ4THI4ElVVLjYyBHg==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.9.1.tgz", + "integrity": "sha512-Ngb95ryR5A9xqvQFT5mAmYkCwbXvoLavLFwmi7zVg/IowFPCfiqRfkOKnbc/ZRL8ZKJ4f+Tp6kSu6wjDQb8L/g==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.12.0", - "@smithy/middleware-endpoint": "^4.2.4", - "@smithy/middleware-stack": "^4.1.1", - "@smithy/protocol-http": "^5.2.1", - "@smithy/types": "^4.5.0", - "@smithy/util-stream": "^4.3.2", + "@smithy/core": "^3.17.1", + "@smithy/middleware-endpoint": "^4.3.5", + "@smithy/middleware-stack": "^4.2.3", + "@smithy/protocol-http": "^5.3.3", + "@smithy/types": "^4.8.0", + "@smithy/util-stream": "^4.5.4", "tslib": "^2.6.2" }, "engines": { @@ -2775,9 +2828,9 @@ } }, "node_modules/@smithy/types": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.5.0.tgz", - "integrity": "sha512-RkUpIOsVlAwUIZXO1dsz8Zm+N72LClFfsNqf173catVlvRZiwPy0x2u0JLEA4byreOPKDZPGjmPDylMoP8ZJRg==", + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.8.0.tgz", + "integrity": "sha512-QpELEHLO8SsQVtqP+MkEgCYTFW0pleGozfs3cZ183ZBj9z3VC1CX1/wtFMK64p+5bhtZo41SeLK1rBRtd25nHQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2788,14 +2841,14 @@ } }, "node_modules/@smithy/url-parser": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.1.1.tgz", - "integrity": "sha512-bx32FUpkhcaKlEoOMbScvc93isaSiRM75pQ5IgIBaMkT7qMlIibpPRONyx/0CvrXHzJLpOn/u6YiDX2hcvs7Dg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.3.tgz", + "integrity": "sha512-I066AigYvY3d9VlU3zG9XzZg1yT10aNqvCaBTw9EPgu5GrsEl1aUkcMvhkIXascYH1A8W0LQo3B1Kr1cJNcQEw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/querystring-parser": "^4.1.1", - "@smithy/types": "^4.5.0", + "@smithy/querystring-parser": "^4.2.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -2803,14 +2856,14 @@ } }, "node_modules/@smithy/util-base64": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.1.0.tgz", - "integrity": "sha512-RUGd4wNb8GeW7xk+AY5ghGnIwM96V0l2uzvs/uVHf+tIuVX2WSvynk5CxNoBCsM2rQRSZElAo9rt3G5mJ/gktQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.0.tgz", + "integrity": "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.1.0", - "@smithy/util-utf8": "^4.1.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -2818,9 +2871,9 @@ } }, "node_modules/@smithy/util-body-length-browser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.1.0.tgz", - "integrity": "sha512-V2E2Iez+bo6bUMOTENPr6eEmepdY8Hbs+Uc1vkDKgKNA/brTJqOW/ai3JO1BGj9GbCeLqw90pbbH7HFQyFotGQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.0.tgz", + "integrity": "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2831,9 +2884,9 @@ } }, "node_modules/@smithy/util-body-length-node": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.1.0.tgz", - "integrity": "sha512-BOI5dYjheZdgR9XiEM3HJcEMCXSoqbzu7CzIgYrx0UtmvtC3tC2iDGpJLsSRFffUpy8ymsg2ARMP5fR8mtuUQQ==", + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.1.tgz", + "integrity": "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2844,13 +2897,13 @@ } }, "node_modules/@smithy/util-buffer-from": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.1.0.tgz", - "integrity": "sha512-N6yXcjfe/E+xKEccWEKzK6M+crMrlwaCepKja0pNnlSkm6SjAeLKKA++er5Ba0I17gvKfN/ThV+ZOx/CntKTVw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.0.tgz", + "integrity": "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/is-array-buffer": "^4.1.0", + "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -2858,9 +2911,9 @@ } }, "node_modules/@smithy/util-config-provider": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.1.0.tgz", - "integrity": "sha512-swXz2vMjrP1ZusZWVTB/ai5gK+J8U0BWvP10v9fpcFvg+Xi/87LHvHfst2IgCs1i0v4qFZfGwCmeD/KNCdJZbQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.0.tgz", + "integrity": "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2871,16 +2924,15 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.1.4.tgz", - "integrity": "sha512-mLDJ1s4eA3vwOGaQOEPlg5LB4LdZUUMpB5UMOMofeGhWqiS7WR7dTpLiNi9zVn+YziKUd3Af5NLfxDs7NJqmIw==", + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.4.tgz", + "integrity": "sha512-qI5PJSW52rnutos8Bln8nwQZRpyoSRN6k2ajyoUHNMUzmWqHnOJCnDELJuV6m5PML0VkHI+XcXzdB+6awiqYUw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/property-provider": "^4.1.1", - "@smithy/smithy-client": "^4.6.4", - "@smithy/types": "^4.5.0", - "bowser": "^2.11.0", + "@smithy/property-provider": "^4.2.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -2888,18 +2940,18 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.1.4.tgz", - "integrity": "sha512-pjX2iMTcOASaSanAd7bu6i3fcMMezr3NTr8Rh64etB0uHRZi+Aw86DoCxPESjY4UTIuA06hhqtTtw95o//imYA==", + "version": "4.2.6", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.6.tgz", + "integrity": "sha512-c6M/ceBTm31YdcFpgfgQAJaw3KbaLuRKnAz91iMWFLSrgxRpYm03c3bu5cpYojNMfkV9arCUelelKA7XQT36SQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/config-resolver": "^4.2.2", - "@smithy/credential-provider-imds": "^4.1.2", - "@smithy/node-config-provider": "^4.2.2", - "@smithy/property-provider": "^4.1.1", - "@smithy/smithy-client": "^4.6.4", - "@smithy/types": "^4.5.0", + "@smithy/config-resolver": "^4.4.0", + "@smithy/credential-provider-imds": "^4.2.3", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/property-provider": "^4.2.3", + "@smithy/smithy-client": "^4.9.1", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -2907,14 +2959,14 @@ } }, "node_modules/@smithy/util-endpoints": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.1.2.tgz", - "integrity": "sha512-+AJsaaEGb5ySvf1SKMRrPZdYHRYSzMkCoK16jWnIMpREAnflVspMIDeCVSZJuj+5muZfgGpNpijE3mUNtjv01Q==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.2.3.tgz", + "integrity": "sha512-aCfxUOVv0CzBIkU10TubdgKSx5uRvzH064kaiPEWfNIvKOtNpu642P4FP1hgOFkjQIkDObrfIDnKMKkeyrejvQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/node-config-provider": "^4.2.2", - "@smithy/types": "^4.5.0", + "@smithy/node-config-provider": "^4.3.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -2922,9 +2974,9 @@ } }, "node_modules/@smithy/util-hex-encoding": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.1.0.tgz", - "integrity": "sha512-1LcueNN5GYC4tr8mo14yVYbh/Ur8jHhWOxniZXii+1+ePiIbsLZ5fEI0QQGtbRRP5mOhmooos+rLmVASGGoq5w==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.0.tgz", + "integrity": "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2935,13 +2987,13 @@ } }, "node_modules/@smithy/util-middleware": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.1.1.tgz", - "integrity": "sha512-CGmZ72mL29VMfESz7S6dekqzCh8ZISj3B+w0g1hZFXaOjGTVaSqfAEFAq8EGp8fUL+Q2l8aqNmt8U1tglTikeg==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.3.tgz", + "integrity": "sha512-v5ObKlSe8PWUHCqEiX2fy1gNv6goiw6E5I/PN2aXg3Fb/hse0xeaAnSpXDiWl7x6LamVKq7senB+m5LOYHUAHw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/types": "^4.5.0", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -2949,14 +3001,14 @@ } }, "node_modules/@smithy/util-retry": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.1.2.tgz", - "integrity": "sha512-NCgr1d0/EdeP6U5PSZ9Uv5SMR5XRRYoVr1kRVtKZxWL3tixEL3UatrPIMFZSKwHlCcp2zPLDvMubVDULRqeunA==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.2.3.tgz", + "integrity": "sha512-lLPWnakjC0q9z+OtiXk+9RPQiYPNAovt2IXD3CP4LkOnd9NpUsxOjMx1SnoUVB7Orb7fZp67cQMtTBKMFDvOGg==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/service-error-classification": "^4.1.2", - "@smithy/types": "^4.5.0", + "@smithy/service-error-classification": "^4.2.3", + "@smithy/types": "^4.8.0", "tslib": "^2.6.2" }, "engines": { @@ -2964,19 +3016,19 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.3.2.tgz", - "integrity": "sha512-Ka+FA2UCC/Q1dEqUanCdpqwxOFdf5Dg2VXtPtB1qxLcSGh5C1HdzklIt18xL504Wiy9nNUKwDMRTVCbKGoK69g==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.4.tgz", + "integrity": "sha512-+qDxSkiErejw1BAIXUFBSfM5xh3arbz1MmxlbMCKanDDZtVEQ7PSKW9FQS0Vud1eI/kYn0oCTVKyNzRlq+9MUw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.2.1", - "@smithy/node-http-handler": "^4.2.1", - "@smithy/types": "^4.5.0", - "@smithy/util-base64": "^4.1.0", - "@smithy/util-buffer-from": "^4.1.0", - "@smithy/util-hex-encoding": "^4.1.0", - "@smithy/util-utf8": "^4.1.0", + "@smithy/fetch-http-handler": "^5.3.4", + "@smithy/node-http-handler": "^4.4.3", + "@smithy/types": "^4.8.0", + "@smithy/util-base64": "^4.3.0", + "@smithy/util-buffer-from": "^4.2.0", + "@smithy/util-hex-encoding": "^4.2.0", + "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -2984,9 +3036,9 @@ } }, "node_modules/@smithy/util-uri-escape": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.1.0.tgz", - "integrity": "sha512-b0EFQkq35K5NHUYxU72JuoheM6+pytEVUGlTwiFxWFpmddA+Bpz3LgsPRIpBk8lnPE47yT7AF2Egc3jVnKLuPg==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.0.tgz", + "integrity": "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2997,13 +3049,13 @@ } }, "node_modules/@smithy/util-utf8": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.1.0.tgz", - "integrity": "sha512-mEu1/UIXAdNYuBcyEPbjScKi/+MQVXNIuY/7Cm5XLIWe319kDrT5SizBE95jqtmEXoDbGoZxKLCMttdZdqTZKQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.0.tgz", + "integrity": "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@smithy/util-buffer-from": "^4.1.0", + "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" }, "engines": { @@ -3011,9 +3063,9 @@ } }, "node_modules/@smithy/uuid": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.0.0.tgz", - "integrity": "sha512-OlA/yZHh0ekYFnbUkmYBDQPE6fGfdrvgz39ktp8Xf+FA6BfxLejPTMDOG0Nfk5/rDySAz1dRbFf24zaAFYVXlQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.0.tgz", + "integrity": "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -3023,6 +3075,62 @@ "node": ">=18.0.0" } }, + "node_modules/@so-ric/colorspace": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@so-ric/colorspace/-/colorspace-1.1.6.tgz", + "integrity": "sha512-/KiKkpHNOBgkFJwu9sh48LkHSMYGyuTcSFK/qMBdnOAlrRJzRSXAOFB5qwzaVQuDl8wAvHVMkaASQDReTahxuw==", + "license": "MIT", + "dependencies": { + "color": "^5.0.2", + "text-hex": "1.0.x" + } + }, + "node_modules/@so-ric/colorspace/node_modules/color": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/color/-/color-5.0.2.tgz", + "integrity": "sha512-e2hz5BzbUPcYlIRHo8ieAhYgoajrJr+hWoceg6E345TPsATMUKqDgzt8fSXZJJbxfpiPzkWyphz8yn8At7q3fA==", + "license": "MIT", + "dependencies": { + "color-convert": "^3.0.1", + "color-string": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@so-ric/colorspace/node_modules/color-convert": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-3.1.2.tgz", + "integrity": "sha512-UNqkvCDXstVck3kdowtOTWROIJQwafjOfXSmddoDrXo4cewMKmusCeF22Q24zvjR8nwWib/3S/dfyzPItPEiJg==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=14.6" + } + }, + "node_modules/@so-ric/colorspace/node_modules/color-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-2.0.2.tgz", + "integrity": "sha512-9vEt7gE16EW7Eu7pvZnR0abW9z6ufzhXxGXZEVU9IqPdlsUiMwJeJfRtq0zePUmnbHGT9zajca7mX8zgoayo4A==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, + "node_modules/@so-ric/colorspace/node_modules/color-string": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-2.1.2.tgz", + "integrity": "sha512-RxmjYxbWemV9gKu4zPgiZagUxbH3RQpEIO77XoSSX0ivgABDZ+h8Zuash/EMFLTI4N9QgFPOJ6JQpPZKFxa+dA==", + "license": "MIT", + "dependencies": { + "color-name": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@szmarczak/http-timer": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", @@ -3035,17 +3143,6 @@ "node": ">=14.16" } }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "license": "ISC", - "optional": true, - "peer": true, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -3087,9 +3184,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", - "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.0.tgz", + "integrity": "sha512-jnHMsrd0Mwa9Cf4IdOzbz543y4XJepXrbia2T4b6+spXC2We3t1y6K44D3mR8XMFSXMCf3/l7rCgddfx7UNVBA==", "dev": true, "license": "MIT", "dependencies": { @@ -3152,12 +3249,12 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.3.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", - "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "version": "24.9.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", + "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "license": "MIT", "dependencies": { - "undici-types": "~7.10.0" + "undici-types": "~7.16.0" } }, "node_modules/@types/nodemailer": { @@ -3199,26 +3296,36 @@ "license": "MIT" }, "node_modules/@types/send": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", "dev": true, "license": "MIT", "dependencies": { - "@types/mime": "^1", "@types/node": "*" } }, "node_modules/@types/serve-static": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", + "version": "1.15.10", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", + "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", "dev": true, "license": "MIT", "dependencies": { "@types/http-errors": "*", "@types/node": "*", - "@types/send": "*" + "@types/send": "<1" + } + }, + "node_modules/@types/serve-static/node_modules/@types/send": { + "version": "0.17.6", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", + "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" } }, "node_modules/@types/triple-beam": { @@ -3330,9 +3437,9 @@ } }, "node_modules/ansi-regex": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", - "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", "license": "MIT", "engines": { "node": ">=12" @@ -3425,9 +3532,9 @@ "license": "MIT" }, "node_modules/axios": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", - "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.0.tgz", + "integrity": "sha512-zt40Pz4zcRXra9CVV31KeyofwiNvAbJ5B6YPz9pMJ+yOSLikvPT4Yi5LjfgjRa9CawVYBaD1JQzIVcIvBejKeA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -3488,6 +3595,15 @@ ], "license": "MIT" }, + "node_modules/baseline-browser-mapping": { + "version": "2.8.20", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.20.tgz", + "integrity": "sha512-JMWsdF+O8Orq3EMukbUN1QfbLK9mX2CkUmQBcW2T0s8OmdAUL5LLM/6wFwSrqXzlXB13yhyK9gTKS1rIizOduQ==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -3506,6 +3622,15 @@ "bcrypt": "bin/bcrypt" } }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3612,9 +3737,9 @@ "license": "ISC" }, "node_modules/browserslist": { - "version": "4.25.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", - "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", + "version": "4.27.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", + "integrity": "sha512-AXVQwdhot1eqLihwasPElhX2tAZiBjWdJ9i/Zcj2S6QYIjkx62OKSfnobkriB81C3l4w0rVy3Nt4jaTBltYEpw==", "funding": [ { "type": "opencollective", @@ -3631,10 +3756,11 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001737", - "electron-to-chromium": "^1.5.211", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.8.19", + "caniuse-lite": "^1.0.30001751", + "electron-to-chromium": "^1.5.238", + "node-releases": "^2.0.26", + "update-browserslist-db": "^1.1.4" }, "bin": { "browserslist": "cli.js" @@ -3857,9 +3983,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001739", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", - "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", + "version": "1.0.30001751", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz", + "integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==", "funding": [ { "type": "opencollective", @@ -4128,41 +4254,6 @@ "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", "license": "MIT" }, - "node_modules/colorspace": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/colorspace/-/colorspace-1.1.4.tgz", - "integrity": "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w==", - "license": "MIT", - "dependencies": { - "color": "^3.1.3", - "text-hex": "1.0.x" - } - }, - "node_modules/colorspace/node_modules/color": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/color/-/color-3.2.1.tgz", - "integrity": "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA==", - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - } - }, - "node_modules/colorspace/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/colorspace/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "license": "MIT" - }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -4429,9 +4520,9 @@ } }, "node_modules/css-declaration-sorter": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", - "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==", + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.0.tgz", + "integrity": "sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ==", "license": "ISC", "engines": { "node": "^14 || ^16 || >=18" @@ -4457,14 +4548,12 @@ } }, "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", + "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "mdn-data": "2.0.30", + "mdn-data": "2.12.2", "source-map-js": "^1.0.1" }, "engines": { @@ -4790,9 +4879,9 @@ } }, "node_modules/detect-libc": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", - "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -4902,9 +4991,9 @@ } }, "node_modules/dompurify": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", - "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.0.tgz", + "integrity": "sha512-r+f6MYR1gGN1eJv0TVQbhA7if/U7P87cdPl3HN5rikqaBSBxLiCb/b9O+2eG0cxz0ghyU+mU1QkbsOwERMYlWQ==", "license": "(MPL-2.0 OR Apache-2.0)", "optionalDependencies": { "@types/trusted-types": "^2.0.7" @@ -4972,9 +5061,9 @@ "license": "MIT" }, "node_modules/electron-to-chromium": { - "version": "1.5.213", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.213.tgz", - "integrity": "sha512-xr9eRzSLNa4neDO0xVFrkXu3vyIzG4Ay08dApecw42Z1NbmCt+keEpXdvlYGVe0wtvY5dhW0Ay0lY0IOfsCg0Q==", + "version": "1.5.240", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.240.tgz", + "integrity": "sha512-OBwbZjWgrCOH+g6uJsA2/7Twpas2OlepS9uvByJjR2datRDuKGYeD+nP8lBBks2qnB7bGJNHDUx7c/YLaT3QMQ==", "license": "ISC" }, "node_modules/emitter-component": { @@ -5062,9 +5151,9 @@ } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" @@ -5116,9 +5205,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.10", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.10.tgz", - "integrity": "sha512-9RiGKvCwaqxO2owP61uQ4BgNborAQskMR6QusfWzQqv7AZOg5oGehdY2pRJMTKuwxd1IDBP4rSbI5lHzU7SMsQ==", + "version": "0.25.11", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.11.tgz", + "integrity": "sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -5129,32 +5218,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.10", - "@esbuild/android-arm": "0.25.10", - "@esbuild/android-arm64": "0.25.10", - "@esbuild/android-x64": "0.25.10", - "@esbuild/darwin-arm64": "0.25.10", - "@esbuild/darwin-x64": "0.25.10", - "@esbuild/freebsd-arm64": "0.25.10", - "@esbuild/freebsd-x64": "0.25.10", - "@esbuild/linux-arm": "0.25.10", - "@esbuild/linux-arm64": "0.25.10", - "@esbuild/linux-ia32": "0.25.10", - "@esbuild/linux-loong64": "0.25.10", - "@esbuild/linux-mips64el": "0.25.10", - "@esbuild/linux-ppc64": "0.25.10", - "@esbuild/linux-riscv64": "0.25.10", - "@esbuild/linux-s390x": "0.25.10", - "@esbuild/linux-x64": "0.25.10", - "@esbuild/netbsd-arm64": "0.25.10", - "@esbuild/netbsd-x64": "0.25.10", - "@esbuild/openbsd-arm64": "0.25.10", - "@esbuild/openbsd-x64": "0.25.10", - "@esbuild/openharmony-arm64": "0.25.10", - "@esbuild/sunos-x64": "0.25.10", - "@esbuild/win32-arm64": "0.25.10", - "@esbuild/win32-ia32": "0.25.10", - "@esbuild/win32-x64": "0.25.10" + "@esbuild/aix-ppc64": "0.25.11", + "@esbuild/android-arm": "0.25.11", + "@esbuild/android-arm64": "0.25.11", + "@esbuild/android-x64": "0.25.11", + "@esbuild/darwin-arm64": "0.25.11", + "@esbuild/darwin-x64": "0.25.11", + "@esbuild/freebsd-arm64": "0.25.11", + "@esbuild/freebsd-x64": "0.25.11", + "@esbuild/linux-arm": "0.25.11", + "@esbuild/linux-arm64": "0.25.11", + "@esbuild/linux-ia32": "0.25.11", + "@esbuild/linux-loong64": "0.25.11", + "@esbuild/linux-mips64el": "0.25.11", + "@esbuild/linux-ppc64": "0.25.11", + "@esbuild/linux-riscv64": "0.25.11", + "@esbuild/linux-s390x": "0.25.11", + "@esbuild/linux-x64": "0.25.11", + "@esbuild/netbsd-arm64": "0.25.11", + "@esbuild/netbsd-x64": "0.25.11", + "@esbuild/openbsd-arm64": "0.25.11", + "@esbuild/openbsd-x64": "0.25.11", + "@esbuild/openharmony-arm64": "0.25.11", + "@esbuild/sunos-x64": "0.25.11", + "@esbuild/win32-arm64": "0.25.11", + "@esbuild/win32-ia32": "0.25.11", + "@esbuild/win32-x64": "0.25.11" } }, "node_modules/escalade": { @@ -5198,25 +5287,24 @@ } }, "node_modules/eslint": { - "version": "9.34.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", - "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", + "version": "9.38.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.38.0.tgz", + "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", - "@eslint/config-array": "^0.21.0", - "@eslint/config-helpers": "^0.3.1", - "@eslint/core": "^0.15.2", + "@eslint/config-array": "^0.21.1", + "@eslint/config-helpers": "^0.4.1", + "@eslint/core": "^0.16.0", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.34.0", - "@eslint/plugin-kit": "^0.3.5", + "@eslint/js": "9.38.0", + "@eslint/plugin-kit": "^0.4.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", - "@types/json-schema": "^7.0.15", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", @@ -5570,9 +5658,9 @@ "license": "MIT" }, "node_modules/fast-xml-parser": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.3.tgz", - "integrity": "sha512-OdCYfRqfpuLUFonTNjvd30rCBZUneHpSQkCqfaeWQ9qrKcl6XlWeDBNVwGb+INAIxRshuN2jF+BE0L6gbBO2mw==", + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", "funding": [ { "type": "github", @@ -5849,19 +5937,19 @@ } }, "node_modules/gamedig": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/gamedig/-/gamedig-5.3.1.tgz", - "integrity": "sha512-RM/eCR8bAKEX+5dA9sxg4D6oQjS5t0MPsIxGquerBLSIu0f1hAVaDGC58Lp1srYwF6A1C2wYY2p9PzOrPTKf4Q==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/gamedig/-/gamedig-5.3.2.tgz", + "integrity": "sha512-R2b1LwjW783PZsHRl9M8R06UkvJwXmJ6PDKk48UukJQ9ktiwrCeAk90MAZx6nF3oA444uf7r5eHjfaYbNoSV+Q==", "license": "MIT", "dependencies": { - "fast-xml-parser": "5.2.3", + "fast-xml-parser": "5.2.5", "gbxremote": "0.2.1", "got": "13.0.0", - "iconv-lite": "0.6.3", + "iconv-lite": "0.7.0", "long": "5.3.2", "minimist": "1.2.8", "seek-bzip": "2.0.0", - "telnet-client": "2.2.5", + "telnet-client": "2.2.6", "varint": "6.0.0" }, "bin": { @@ -5948,15 +6036,19 @@ } }, "node_modules/gamedig/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.0.tgz", + "integrity": "sha512-cf6L2Ds3h57VVmkZe+Pn+5APsT7FpqJtEhhieDCvrE2MK5Qk9MyffgQyuxQTm6BChfeZNtcOLHp9IcWRVcIcBQ==", "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" }, "engines": { "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/gamedig/node_modules/p-cancelable": { @@ -6074,9 +6166,9 @@ } }, "node_modules/get-tsconfig": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", - "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "version": "4.13.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.0.tgz", + "integrity": "sha512-1VKTZJCwBrvbd+Wn3AOgQP/2Av+TfTCOlE4AcRJE72W1ksZXbAx8PPBR9RzgTeSPzlPMHrbANMH3LbltH73wxQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6302,9 +6394,9 @@ "license": "MIT" }, "node_modules/htmlnano": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-2.1.3.tgz", - "integrity": "sha512-mzTUHhxdfDw80X36rv5qFvjvor7r0uCwXI5mzo8CW61tImbe5Jpvl3JzPAetVh54wUYVuoa8x3qw8LFn8B3gHQ==", + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-2.1.5.tgz", + "integrity": "sha512-IXffzXq1beGQN2rsr03aIPK/rVU1jR2uwHymlAIEf97Tl5WdpG50IsQ5nWGvSGQJ+x6U7S6yac9rRiFgAg4/xQ==", "license": "MIT", "dependencies": { "@types/relateurl": "^0.2.33", @@ -6512,12 +6604,12 @@ "license": "ISC" }, "node_modules/ioredis": { - "version": "5.7.0", - "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.7.0.tgz", - "integrity": "sha512-NUcA93i1lukyXU+riqEyPtSEkyFq8tX90uL659J+qpCZ3rEdViB/APC58oAhIh3+bJln2hzdlZbBZsGNrlsR8g==", + "version": "5.8.2", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.8.2.tgz", + "integrity": "sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==", "license": "MIT", "dependencies": { - "@ioredis/commands": "^1.3.0", + "@ioredis/commands": "1.4.0", "cluster-key-slot": "^1.1.0", "debug": "^4.3.4", "denque": "^2.1.0", @@ -6670,16 +6762,192 @@ "license": "ISC" }, "node_modules/isomorphic-dompurify": { - "version": "2.26.0", - "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.26.0.tgz", - "integrity": "sha512-nZmoK4wKdzPs5USq4JHBiimjdKSVAOm2T1KyDoadtMPNXYHxiENd19ou4iU/V4juFM6LVgYQnpxCYmxqNP4Obw==", + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/isomorphic-dompurify/-/isomorphic-dompurify-2.30.1.tgz", + "integrity": "sha512-VJFbthRrns7BE+q3qSUJ5zxGNjuq4FqiaWXKCwnMoJbumnoQJoeOeOzP/oejKLPPtENckLWoDxGQiv5OkEFC+Q==", "license": "MIT", "dependencies": { - "dompurify": "^3.2.6", - "jsdom": "^26.1.0" + "dompurify": "^3.3.0", + "jsdom": "^27.0.1" }, "engines": { - "node": ">=18" + "node": ">=20.19.5" + } + }, + "node_modules/isomorphic-dompurify/node_modules/@asamuzakjp/css-color": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-4.0.5.tgz", + "integrity": "sha512-lMrXidNhPGsDjytDy11Vwlb6OIGrT3CmLg3VWNFyWkLWtijKl7xjvForlh8vuj0SHGjgl4qZEQzUmYTeQA2JFQ==", + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.4", + "@csstools/css-color-parser": "^3.1.0", + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "lru-cache": "^11.2.1" + } + }, + "node_modules/isomorphic-dompurify/node_modules/cssstyle": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-5.3.1.tgz", + "integrity": "sha512-g5PC9Aiph9eiczFpcgUhd9S4UUO3F+LHGRIi5NUMZ+4xtoIYbHNZwZnWA2JsFGe8OU8nl4WyaEFiZuGuxlutJQ==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^4.0.3", + "@csstools/css-syntax-patches-for-csstree": "^1.0.14", + "css-tree": "^3.1.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/isomorphic-dompurify/node_modules/data-urls": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-6.0.0.tgz", + "integrity": "sha512-BnBS08aLUM+DKamupXs3w2tJJoqU+AkaE/+6vQxi/G/DPmIZFJJp9Dkb1kM03AZx8ADehDUZgsNxju3mPXZYIA==", + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/isomorphic-dompurify/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/isomorphic-dompurify/node_modules/jsdom": { + "version": "27.0.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-27.0.1.tgz", + "integrity": "sha512-SNSQteBL1IlV2zqhwwolaG9CwhIhTvVHWg3kTss/cLE7H/X4644mtPQqYvCfsSrGQWt9hSZcgOXX8bOZaMN+kA==", + "license": "MIT", + "dependencies": { + "@asamuzakjp/dom-selector": "^6.7.2", + "cssstyle": "^5.3.1", + "data-urls": "^6.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "parse5": "^8.0.0", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^15.1.0", + "ws": "^8.18.3", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/isomorphic-dompurify/node_modules/lru-cache": { + "version": "11.2.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.2.tgz", + "integrity": "sha512-F9ODfyqML2coTIsQpSkRHnLSZMtkU8Q+mSfcaIyKwy58u+8k5nvAYeiNhsyMARvzNcXJ9QfWVrcPsC9e9rAxtg==", + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/isomorphic-dompurify/node_modules/parse5": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", + "integrity": "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/isomorphic-dompurify/node_modules/tldts": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.0.17.tgz", + "integrity": "sha512-Y1KQBgDd/NUc+LfOtKS6mNsC9CCaH+m2P1RoIZy7RAPo3C3/t8X45+zgut31cRZtZ3xKPjfn3TkGTrctC2TQIQ==", + "license": "MIT", + "dependencies": { + "tldts-core": "^7.0.17" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/isomorphic-dompurify/node_modules/tldts-core": { + "version": "7.0.17", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.0.17.tgz", + "integrity": "sha512-DieYoGrP78PWKsrXr8MZwtQ7GLCUeLxihtjC1jZsW1DnvSMdKPitJSe8OSYDM2u5H6g3kWJZpePqkp43TfLh0g==", + "license": "MIT" + }, + "node_modules/isomorphic-dompurify/node_modules/tough-cookie": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.0.tgz", + "integrity": "sha512-kXuRi1mtaKMrsLUxz3sQYvVl37B0Ns6MzfrtV5DvJceE9bPyspOqk9xxv7XbZWcfLWbFmm997vl83qUWVJA64w==", + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/isomorphic-dompurify/node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/isomorphic-dompurify/node_modules/webidl-conversions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.0.tgz", + "integrity": "sha512-n4W4YFyz5JzOfQeA8oN7dUYpR+MBP3PIUsn2jLjWXwK5ASUzt0Jc/A5sAUZoCYFJRGF0FBKJ+1JjN43rNdsQzA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/isomorphic-dompurify/node_modules/whatwg-url": { + "version": "15.1.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-15.1.0.tgz", + "integrity": "sha512-2ytDk0kiEj/yu90JOAp44PVPUkO9+jVhyf+SybKlRHSDlvOOZhdPIrr7xTH64l4WixO2cP+wQIcgujkGBPPz6g==", + "license": "MIT", + "dependencies": { + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.0" + }, + "engines": { + "node": ">=20" } }, "node_modules/isomorphic-unfetch": { @@ -6892,14 +7160,14 @@ } }, "node_modules/juice": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/juice/-/juice-11.0.1.tgz", - "integrity": "sha512-R3KLud4l/sN9AMmFZs0QY7cugGSiKvPhGyIsufCV5nJ0MjSlngUE7k80TmFeK9I62wOXrjWBtYA1knVs2OkF8w==", + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/juice/-/juice-11.0.3.tgz", + "integrity": "sha512-VYjPg4WylyWyLPnSiUsJ9tnnGhRZF0vn0YD8WWwaI8FhP9+1UdRMyRDbvqPOH/nBotmLKOc+FI+Oma6FwVWfSw==", "license": "MIT", "dependencies": { - "cheerio": "^1.0.0", + "cheerio": "1.0.0", "commander": "^12.1.0", - "entities": "^4.5.0", + "entities": "^7.0.0", "mensch": "^0.3.4", "slick": "^1.12.2", "web-resource-inliner": "^7.0.0" @@ -6912,34 +7180,46 @@ } }, "node_modules/juice/node_modules/cheerio": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.1.2.tgz", - "integrity": "sha512-IkxPpb5rS/d1IiLbHMgfPuS0FgiWTtFIm/Nj+2woXDLTZ7fOT2eqzgYbdMlLweqlHbsZjxEChoVK+7iph7jyQg==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0.tgz", + "integrity": "sha512-quS9HgjQpdaXOvsZz82Oz7uxtXiy6UIsIQcpBj7HRw2M63Skasm9qlDocAM7jNuaxdhpPU7c4kJN+gA5MCu4ww==", "license": "MIT", "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", - "domutils": "^3.2.2", - "encoding-sniffer": "^0.2.1", - "htmlparser2": "^10.0.0", - "parse5": "^7.3.0", - "parse5-htmlparser2-tree-adapter": "^7.1.0", + "domutils": "^3.1.0", + "encoding-sniffer": "^0.2.0", + "htmlparser2": "^9.1.0", + "parse5": "^7.1.2", + "parse5-htmlparser2-tree-adapter": "^7.0.0", "parse5-parser-stream": "^7.1.2", - "undici": "^7.12.0", + "undici": "^6.19.5", "whatwg-mimetype": "^4.0.0" }, "engines": { - "node": ">=20.18.1" + "node": ">=18.17" }, "funding": { "url": "https://github.com/cheeriojs/cheerio?sponsor=1" } }, + "node_modules/juice/node_modules/entities": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.0.tgz", + "integrity": "sha512-FDWG5cmEYf2Z00IkYRhbFrwIwvdFKH07uV8dvNy0omp/Qb1xcyCWp2UDtcwJF4QZZvk0sLudP6/hAu42TaqVhQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/juice/node_modules/htmlparser2": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", - "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", { @@ -6951,14 +7231,14 @@ "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.2.1", - "entities": "^6.0.0" + "domutils": "^3.1.0", + "entities": "^4.5.0" } }, "node_modules/juice/node_modules/htmlparser2/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "license": "BSD-2-Clause", "engines": { "node": ">=0.12" @@ -7218,9 +7498,9 @@ "license": "ISC" }, "node_modules/luxon": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.1.tgz", - "integrity": "sha512-RkRWjA926cTvz5rAb1BqyWkKbbjzCGchDUIKMCUvNi17j6f6j8uHGDV82Aqcqtzd+icoYpELmG3ksgGiFNNcNg==", + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", "license": "MIT", "engines": { "node": ">=12" @@ -7263,12 +7543,10 @@ } }, "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "license": "CC0-1.0", - "optional": true, - "peer": true + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", + "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", + "license": "CC0-1.0" }, "node_modules/media-typer": { "version": "0.3.0", @@ -7938,14 +8216,14 @@ } }, "node_modules/mongodb": { - "version": "6.19.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.19.0.tgz", - "integrity": "sha512-H3GtYujOJdeKIMLKBT9PwlDhGrQfplABNF1G904w6r5ZXKWyv77aB0X9B+rhmaAwjtllHzaEkvi9mkGVZxs2Bw==", + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.20.0.tgz", + "integrity": "sha512-Tl6MEIU3K4Rq3TSHd+sZQqRBoGlFsOgNrH5ltAcFBV62Re3Fd+FcaVf8uSEQFOJ51SDowDVttBTONMfoYWrWlQ==", "license": "Apache-2.0", "dependencies": { - "@mongodb-js/saslprep": "^1.1.9", + "@mongodb-js/saslprep": "^1.3.0", "bson": "^6.10.4", - "mongodb-connection-string-url": "^3.0.0" + "mongodb-connection-string-url": "^3.0.2" }, "engines": { "node": ">=16.20.1" @@ -7994,14 +8272,14 @@ } }, "node_modules/mongoose": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.18.0.tgz", - "integrity": "sha512-3TixPihQKBdyaYDeJqRjzgb86KbilEH07JmzV8SoSjgoskNTpa6oTBmDxeoF9p8YnWQoz7shnCyPkSV/48y3yw==", + "version": "8.19.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.19.2.tgz", + "integrity": "sha512-ww2T4dBV+suCbOfG5YPwj9pLCfUVyj8FEA1D3Ux1HHqutpLxGyOYEPU06iPRBW4cKr3PJfOSYsIpHWPTkz5zig==", "license": "MIT", "dependencies": { "bson": "^6.10.4", "kareem": "2.6.3", - "mongodb": "~6.18.0", + "mongodb": "~6.20.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -8015,52 +8293,6 @@ "url": "https://opencollective.com/mongoose" } }, - "node_modules/mongoose/node_modules/mongodb": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.18.0.tgz", - "integrity": "sha512-fO5ttN9VC8P0F5fqtQmclAkgXZxbIkYRTUi1j8JO6IYwvamkhtYDilJr35jOPELR49zqCJgXZWwCtW7B+TM8vQ==", - "license": "Apache-2.0", - "dependencies": { - "@mongodb-js/saslprep": "^1.1.9", - "bson": "^6.10.4", - "mongodb-connection-string-url": "^3.0.0" - }, - "engines": { - "node": ">=16.20.1" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", - "socks": "^2.7.1" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } - } - }, "node_modules/mpath": { "version": "0.9.0", "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz", @@ -8300,9 +8532,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.26", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", + "integrity": "sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==", "license": "MIT" }, "node_modules/nodemailer": { @@ -8324,9 +8556,9 @@ } }, "node_modules/normalize-url": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.0.2.tgz", - "integrity": "sha512-Ee/R3SyN4BuynXcnTaekmaVdbDAEiNrHqjQIA37mHU8G9pf7aaAD4ZX3XjBLo6rsdcxA/gtkcNYZLt30ACgynw==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz", + "integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==", "license": "MIT", "engines": { "node": ">=14.16" @@ -8354,9 +8586,9 @@ "license": "MIT" }, "node_modules/nwsapi": { - "version": "2.2.21", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.21.tgz", - "integrity": "sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==", + "version": "2.2.22", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.22.tgz", + "integrity": "sha512-ujSMe1OWVn55euT1ihwCI1ZcAaAU3nxUiDwfDQldc51ZXaB9m2AyOn6/jh1BLe2t/G8xd6uKG1UBF2aZJeg2SQ==", "license": "MIT" }, "node_modules/object-assign": { @@ -9093,59 +9325,6 @@ "postcss": "^8.4.32" } }, - "node_modules/postcss-svgo/node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", - "license": "MIT", - "engines": { - "node": ">=16" - } - }, - "node_modules/postcss-svgo/node_modules/css-tree": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.1.0.tgz", - "integrity": "sha512-0eW44TGN5SQXU1mWSkKwFstI/22X2bG1nYzZTYMAWjylYURhse752YgbE4Cx46AC+bAvI+/dYTPRk1LqSUnu6w==", - "license": "MIT", - "dependencies": { - "mdn-data": "2.12.2", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/postcss-svgo/node_modules/mdn-data": { - "version": "2.12.2", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.12.2.tgz", - "integrity": "sha512-IEn+pegP1aManZuckezWCO+XZQDplx1366JoVhTpMpBB1sPey/SbveZQUosKiKiGYjg1wH4pMlNgXbCiYgihQA==", - "license": "CC0-1.0" - }, - "node_modules/postcss-svgo/node_modules/svgo": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", - "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==", - "license": "MIT", - "dependencies": { - "commander": "^11.1.0", - "css-select": "^5.1.0", - "css-tree": "^3.0.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.1.1", - "sax": "^1.4.1" - }, - "bin": { - "svgo": "bin/svgo.js" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" - } - }, "node_modules/postcss-unique-selectors": { "version": "7.0.4", "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.4.tgz", @@ -9168,9 +9347,9 @@ "license": "MIT" }, "node_modules/posthtml": { - "version": "0.16.6", - "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.6.tgz", - "integrity": "sha512-JcEmHlyLK/o0uGAlj65vgg+7LIms0xKXe60lcDOTU7oVX/3LuEuLwrQpW3VJ7de5TaFKiW4kWkaIpJL42FEgxQ==", + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.7.tgz", + "integrity": "sha512-7Hc+IvlQ7hlaIfQFZnxlRl0jnpWq2qwibORBhQYIb0QbNtuicc5ZxvKkVT71HJ4Py1wSZ/3VR1r8LfkCtoCzhw==", "license": "MIT", "dependencies": { "posthtml-parser": "^0.11.0", @@ -9504,6 +9683,15 @@ "node": ">=0.10.0" } }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/resolve-alpn": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", @@ -9626,9 +9814,9 @@ } }, "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -9867,18 +10055,18 @@ } }, "node_modules/simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", "license": "MIT", "dependencies": { "is-arrayish": "^0.3.1" } }, "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", "license": "MIT" }, "node_modules/sinon": { @@ -10131,9 +10319,9 @@ } }, "node_modules/strip-ansi": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", - "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" @@ -10209,15 +10397,27 @@ } }, "node_modules/super-simple-scheduler": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/super-simple-scheduler/-/super-simple-scheduler-1.4.1.tgz", - "integrity": "sha512-jKVwxdRBXOHrUstA2pvO4jBZIA3CUinKiQ6k5C2Wn5tscoV3srppPxW0zG3VNKTCaGxo3yPn6Zaf3IDVBcAwRA==", + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/super-simple-scheduler/-/super-simple-scheduler-1.4.4.tgz", + "integrity": "sha512-o99zHFrrFECwj1FFE+ns4su5hKSYif+SUNB3fUJbaRWGkwLLoj19TRuRJYp72TYt9eDiCii68UBuKiLF4tlkMg==", "license": "MIT", "dependencies": { "uuid": "11.1.0", "winston": "3.17.0" } }, + "node_modules/super-simple-scheduler/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/super-simple-scheduler/node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", @@ -10231,6 +10431,28 @@ "uuid": "dist/esm/bin/uuid" } }, + "node_modules/super-simple-scheduler/node_modules/winston": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", + "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "license": "MIT", + "dependencies": { + "@colors/colors": "^1.6.0", + "@dabh/diagnostics": "^2.0.2", + "async": "^3.2.3", + "is-stream": "^2.0.0", + "logform": "^2.7.0", + "one-time": "^1.0.0", + "readable-stream": "^3.4.0", + "safe-stable-stringify": "^2.3.1", + "stack-trace": "0.0.x", + "triple-beam": "^1.3.0", + "winston-transport": "^4.9.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -10245,26 +10467,24 @@ } }, "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.0.tgz", + "integrity": "sha512-VvrHQ+9uniE+Mvx3+C9IEe/lWasXCU0nXMY2kZeLrHNICuRiC8uMPyM14UEaMOFA5mhyQqEkB02VoQ16n3DLaw==", "license": "MIT", - "optional": true, - "peer": true, "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", + "commander": "^11.1.0", "css-select": "^5.1.0", - "css-tree": "^2.3.1", + "css-tree": "^3.0.1", "css-what": "^6.1.0", "csso": "^5.0.5", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1", + "sax": "^1.4.1" }, "bin": { - "svgo": "bin/svgo" + "svgo": "bin/svgo.js" }, "engines": { - "node": ">=14.0.0" + "node": ">=16" }, "funding": { "type": "opencollective", @@ -10272,20 +10492,18 @@ } }, "node_modules/svgo/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", "license": "MIT", - "optional": true, - "peer": true, "engines": { - "node": ">= 10" + "node": ">=16" } }, "node_modules/swagger-ui-dist": { - "version": "5.28.1", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.28.1.tgz", - "integrity": "sha512-IvPrtNi8MvjiuDgoSmPYgg27Lvu38fnLD1OSd8Y103xXsPAqezVNnNeHnVCZ/d+CMXJblflGaIyHxAYIF3O71w==", + "version": "5.30.0", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.30.0.tgz", + "integrity": "sha512-BoiDSeT9PCtHfDYMgX5UpB/qTQy44UoSFRmzHqvhGfgxzEVPHxIW78a+HMLUIHnSFu3z63wjtbq6L6+Rto20Rw==", "license": "Apache-2.0", "dependencies": { "@scarf/scarf": "=1.4.0" @@ -10313,9 +10531,9 @@ "license": "MIT" }, "node_modules/tar-fs": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", - "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", "license": "MIT", "dependencies": { "chownr": "^1.1.1", @@ -10341,9 +10559,9 @@ } }, "node_modules/telnet-client": { - "version": "2.2.5", - "resolved": "https://registry.npmjs.org/telnet-client/-/telnet-client-2.2.5.tgz", - "integrity": "sha512-X5xEkmKHgpCpngnH7QnkFX87UyBErauHsjzlCGVp87MbhnmHoaAeacuALGfqovHh3MXAfrpPs+g30PgyBpy8Jw==", + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/telnet-client/-/telnet-client-2.2.6.tgz", + "integrity": "sha512-ZUYrLsPtQupQww3eSEORDVOb6ztdtKEghya6TVXPo2tg/UQq2pn5rHhvwuUvyYpbnsoqdNY1fyD1GNkXHR8dYA==", "license": "MIT", "dependencies": { "net": "^1.0.2", @@ -10587,18 +10805,18 @@ } }, "node_modules/undici": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.15.0.tgz", - "integrity": "sha512-7oZJCPvvMvTd0OlqWsIxTuItTpJBpU1tcbVl24FMn3xt3+VSunwUasmfPJRE57oNO1KsZ4PgA1xTdAX4hq8NyQ==", + "version": "6.22.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz", + "integrity": "sha512-hU/10obOIu62MGYjdskASR3CUAiYaFTtC9Pa6vHyf//mAipSvSQg6od2CnJswq7fvzNS3zJhxoRkgNVaHurWKw==", "license": "MIT", "engines": { - "node": ">=20.18.1" + "node": ">=18.17" } }, "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "version": "7.16.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", + "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", "license": "MIT" }, "node_modules/unfetch": { @@ -10620,9 +10838,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", + "integrity": "sha512-q0SPT4xyU84saUX+tomz1WLkxUbuaJnR1xWt17M7fJtEJigJeWUNGUqrauFXsHnqev9y9JTRGwk13tFBuKby4A==", "funding": [ { "type": "opencollective", @@ -10943,13 +11161,13 @@ } }, "node_modules/winston": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz", - "integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==", + "version": "3.18.3", + "resolved": "https://registry.npmjs.org/winston/-/winston-3.18.3.tgz", + "integrity": "sha512-NoBZauFNNWENgsnC9YpgyYwOVrl2m58PpQ8lNHjV3kosGs7KJ7Npk9pCUE+WJlawVSe8mykWDKWFSVfs3QO9ww==", "license": "MIT", "dependencies": { "@colors/colors": "^1.6.0", - "@dabh/diagnostics": "^2.0.2", + "@dabh/diagnostics": "^2.0.8", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", @@ -11090,9 +11308,9 @@ } }, "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", - "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", "license": "MIT", "engines": { "node": ">=12" diff --git a/server/package.json b/server/package.json index 74bd55804..380755b5b 100755 --- a/server/package.json +++ b/server/package.json @@ -51,7 +51,7 @@ "ping": "0.4.4", "sharp": "0.33.5", "ssl-checker": "2.0.10", - "super-simple-scheduler": "1.4.1", + "super-simple-scheduler": "1.4.4", "swagger-ui-express": "5.0.1", "winston": "^3.13.0" }, From 931104fbcfb9c76f7c7a5757f191a140bab4b325 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 27 Oct 2025 09:45:21 -0700 Subject: [PATCH 101/400] type error --- server/src/service/v2/business/MonitorService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/service/v2/business/MonitorService.ts b/server/src/service/v2/business/MonitorService.ts index c0570236b..fb338c348 100644 --- a/server/src/service/v2/business/MonitorService.ts +++ b/server/src/service/v2/business/MonitorService.ts @@ -381,7 +381,7 @@ class MonitorService implements IMonitorService { // Get monitor stats const monitorStats = await MonitorStats.findOne({ monitorId: monitor._id, - }).lean(); + }); if (!monitorStats) { throw new ApiError("Monitor stats not found", 404); From c9e1d35daf0f67bf58b82486ea61e6070a5d1f1f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 27 Oct 2025 10:27:45 -0700 Subject: [PATCH 102/400] bump version --- client/vite.config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/vite.config.ts b/client/vite.config.ts index 4c60ce2a4..5a6b428c7 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); export default defineConfig(({}) => { - let version = "3.2.0"; + let version = "3.2.1"; return { base: "/", From 32e4f7514d51d88a938bbfbdb35d8ad49225b044 Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Mon, 27 Oct 2025 17:29:47 +0000 Subject: [PATCH 103/400] feat: translations updated from POEditor --- client/src/locales/en.json | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 214a28221..13d4a7731 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -736,16 +736,6 @@ "webhookLabel": "Discord Webhook URL", "webhookPlaceholder": "https://your-server.com/webhook" }, - "matrixSettings": { - "title": "Matrix", - "description": "Configure your Matrix notification here", - "homeserverLabel": "Homeserver URL", - "homeserverPlaceholder": "https://matrix.org", - "roomIdLabel": "Room ID", - "roomIdPlaceholder": "!QMdRCpUIfLwsfjxye6:home.server", - "accessTokenLabel": "Access Token", - "accessTokenPlaceholder": "Your Matrix access token" - }, "webhookSettings": { "title": "Webhook", "description": "Configure your webhook here", From ce77b43705b1d552a100447a1afdc0536a433ed6 Mon Sep 17 00:00:00 2001 From: Abi <46168827+Abdullah85MBA@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:14:58 -0400 Subject: [PATCH 104/400] CodeCanvas: Save --- Checkmate.CodeCanvas | 11325 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 11325 insertions(+) create mode 100644 Checkmate.CodeCanvas diff --git a/Checkmate.CodeCanvas b/Checkmate.CodeCanvas new file mode 100644 index 000000000..623ea35bb --- /dev/null +++ b/Checkmate.CodeCanvas @@ -0,0 +1,11325 @@ +{ + "drawioXML": "\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n \n\n", + "fileName": "Checkmate.CodeCanvas", + "fileURL": "github", + "diagramTemplateVersion": 0.2, + "filePath": "Checkmate.CodeCanvas", + "repoData": { + "client": { + "path": "client", + "fileName": "client", + "cellName": "client", + "cellId": "0b23ea66-3e37-4643-b3b9-acb87ea02bae", + "visible": true, + "children": [ + "client/src" + ] + }, + "client/src": { + "path": "client/src", + "fileName": "src", + "cellName": "src", + "cellId": "d3264577-10f0-42fe-9411-7e9df0754d1e", + "visible": true, + "parentCellId": "0b23ea66-3e37-4643-b3b9-acb87ea02bae", + "children": [ + "client/src/Pages", + "client/src/Hooks", + "client/src/Utils", + "client/src/Routes" + ] + }, + "client/src/Hooks": { + "path": "client/src/Hooks", + "fileName": "Hooks", + "cellName": "Hooks", + "cellId": "4295a047-076f-4c07-95dd-b3123b6614f7", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e", + "children": [ + "client/src/Hooks/v1" + ] + }, + "client/src/Hooks/v1": { + "path": "client/src/Hooks/v1", + "fileName": "v1", + "cellName": "v1", + "cellId": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961", + "visible": true, + "parentCellId": "4295a047-076f-4c07-95dd-b3123b6614f7", + "children": [ + "client/src/Hooks/v1/monitorHooks.js", + "client/src/Hooks/v1/useNotifications.js", + "client/src/Hooks/v1/checkHooks.js", + "client/src/Hooks/v1/settingsHooks.js" + ] + }, + "client/src/Hooks/v1/checkHooks.js": { + "path": "client/src/Hooks/v1/checkHooks.js", + "fileName": "checkHooks.js", + "cellName": "checkHooks.js", + "cellId": "933a6cdc-349c-4725-b7ae-14cf780bfde8", + "visible": true, + "parentCellId": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961", + "children": [ + "client/src/Hooks/v1/checkHooks.js-simstep-8dc4347c-3e9e-40d7-ae6f-54684f2e6956", + "client/src/Hooks/v1/checkHooks.js-simstep-faba07f3-7460-47ae-924d-f92f5b2e821d" + ] + }, + "client/src/Hooks/v1/monitorHooks.js": { + "path": "client/src/Hooks/v1/monitorHooks.js", + "fileName": "monitorHooks.js", + "cellName": "monitorHooks.js", + "cellId": "595cf0e8-2f86-4543-b51c-b2aeac307f4b", + "visible": true, + "parentCellId": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961" + }, + "client/src/Hooks/v1/settingsHooks.js": { + "path": "client/src/Hooks/v1/settingsHooks.js", + "fileName": "settingsHooks.js", + "cellName": "settingsHooks.js", + "cellId": "8ee5d524-e61d-4e39-a729-babf54929def", + "visible": true, + "parentCellId": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961", + "children": [ + "client/src/Hooks/v1/settingsHooks.js-simstep-d7098072-9a83-4369-8176-9646ba34e186", + "client/src/Hooks/v1/settingsHooks.js-simstep-a157bd07-b969-44f8-914c-3d8393c1605a" + ] + }, + "client/src/Hooks/v1/useNotifications.js": { + "path": "client/src/Hooks/v1/useNotifications.js", + "fileName": "useNotifications.js", + "cellName": "useNotifications.js", + "cellId": "cd2428fd-6545-48dd-b685-5261311b7532", + "visible": true, + "parentCellId": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961", + "children": [ + "client/src/Hooks/v1/useNotifications.js-simstep-383c6be8-aa51-4398-9ae3-21035413f73a" + ] + }, + "client/src/Pages": { + "path": "client/src/Pages", + "fileName": "Pages", + "cellName": "Pages", + "cellId": "f0feab85-316a-41b5-a2f1-629220018846", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e", + "children": [ + "client/src/Pages/v1" + ] + }, + "client/src/Pages/v1": { + "path": "client/src/Pages/v1", + "fileName": "v1", + "cellName": "v1", + "cellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "visible": true, + "parentCellId": "f0feab85-316a-41b5-a2f1-629220018846", + "children": [ + "client/src/Pages/v1/Uptime", + "client/src/Pages/v1/Infrastructure", + "client/src/Pages/v1/Notifications", + "client/src/Pages/v1/StatusPage", + "client/src/Pages/v1/PageSpeed", + "client/src/Pages/v1/Maintenance", + "client/src/Pages/v1/Account", + "client/src/Pages/v1/Settings", + "client/src/Pages/v1/Logs" + ] + }, + "client/src/Pages/v1/Account": { + "path": "client/src/Pages/v1/Account", + "fileName": "Account", + "cellName": "Account", + "cellId": "3cad7666-0d1a-4e31-b6ca-70d9c3a46b19", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "children": [ + "client/src/Pages/v1/Account/index.jsx", + "client/src/Pages/v1/Account/components" + ] + }, + "client/src/Pages/v1/Account/components": { + "path": "client/src/Pages/v1/Account/components", + "fileName": "components", + "cellName": "components", + "cellId": "425a4c15-043f-4d05-8832-da00e18f97b9", + "visible": true, + "parentCellId": "3cad7666-0d1a-4e31-b6ca-70d9c3a46b19", + "children": [ + "client/src/Pages/v1/Account/components/TeamPanel.jsx" + ] + }, + "client/src/Pages/v1/Account/components/TeamPanel.jsx": { + "path": "client/src/Pages/v1/Account/components/TeamPanel.jsx", + "fileName": "TeamPanel.jsx", + "cellName": "TeamPanel.jsx", + "cellId": "1145f351-2257-411d-86f7-30bf8c26ad68", + "visible": true, + "parentCellId": "425a4c15-043f-4d05-8832-da00e18f97b9", + "children": [ + "client/src/Pages/v1/Account/components/TeamPanel.jsx-simstep-eebaca25-d588-49c4-a35c-9abc82fde580", + "client/src/Pages/v1/Account/components/TeamPanel.jsx-simstep-700f35e0-b39d-417c-b0a8-0612feade977" + ] + }, + "client/src/Pages/v1/Account/index.jsx": { + "path": "client/src/Pages/v1/Account/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "33960806-1b9e-4d61-a163-e4bebf82d87a", + "visible": true, + "parentCellId": "3cad7666-0d1a-4e31-b6ca-70d9c3a46b19", + "children": [ + "client/src/Pages/v1/Account/index.jsx-simstep-2d5a952a-ce62-4948-9309-96b28ad74e59" + ] + }, + "client/src/Pages/v1/Infrastructure": { + "path": "client/src/Pages/v1/Infrastructure", + "fileName": "Infrastructure", + "cellName": "Infrastructure", + "cellId": "3f26b33e-477f-41df-a78a-3e3e4f4209ae", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "children": [ + "client/src/Pages/v1/Infrastructure/Create", + "client/src/Pages/v1/Infrastructure/Details" + ] + }, + "client/src/Pages/v1/Infrastructure/Create": { + "path": "client/src/Pages/v1/Infrastructure/Create", + "fileName": "Create", + "cellName": "Create", + "cellId": "74019e8a-3148-4a32-aae2-5afd16f3eb9b", + "visible": true, + "parentCellId": "3f26b33e-477f-41df-a78a-3e3e4f4209ae", + "children": [ + "client/src/Pages/v1/Infrastructure/Create/Components" + ] + }, + "client/src/Pages/v1/Infrastructure/Create/Components": { + "path": "client/src/Pages/v1/Infrastructure/Create/Components", + "fileName": "Components", + "cellName": "Components", + "cellId": "ceded1fc-87a9-43c8-a756-746be3d4ad8d", + "visible": true, + "parentCellId": "74019e8a-3148-4a32-aae2-5afd16f3eb9b", + "children": [ + "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx" + ] + }, + "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx": { + "path": "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx", + "fileName": "CustomAlertsSection.jsx", + "cellName": "CustomAlertsSection.jsx", + "cellId": "83800be3-2a0e-4419-b379-3f995f4050e7", + "visible": true, + "parentCellId": "ceded1fc-87a9-43c8-a756-746be3d4ad8d", + "children": [ + "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx-simstep-80dd889a-1fc9-4d56-b9cf-5b45bf1110b1" + ] + }, + "client/src/Pages/v1/Infrastructure/Details": { + "path": "client/src/Pages/v1/Infrastructure/Details", + "fileName": "Details", + "cellName": "Details", + "cellId": "591128f9-0994-4e01-a1ab-f23357f97aff", + "visible": true, + "parentCellId": "3f26b33e-477f-41df-a78a-3e3e4f4209ae", + "children": [ + "client/src/Pages/v1/Infrastructure/Details/index.jsx", + "client/src/Pages/v1/Infrastructure/Details/Components" + ] + }, + "client/src/Pages/v1/Infrastructure/Details/Components": { + "path": "client/src/Pages/v1/Infrastructure/Details/Components", + "fileName": "Components", + "cellName": "Components", + "cellId": "e97d4e61-483e-4ac5-85a1-0ff72fcdd449", + "visible": true, + "parentCellId": "591128f9-0994-4e01-a1ab-f23357f97aff", + "children": [ + "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes" + ] + }, + "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes": { + "path": "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes", + "fileName": "GaugeBoxes", + "cellName": "GaugeBoxes", + "cellId": "6a7c7720-a399-4d4d-8225-36e0b2f59731", + "visible": true, + "parentCellId": "e97d4e61-483e-4ac5-85a1-0ff72fcdd449", + "children": [ + "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx" + ] + }, + "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx": { + "path": "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "8fe3d890-3605-407d-b06c-1d890a354e46", + "visible": true, + "parentCellId": "6a7c7720-a399-4d4d-8225-36e0b2f59731", + "children": [ + "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx-simstep-6eedce82-b9c8-4022-95e5-3a6e00628105" + ] + }, + "client/src/Pages/v1/Infrastructure/Details/index.jsx": { + "path": "client/src/Pages/v1/Infrastructure/Details/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "e292c552-b3df-4bac-bd9a-8f44c260a5aa", + "visible": true, + "parentCellId": "591128f9-0994-4e01-a1ab-f23357f97aff", + "children": [ + "client/src/Pages/v1/Infrastructure/Details/index.jsx-simstep-c27365db-9db6-4cc9-96f0-4f046fd38562" + ] + }, + "client/src/Pages/v1/Logs": { + "path": "client/src/Pages/v1/Logs", + "fileName": "Logs", + "cellName": "Logs", + "cellId": "1716ec41-8b42-4386-9850-55b8a607638e", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "children": [ + "client/src/Pages/v1/Logs/Diagnostics" + ] + }, + "client/src/Pages/v1/Logs/Diagnostics": { + "path": "client/src/Pages/v1/Logs/Diagnostics", + "fileName": "Diagnostics", + "cellName": "Diagnostics", + "cellId": "e0cb598c-e300-4fd3-8554-db7ffd6201f1", + "visible": true, + "parentCellId": "1716ec41-8b42-4386-9850-55b8a607638e", + "children": [ + "client/src/Pages/v1/Logs/Diagnostics/index.jsx" + ] + }, + "client/src/Pages/v1/Logs/Diagnostics/index.jsx": { + "path": "client/src/Pages/v1/Logs/Diagnostics/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "87e0e2ea-4a6d-460d-95bc-31e2efd9c9cf", + "visible": true, + "parentCellId": "e0cb598c-e300-4fd3-8554-db7ffd6201f1", + "children": [ + "client/src/Pages/v1/Logs/Diagnostics/index.jsx-simstep-a9b24b5f-534b-4baf-a185-bac7c32a1c42", + "client/src/Pages/v1/Logs/Diagnostics/index.jsx-simstep-e6c94f52-a599-4568-a06f-2db18f963817" + ] + }, + "client/src/Pages/v1/Maintenance": { + "path": "client/src/Pages/v1/Maintenance", + "fileName": "Maintenance", + "cellName": "Maintenance", + "cellId": "8021b55d-2d3a-4416-b598-e134e76c9434", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "children": [ + "client/src/Pages/v1/Maintenance/CreateMaintenance", + "client/src/Pages/v1/Maintenance/index.jsx", + "client/src/Pages/v1/Maintenance/MaintenanceTable" + ] + }, + "client/src/Pages/v1/Maintenance/CreateMaintenance": { + "path": "client/src/Pages/v1/Maintenance/CreateMaintenance", + "fileName": "CreateMaintenance", + "cellName": "CreateMaintenance", + "cellId": "6f7119ac-d65a-4fdf-a6f2-31052e0f5549", + "visible": true, + "parentCellId": "8021b55d-2d3a-4416-b598-e134e76c9434", + "children": [ + "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx", + "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks" + ] + }, + "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks": { + "path": "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks", + "fileName": "hooks", + "cellName": "hooks", + "cellId": "015a17fc-3fcf-45e0-8936-19f5e2ca86a3", + "visible": true, + "parentCellId": "6f7119ac-d65a-4fdf-a6f2-31052e0f5549", + "children": [ + "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx" + ] + }, + "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx": { + "path": "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx", + "fileName": "useMaintenanceActions.jsx", + "cellName": "useMaintenanceActions.jsx", + "cellId": "6ad0396e-9b38-4f5d-9ad9-6c9fcb4434f4", + "visible": true, + "parentCellId": "015a17fc-3fcf-45e0-8936-19f5e2ca86a3", + "children": [ + "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx-simstep-5e0d7085-099b-478a-afaa-47ad4dd07043" + ] + }, + "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx": { + "path": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "92c244d2-e6a7-4804-a643-1e8d36baf9c6", + "visible": true, + "parentCellId": "6f7119ac-d65a-4fdf-a6f2-31052e0f5549", + "children": [ + "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx-simstep-2401ef7a-8d8e-4056-868a-547ab213daf1", + "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx-simstep-ebe8fd52-f2a1-4a98-a71b-b04d6f4cdadc" + ] + }, + "client/src/Pages/v1/Maintenance/MaintenanceTable": { + "path": "client/src/Pages/v1/Maintenance/MaintenanceTable", + "fileName": "MaintenanceTable", + "cellName": "MaintenanceTable", + "cellId": "5ad4d33f-0932-479c-bb57-22ce6405758f", + "visible": true, + "parentCellId": "8021b55d-2d3a-4416-b598-e134e76c9434", + "children": [ + "client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx" + ] + }, + "client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx": { + "path": "client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "e7ec5ca9-6d25-4bf2-9695-639556f7d141", + "visible": true, + "parentCellId": "5ad4d33f-0932-479c-bb57-22ce6405758f", + "children": [ + "client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx-simstep-acf97639-e291-4ca6-81a6-d0487397f42a" + ] + }, + "client/src/Pages/v1/Maintenance/index.jsx": { + "path": "client/src/Pages/v1/Maintenance/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "1eae6263-aaa2-4206-b92e-1e67f529c1c9", + "visible": true, + "parentCellId": "8021b55d-2d3a-4416-b598-e134e76c9434", + "children": [ + "client/src/Pages/v1/Maintenance/index.jsx-simstep-82b5ea14-bb00-4511-aba4-76196029fd81" + ] + }, + "client/src/Pages/v1/Notifications": { + "path": "client/src/Pages/v1/Notifications", + "fileName": "Notifications", + "cellName": "Notifications", + "cellId": "b3575a92-df07-4cf7-810f-8e5e0672d666", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "children": [ + "client/src/Pages/v1/Notifications/create" + ] + }, + "client/src/Pages/v1/Notifications/create": { + "path": "client/src/Pages/v1/Notifications/create", + "fileName": "create", + "cellName": "create", + "cellId": "a6fb23f0-73bc-40ee-bb3c-ed93605a2bcb", + "visible": true, + "parentCellId": "b3575a92-df07-4cf7-810f-8e5e0672d666", + "children": [ + "client/src/Pages/v1/Notifications/create/index.jsx" + ] + }, + "client/src/Pages/v1/Notifications/create/index.jsx": { + "path": "client/src/Pages/v1/Notifications/create/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "b71b02fb-d08e-4fcf-b959-de1b9547d9c2", + "visible": true, + "parentCellId": "a6fb23f0-73bc-40ee-bb3c-ed93605a2bcb", + "children": [ + "client/src/Pages/v1/Notifications/create/index.jsx-simstep-e924ec15-1bea-4e3b-befb-a8bf30cbf740" + ] + }, + "client/src/Pages/v1/PageSpeed": { + "path": "client/src/Pages/v1/PageSpeed", + "fileName": "PageSpeed", + "cellName": "PageSpeed", + "cellId": "043d3dac-d4bb-449b-a3a7-270d53b66dac", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "children": [ + "client/src/Pages/v1/PageSpeed/Create", + "client/src/Pages/v1/PageSpeed/Details" + ] + }, + "client/src/Pages/v1/PageSpeed/Create": { + "path": "client/src/Pages/v1/PageSpeed/Create", + "fileName": "Create", + "cellName": "Create", + "cellId": "414c500b-7b68-42ba-8110-e855efbee750", + "visible": true, + "parentCellId": "043d3dac-d4bb-449b-a3a7-270d53b66dac", + "children": [ + "client/src/Pages/v1/PageSpeed/Create/index.jsx" + ] + }, + "client/src/Pages/v1/PageSpeed/Create/index.jsx": { + "path": "client/src/Pages/v1/PageSpeed/Create/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "5ffe0baa-054a-4e5b-8b29-1254f07ea975", + "visible": true, + "parentCellId": "414c500b-7b68-42ba-8110-e855efbee750", + "children": [ + "client/src/Pages/v1/PageSpeed/Create/index.jsx-simstep-b0cc6e6a-961e-4ea9-aa8a-6bfa44d6ba12" + ] + }, + "client/src/Pages/v1/PageSpeed/Details": { + "path": "client/src/Pages/v1/PageSpeed/Details", + "fileName": "Details", + "cellName": "Details", + "cellId": "581b4919-8b0c-4f0d-ae67-ad6de11d8c8b", + "visible": true, + "parentCellId": "043d3dac-d4bb-449b-a3a7-270d53b66dac", + "children": [ + "client/src/Pages/v1/PageSpeed/Details/index.jsx" + ] + }, + "client/src/Pages/v1/PageSpeed/Details/index.jsx": { + "path": "client/src/Pages/v1/PageSpeed/Details/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "c4a7bb58-1d9c-4690-b6b8-460c1cc33702", + "visible": true, + "parentCellId": "581b4919-8b0c-4f0d-ae67-ad6de11d8c8b", + "children": [ + "client/src/Pages/v1/PageSpeed/Details/index.jsx-simstep-0ec0eb06-05eb-4204-b077-9e47eb2cc316" + ] + }, + "client/src/Pages/v1/Settings": { + "path": "client/src/Pages/v1/Settings", + "fileName": "Settings", + "cellName": "Settings", + "cellId": "50f77798-f41b-4f5b-b096-7afc921a0419", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "children": [ + "client/src/Pages/v1/Settings/index.jsx", + "client/src/Pages/v1/Settings/SettingsEmail.jsx" + ] + }, + "client/src/Pages/v1/Settings/SettingsEmail.jsx": { + "path": "client/src/Pages/v1/Settings/SettingsEmail.jsx", + "fileName": "SettingsEmail.jsx", + "cellName": "SettingsEmail.jsx", + "cellId": "d8aa5b9e-f2a7-44c8-8813-d79c3f3238ee", + "visible": true, + "parentCellId": "50f77798-f41b-4f5b-b096-7afc921a0419", + "children": [ + "client/src/Pages/v1/Settings/SettingsEmail.jsx-simstep-ae5c5146-4dbb-4612-9d23-086e57f72474" + ] + }, + "client/src/Pages/v1/Settings/index.jsx": { + "path": "client/src/Pages/v1/Settings/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "d4b34195-7e1e-47a0-9303-603987c81f95", + "visible": true, + "parentCellId": "50f77798-f41b-4f5b-b096-7afc921a0419", + "children": [ + "client/src/Pages/v1/Settings/index.jsx-simstep-64e9f1af-13b7-4361-a2a9-3f45760e316d" + ] + }, + "client/src/Pages/v1/StatusPage": { + "path": "client/src/Pages/v1/StatusPage", + "fileName": "StatusPage", + "cellName": "StatusPage", + "cellId": "9d662180-8b65-4426-ad37-cfed4bc01fb4", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "children": [ + "client/src/Pages/v1/StatusPage/Create", + "client/src/Pages/v1/StatusPage/Status" + ] + }, + "client/src/Pages/v1/StatusPage/Create": { + "path": "client/src/Pages/v1/StatusPage/Create", + "fileName": "Create", + "cellName": "Create", + "cellId": "08602a28-f073-484e-9403-7eb798126dd4", + "visible": true, + "parentCellId": "9d662180-8b65-4426-ad37-cfed4bc01fb4", + "children": [ + "client/src/Pages/v1/StatusPage/Create/index.jsx", + "client/src/Pages/v1/StatusPage/Create/Hooks" + ] + }, + "client/src/Pages/v1/StatusPage/Create/Hooks": { + "path": "client/src/Pages/v1/StatusPage/Create/Hooks", + "fileName": "Hooks", + "cellName": "Hooks", + "cellId": "56f6775e-9843-4ca2-bc8c-1c263b6c35e9", + "visible": true, + "parentCellId": "08602a28-f073-484e-9403-7eb798126dd4", + "children": [ + "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx" + ] + }, + "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx": { + "path": "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx", + "fileName": "useCreateStatusPage.jsx", + "cellName": "useCreateStatusPage.jsx", + "cellId": "3945e05f-2d99-4d6d-9a3e-b6c75ea3c45a", + "visible": true, + "parentCellId": "56f6775e-9843-4ca2-bc8c-1c263b6c35e9", + "children": [ + "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx-simstep-3c99ae6a-b671-4f47-bed0-ddccd8ff5d35" + ] + }, + "client/src/Pages/v1/StatusPage/Create/index.jsx": { + "path": "client/src/Pages/v1/StatusPage/Create/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "e2d2bbf6-dffd-4edf-8c5d-34088d9d4745", + "visible": true, + "parentCellId": "08602a28-f073-484e-9403-7eb798126dd4", + "children": [ + "client/src/Pages/v1/StatusPage/Create/index.jsx-simstep-de7b5d1f-b370-42a4-a410-4178f1e2d3a6" + ] + }, + "client/src/Pages/v1/StatusPage/Status": { + "path": "client/src/Pages/v1/StatusPage/Status", + "fileName": "Status", + "cellName": "Status", + "cellId": "8ef70566-37d9-48fb-b5e8-37e5b68768a2", + "visible": true, + "parentCellId": "9d662180-8b65-4426-ad37-cfed4bc01fb4", + "children": [ + "client/src/Pages/v1/StatusPage/Status/index.jsx", + "client/src/Pages/v1/StatusPage/Status/Hooks" + ] + }, + "client/src/Pages/v1/StatusPage/Status/Hooks": { + "path": "client/src/Pages/v1/StatusPage/Status/Hooks", + "fileName": "Hooks", + "cellName": "Hooks", + "cellId": "79da0721-d810-4f7f-9cf9-3ea462bf0bff", + "visible": true, + "parentCellId": "8ef70566-37d9-48fb-b5e8-37e5b68768a2", + "children": [ + "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx" + ] + }, + "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx": { + "path": "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx", + "fileName": "useStatusPageFetch.jsx", + "cellName": "useStatusPageFetch.jsx", + "cellId": "5b2cdab8-66f1-40a1-9621-0efaf54e04ae", + "visible": true, + "parentCellId": "79da0721-d810-4f7f-9cf9-3ea462bf0bff", + "children": [ + "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx-simstep-b57bb249-9066-454e-aa8c-89108fdfcc2e" + ] + }, + "client/src/Pages/v1/StatusPage/Status/index.jsx": { + "path": "client/src/Pages/v1/StatusPage/Status/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "e1e6f5e0-fd8c-4260-aa4f-cdb096c6b1c3", + "visible": true, + "parentCellId": "8ef70566-37d9-48fb-b5e8-37e5b68768a2", + "children": [ + "client/src/Pages/v1/StatusPage/Status/index.jsx-simstep-3d51e811-a659-442e-97e0-00798cd15647" + ] + }, + "client/src/Pages/v1/Uptime": { + "path": "client/src/Pages/v1/Uptime", + "fileName": "Uptime", + "cellName": "Uptime", + "cellId": "7090fcdb-09be-4c85-86fd-e73553bbb2ff", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "children": [ + "client/src/Pages/v1/Uptime/Create", + "client/src/Pages/v1/Uptime/Details", + "client/src/Pages/v1/Uptime/BulkImport" + ] + }, + "client/src/Pages/v1/Uptime/BulkImport": { + "path": "client/src/Pages/v1/Uptime/BulkImport", + "fileName": "BulkImport", + "cellName": "BulkImport", + "cellId": "56dae13e-5735-4089-a9a4-e18be315aff7", + "visible": true, + "parentCellId": "7090fcdb-09be-4c85-86fd-e73553bbb2ff", + "children": [ + "client/src/Pages/v1/Uptime/BulkImport/index.jsx", + "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx" + ] + }, + "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx": { + "path": "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx", + "fileName": "Upload.jsx", + "cellName": "Upload.jsx", + "cellId": "cd84476a-1c8b-4781-811f-58fb9944365b", + "visible": true, + "parentCellId": "56dae13e-5735-4089-a9a4-e18be315aff7", + "children": [ + "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx-simstep-bbd04d55-fc6a-4ba8-bcde-5a4dc263d6eb" + ] + }, + "client/src/Pages/v1/Uptime/BulkImport/index.jsx": { + "path": "client/src/Pages/v1/Uptime/BulkImport/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "29b6ad3c-9538-47bc-8796-aab942dc4b71", + "visible": true, + "parentCellId": "56dae13e-5735-4089-a9a4-e18be315aff7", + "children": [ + "client/src/Pages/v1/Uptime/BulkImport/index.jsx-simstep-0139fcaa-eb46-4176-b7bd-1cb15e7f1f4c", + "client/src/Pages/v1/Uptime/BulkImport/index.jsx-simstep-72b3f16d-290d-4a61-8748-2ad90d63ec68" + ] + }, + "client/src/Pages/v1/Uptime/Create": { + "path": "client/src/Pages/v1/Uptime/Create", + "fileName": "Create", + "cellName": "Create", + "cellId": "3dba5acc-cf25-40b7-ad21-9cd715454cd5", + "visible": true, + "parentCellId": "7090fcdb-09be-4c85-86fd-e73553bbb2ff", + "children": [ + "client/src/Pages/v1/Uptime/Create/index.jsx" + ] + }, + "client/src/Pages/v1/Uptime/Create/index.jsx": { + "path": "client/src/Pages/v1/Uptime/Create/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "391ad99e-3114-4f54-a6ca-fd89b33c7e06", + "visible": true, + "parentCellId": "3dba5acc-cf25-40b7-ad21-9cd715454cd5", + "children": [ + "client/src/Pages/v1/Uptime/Create/index.jsx-simstep-ab5de0b9-c70d-492c-98d9-8ddee66284ad", + "client/src/Pages/v1/Uptime/Create/index.jsx-simstep-e02ddf57-270e-4f71-b4c4-d5183bb7879a" + ] + }, + "client/src/Pages/v1/Uptime/Details": { + "path": "client/src/Pages/v1/Uptime/Details", + "fileName": "Details", + "cellName": "Details", + "cellId": "273f3009-69d0-46fe-af57-3cd94a24b55b", + "visible": true, + "parentCellId": "7090fcdb-09be-4c85-86fd-e73553bbb2ff", + "children": [ + "client/src/Pages/v1/Uptime/Details/index.jsx", + "client/src/Pages/v1/Uptime/Details/Components" + ] + }, + "client/src/Pages/v1/Uptime/Details/Components": { + "path": "client/src/Pages/v1/Uptime/Details/Components", + "fileName": "Components", + "cellName": "Components", + "cellId": "6aed78bb-62f1-4ac1-9b9a-9d5977e87404", + "visible": true, + "parentCellId": "273f3009-69d0-46fe-af57-3cd94a24b55b", + "children": [ + "client/src/Pages/v1/Uptime/Details/Components/ResponseTable" + ] + }, + "client/src/Pages/v1/Uptime/Details/Components/ResponseTable": { + "path": "client/src/Pages/v1/Uptime/Details/Components/ResponseTable", + "fileName": "ResponseTable", + "cellName": "ResponseTable", + "cellId": "5ac0878f-d238-4763-8464-a85b4885a40f", + "visible": true, + "parentCellId": "6aed78bb-62f1-4ac1-9b9a-9d5977e87404", + "children": [ + "client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx" + ] + }, + "client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx": { + "path": "client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "d3f74ac7-3b9a-4cae-8d77-a0d5d605bb27", + "visible": true, + "parentCellId": "5ac0878f-d238-4763-8464-a85b4885a40f", + "children": [ + "client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx-simstep-2b1651cd-2e70-4cc6-b2bb-23f94fa73d66" + ] + }, + "client/src/Pages/v1/Uptime/Details/index.jsx": { + "path": "client/src/Pages/v1/Uptime/Details/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "1807d5b9-54be-430b-8997-cada4dd9b521", + "visible": true, + "parentCellId": "273f3009-69d0-46fe-af57-3cd94a24b55b", + "children": [ + "client/src/Pages/v1/Uptime/Details/index.jsx-simstep-35195328-9847-4678-b014-9331553f0d19" + ] + }, + "client/src/Routes": { + "path": "client/src/Routes", + "fileName": "Routes", + "cellName": "Routes", + "cellId": "987f145b-5df6-4c30-bf1c-2a2d2b0e5cdc", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e", + "children": [ + "client/src/Routes/index.jsx" + ] + }, + "client/src/Routes/index.jsx": { + "path": "client/src/Routes/index.jsx", + "fileName": "index.jsx", + "cellName": "index.jsx", + "cellId": "c4d7e7dd-75b1-44f1-a34a-6ed290a8e562", + "visible": true, + "parentCellId": "987f145b-5df6-4c30-bf1c-2a2d2b0e5cdc", + "children": [ + "client/src/Routes/index.jsx-simstep-132354d7-df7a-4e03-9d00-16d54f836a1b", + "client/src/Routes/index.jsx-simstep-a29da01e-8c5a-4e6d-8d3c-e49af528e4fa" + ] + }, + "client/src/Utils": { + "path": "client/src/Utils", + "fileName": "Utils", + "cellName": "Utils", + "cellId": "02b07589-ec21-4366-b7d2-9071e8f3bad8", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e", + "children": [ + "client/src/Utils/NetworkService.js" + ] + }, + "client/src/Utils/NetworkService.js": { + "path": "client/src/Utils/NetworkService.js", + "fileName": "NetworkService.js", + "cellName": "NetworkService.js", + "cellId": "16731e9d-3f73-4055-9d79-5ca9be679f68", + "visible": true, + "parentCellId": "02b07589-ec21-4366-b7d2-9071e8f3bad8", + "children": [ + "client/src/Utils/NetworkService.js-simstep-14e9986b-c6ce-475e-97ed-da1fa2b6968d", + "client/src/Utils/NetworkService.js-simstep-2fc087b3-033d-4cf5-81f2-e35b99a14bce" + ] + }, + "server": { + "path": "server", + "fileName": "server", + "cellName": "server", + "cellId": "98af25c7-efc2-43d2-a273-c8da4e1038ad", + "visible": true, + "children": [ + "server/src" + ] + }, + "server/src": { + "path": "server/src", + "fileName": "src", + "cellName": "src", + "cellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be", + "visible": true, + "parentCellId": "98af25c7-efc2-43d2-a273-c8da4e1038ad", + "children": [ + "server/src/routes", + "server/src/controllers", + "server/src/service", + "server/src/db", + "server/src/config", + "server/src/middleware" + ] + }, + "server/src/config": { + "path": "server/src/config", + "fileName": "config", + "cellName": "config", + "cellId": "fe47576c-c186-4950-82a5-b2fd24eb2561", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be", + "children": [ + "server/src/config/routes.js" + ] + }, + "server/src/config/routes.js": { + "path": "server/src/config/routes.js", + "fileName": "routes.js", + "cellName": "routes.js", + "cellId": "cb53d612-9b89-47da-911c-165d0382a7d8", + "visible": true, + "parentCellId": "fe47576c-c186-4950-82a5-b2fd24eb2561", + "children": [ + "server/src/config/routes.js-simstep-3e93ef65-8e2c-405b-8dc1-56d8e93937b3" + ] + }, + "server/src/controllers": { + "path": "server/src/controllers", + "fileName": "controllers", + "cellName": "controllers", + "cellId": "478e649c-1d1b-41d2-9b7b-6927e1dec7e1", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be", + "children": [ + "server/src/controllers/v1" + ] + }, + "server/src/controllers/v1": { + "path": "server/src/controllers/v1", + "fileName": "v1", + "cellName": "v1", + "cellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc", + "visible": true, + "parentCellId": "478e649c-1d1b-41d2-9b7b-6927e1dec7e1", + "children": [ + "server/src/controllers/v1/monitorController.js", + "server/src/controllers/v1/notificationController.js", + "server/src/controllers/v1/statusPageController.js", + "server/src/controllers/v1/checkController.js", + "server/src/controllers/v1/maintenanceWindowController.js", + "server/src/controllers/v1/inviteController.js", + "server/src/controllers/v1/settingsController.js", + "server/src/controllers/v1/diagnosticController.js" + ] + }, + "server/src/controllers/v1/checkController.js": { + "path": "server/src/controllers/v1/checkController.js", + "fileName": "checkController.js", + "cellName": "checkController.js", + "cellId": "d983e54f-2fbe-496a-932d-a827e1124804", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc", + "children": [ + "server/src/controllers/v1/checkController.js-simstep-5768fdc5-045c-48ef-b0c9-79769403648f" + ] + }, + "server/src/controllers/v1/diagnosticController.js": { + "path": "server/src/controllers/v1/diagnosticController.js", + "fileName": "diagnosticController.js", + "cellName": "diagnosticController.js", + "cellId": "5882419e-8a92-4edd-a71a-1252983f9af7", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "server/src/controllers/v1/inviteController.js": { + "path": "server/src/controllers/v1/inviteController.js", + "fileName": "inviteController.js", + "cellName": "inviteController.js", + "cellId": "6a186805-9d50-4bc3-9d56-e627a7ec2eb8", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "server/src/controllers/v1/maintenanceWindowController.js": { + "path": "server/src/controllers/v1/maintenanceWindowController.js", + "fileName": "maintenanceWindowController.js", + "cellName": "maintenanceWindowController.js", + "cellId": "a0cae18c-3c0e-40d3-ab03-eef7eace65d6", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc", + "children": [ + "server/src/controllers/v1/maintenanceWindowController.js-simstep-95fc1919-339d-4659-98cb-3dc50f662075", + "server/src/controllers/v1/maintenanceWindowController.js-simstep-1251da83-5949-4e24-a1d2-fc74ff785aa9" + ] + }, + "server/src/controllers/v1/monitorController.js": { + "path": "server/src/controllers/v1/monitorController.js", + "fileName": "monitorController.js", + "cellName": "monitorController.js", + "cellId": "341f2784-67ee-4477-9445-a8c240ee6261", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc", + "children": [ + "server/src/controllers/v1/monitorController.js-simstep-38fd069d-9c44-4078-954b-3a0899e9acf4", + "server/src/controllers/v1/monitorController.js-simstep-a3799570-64bf-481e-9ffe-31dbad8a36e4", + "server/src/controllers/v1/monitorController.js-simstep-f5377571-156f-4c5c-8bab-b6dacdf0fea6", + "server/src/controllers/v1/monitorController.js-simstep-d9db3f40-8add-4551-9323-274348b9800f", + "server/src/controllers/v1/monitorController.js-simstep-256095b8-5e3a-4c20-abd9-be9e6a9d0bad", + "server/src/controllers/v1/monitorController.js-simstep-60abdb27-4078-4678-a010-0f6938d4be48" + ] + }, + "server/src/controllers/v1/notificationController.js": { + "path": "server/src/controllers/v1/notificationController.js", + "fileName": "notificationController.js", + "cellName": "notificationController.js", + "cellId": "8e5941e1-e577-455f-bbf2-83a418ad5908", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "server/src/controllers/v1/settingsController.js": { + "path": "server/src/controllers/v1/settingsController.js", + "fileName": "settingsController.js", + "cellName": "settingsController.js", + "cellId": "a1fea5b0-fb7c-4189-9484-9d3312660767", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc", + "children": [ + "server/src/controllers/v1/settingsController.js-simstep-441b0c45-5f27-478d-8ca8-58fbd5f460ea", + "server/src/controllers/v1/settingsController.js-simstep-7632e8d2-4086-4169-ae3f-bfbf8d657987" + ] + }, + "server/src/controllers/v1/statusPageController.js": { + "path": "server/src/controllers/v1/statusPageController.js", + "fileName": "statusPageController.js", + "cellName": "statusPageController.js", + "cellId": "17a4f435-5a8e-4433-84a6-9ac17362709a", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc", + "children": [ + "server/src/controllers/v1/statusPageController.js-simstep-01694730-8a75-4949-8815-bb4d6e3e2b42", + "server/src/controllers/v1/statusPageController.js-simstep-2a115421-5175-4c8f-8cdd-62f3fe148af5", + "server/src/controllers/v1/statusPageController.js-simstep-5bdbd179-ce8f-4171-a073-af72a99b4468", + "server/src/controllers/v1/statusPageController.js-simstep-83224406-2f1d-4030-9beb-d65c2baf544d" + ] + }, + "server/src/db": { + "path": "server/src/db", + "fileName": "db", + "cellName": "db", + "cellId": "15ee5d81-a0cf-4614-80b8-7a1123ab20b8", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be", + "children": [ + "server/src/db/v1", + "server/src/db/v2" + ] + }, + "server/src/db/v1": { + "path": "server/src/db/v1", + "fileName": "v1", + "cellName": "v1", + "cellId": "58c5a3fd-1e69-4ca4-a64e-314f5644bdaf", + "visible": true, + "parentCellId": "15ee5d81-a0cf-4614-80b8-7a1123ab20b8", + "children": [ + "server/src/db/v1/modules" + ] + }, + "server/src/db/v1/modules": { + "path": "server/src/db/v1/modules", + "fileName": "modules", + "cellName": "modules", + "cellId": "82cb3dbe-c922-438a-a912-255376f4b380", + "visible": true, + "parentCellId": "58c5a3fd-1e69-4ca4-a64e-314f5644bdaf", + "children": [ + "server/src/db/v1/modules/monitorModule.js", + "server/src/db/v1/modules/monitorModuleQueries.js", + "server/src/db/v1/modules/notificationModule.js", + "server/src/db/v1/modules/statusPageModule.js", + "server/src/db/v1/modules/checkModule.js", + "server/src/db/v1/modules/maintenanceWindowModule.js", + "server/src/db/v1/modules/inviteModule.js" + ] + }, + "server/src/db/v1/modules/checkModule.js": { + "path": "server/src/db/v1/modules/checkModule.js", + "fileName": "checkModule.js", + "cellName": "checkModule.js", + "cellId": "860b5eec-a1ce-4e1f-a316-e1586f03af96", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380" + }, + "server/src/db/v1/modules/inviteModule.js": { + "path": "server/src/db/v1/modules/inviteModule.js", + "fileName": "inviteModule.js", + "cellName": "inviteModule.js", + "cellId": "08853f8a-f596-4df0-9190-dad04f54caef", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380" + }, + "server/src/db/v1/modules/maintenanceWindowModule.js": { + "path": "server/src/db/v1/modules/maintenanceWindowModule.js", + "fileName": "maintenanceWindowModule.js", + "cellName": "maintenanceWindowModule.js", + "cellId": "95ed6c41-ebeb-4c08-98c3-bab8b426789e", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380", + "children": [ + "server/src/db/v1/modules/maintenanceWindowModule.js-simstep-a3c47fd4-c701-46c2-9aff-94a667b3e368", + "server/src/db/v1/modules/maintenanceWindowModule.js-simstep-9bfbae5a-f487-40ef-86f5-04a1cc3c7662" + ] + }, + "server/src/db/v1/modules/monitorModule.js": { + "path": "server/src/db/v1/modules/monitorModule.js", + "fileName": "monitorModule.js", + "cellName": "monitorModule.js", + "cellId": "630b3ece-2648-48d5-99b5-ef9611de2ad5", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380", + "children": [ + "server/src/db/v1/modules/monitorModule.js-simstep-ca3ad309-323a-418d-a171-c16f56fae23c", + "server/src/db/v1/modules/monitorModule.js-simstep-8502ae1f-f029-446e-9dde-59c2aba87a8a", + "server/src/db/v1/modules/monitorModule.js-simstep-70f18c21-2918-402e-9c35-1bdce8bc82d4" + ] + }, + "server/src/db/v1/modules/monitorModuleQueries.js": { + "path": "server/src/db/v1/modules/monitorModuleQueries.js", + "fileName": "monitorModuleQueries.js", + "cellName": "monitorModuleQueries.js", + "cellId": "a0b22a5a-59e1-47e3-8218-a4ad083b1b71", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380" + }, + "server/src/db/v1/modules/notificationModule.js": { + "path": "server/src/db/v1/modules/notificationModule.js", + "fileName": "notificationModule.js", + "cellName": "notificationModule.js", + "cellId": "62daeac4-9402-448a-972c-cb8cb760671c", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380", + "children": [ + "server/src/db/v1/modules/notificationModule.js-simstep-d29ab4ab-be50-4f17-af6e-2c61cec9d0ab" + ] + }, + "server/src/db/v1/modules/statusPageModule.js": { + "path": "server/src/db/v1/modules/statusPageModule.js", + "fileName": "statusPageModule.js", + "cellName": "statusPageModule.js", + "cellId": "f8541fe3-4bcf-4833-a226-66b5301adf58", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380", + "children": [ + "server/src/db/v1/modules/statusPageModule.js-simstep-b6fa93cd-6fca-4d9e-969a-5400e2437ee8", + "server/src/db/v1/modules/statusPageModule.js-simstep-ceb044e9-6545-4d04-bf79-ec8146eb8ec9" + ] + }, + "server/src/db/v2": { + "path": "server/src/db/v2", + "fileName": "v2", + "cellName": "v2", + "cellId": "e2f99143-54b7-44dc-b566-1c2fb901bd16", + "visible": true, + "parentCellId": "15ee5d81-a0cf-4614-80b8-7a1123ab20b8", + "children": [ + "server/src/db/v2/models" + ] + }, + "server/src/db/v2/models": { + "path": "server/src/db/v2/models", + "fileName": "models", + "cellName": "models", + "cellId": "b53384d7-b989-4ac7-bd68-1c3621174ef4", + "visible": true, + "parentCellId": "e2f99143-54b7-44dc-b566-1c2fb901bd16", + "children": [ + "server/src/db/v2/models/checks" + ] + }, + "server/src/db/v2/models/checks": { + "path": "server/src/db/v2/models/checks", + "fileName": "checks", + "cellName": "checks", + "cellId": "64e3bb0a-67f5-4269-9e7d-94fe91ac78fc", + "visible": true, + "parentCellId": "b53384d7-b989-4ac7-bd68-1c3621174ef4", + "children": [ + "server/src/db/v2/models/checks/Check.ts" + ] + }, + "server/src/db/v2/models/checks/Check.ts": { + "path": "server/src/db/v2/models/checks/Check.ts", + "fileName": "Check.ts", + "cellName": "Check.ts", + "cellId": "1425ec43-f0f4-4c8b-a140-176c079219d6", + "visible": true, + "parentCellId": "64e3bb0a-67f5-4269-9e7d-94fe91ac78fc" + }, + "server/src/middleware": { + "path": "server/src/middleware", + "fileName": "middleware", + "cellName": "middleware", + "cellId": "a968db5d-8f6d-4fd7-9df0-8b332b521703", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be", + "children": [ + "server/src/middleware/v1" + ] + }, + "server/src/middleware/v1": { + "path": "server/src/middleware/v1", + "fileName": "v1", + "cellName": "v1", + "cellId": "09082f73-e215-465f-9206-0a963fb4c7cb", + "visible": true, + "parentCellId": "a968db5d-8f6d-4fd7-9df0-8b332b521703", + "children": [ + "server/src/middleware/v1/isAllowed.js" + ] + }, + "server/src/middleware/v1/isAllowed.js": { + "path": "server/src/middleware/v1/isAllowed.js", + "fileName": "isAllowed.js", + "cellName": "isAllowed.js", + "cellId": "001fa2e4-eadd-4474-b2ec-79d7ea43465f", + "visible": true, + "parentCellId": "09082f73-e215-465f-9206-0a963fb4c7cb", + "children": [ + "server/src/middleware/v1/isAllowed.js-simstep-50dd7316-7d01-43f7-9036-d97d51a9c178" + ] + }, + "server/src/routes": { + "path": "server/src/routes", + "fileName": "routes", + "cellName": "routes", + "cellId": "ffaaf471-ac3c-4ea3-a5a9-d5255169e54b", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be", + "children": [ + "server/src/routes/v1" + ] + }, + "server/src/routes/v1": { + "path": "server/src/routes/v1", + "fileName": "v1", + "cellName": "v1", + "cellId": "d3375a20-2776-4771-b806-649bbeb541bd", + "visible": true, + "parentCellId": "ffaaf471-ac3c-4ea3-a5a9-d5255169e54b", + "children": [ + "server/src/routes/v1/monitorRoute.js", + "server/src/routes/v1/notificationRoute.js", + "server/src/routes/v1/statusPageRoute.js", + "server/src/routes/v1/checkRoute.js", + "server/src/routes/v1/maintenanceWindowRoute.js", + "server/src/routes/v1/inviteRoute.js" + ] + }, + "server/src/routes/v1/checkRoute.js": { + "path": "server/src/routes/v1/checkRoute.js", + "fileName": "checkRoute.js", + "cellName": "checkRoute.js", + "cellId": "d18dbe1d-2f2c-4336-bd21-47c1670f7bab", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd", + "children": [ + "server/src/routes/v1/checkRoute.js-simstep-c5fd192f-ea28-48a0-b3e7-5bc04f521192" + ] + }, + "server/src/routes/v1/inviteRoute.js": { + "path": "server/src/routes/v1/inviteRoute.js", + "fileName": "inviteRoute.js", + "cellName": "inviteRoute.js", + "cellId": "9edafb8a-3661-4a7e-8510-a47f3cbc7564", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd" + }, + "server/src/routes/v1/maintenanceWindowRoute.js": { + "path": "server/src/routes/v1/maintenanceWindowRoute.js", + "fileName": "maintenanceWindowRoute.js", + "cellName": "maintenanceWindowRoute.js", + "cellId": "5f21f109-1967-416f-a649-9b48e78b90f6", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd" + }, + "server/src/routes/v1/monitorRoute.js": { + "path": "server/src/routes/v1/monitorRoute.js", + "fileName": "monitorRoute.js", + "cellName": "monitorRoute.js", + "cellId": "10611636-ec73-4a26-87a5-654ae64a4843", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd", + "children": [ + "server/src/routes/v1/monitorRoute.js-simstep-07bf3d83-047d-4e51-9f96-8c9c64848500", + "server/src/routes/v1/monitorRoute.js-simstep-63d8d81f-f436-4594-a6eb-8c0fd2110bc1" + ] + }, + "server/src/routes/v1/notificationRoute.js": { + "path": "server/src/routes/v1/notificationRoute.js", + "fileName": "notificationRoute.js", + "cellName": "notificationRoute.js", + "cellId": "96250197-23af-4059-9904-fd6751f70ad5", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd", + "children": [ + "server/src/routes/v1/notificationRoute.js-simstep-2f9266c9-4cbc-4b50-a280-cf8b5800d540", + "server/src/routes/v1/notificationRoute.js-simstep-c1b17b64-90e9-48bc-8927-25d7ebfc3036" + ] + }, + "server/src/routes/v1/statusPageRoute.js": { + "path": "server/src/routes/v1/statusPageRoute.js", + "fileName": "statusPageRoute.js", + "cellName": "statusPageRoute.js", + "cellId": "773e1065-b0a5-46a9-b0cc-6b963f69f5cc", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd", + "children": [ + "server/src/routes/v1/statusPageRoute.js-simstep-84f11567-b3c4-42a2-aa7f-d78c1412099a", + "server/src/routes/v1/statusPageRoute.js-simstep-477c6821-b29b-4b38-8930-ced21b8a75e5" + ] + }, + "server/src/service": { + "path": "server/src/service", + "fileName": "service", + "cellName": "service", + "cellId": "99e9b746-7809-41f4-a433-1de7cc90574b", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be", + "children": [ + "server/src/service/v1", + "server/src/service/v2" + ] + }, + "server/src/service/v1": { + "path": "server/src/service/v1", + "fileName": "v1", + "cellName": "v1", + "cellId": "bbbe53d6-f595-498a-ae45-e08611f39c3d", + "visible": true, + "parentCellId": "99e9b746-7809-41f4-a433-1de7cc90574b", + "children": [ + "server/src/service/v1/business", + "server/src/service/v1/infrastructure" + ] + }, + "server/src/service/v1/business": { + "path": "server/src/service/v1/business", + "fileName": "business", + "cellName": "business", + "cellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb", + "visible": true, + "parentCellId": "bbbe53d6-f595-498a-ae45-e08611f39c3d", + "children": [ + "server/src/service/v1/business/monitorService.js", + "server/src/service/v1/business/checkService.js", + "server/src/service/v1/business/maintenanceWindowService.js", + "server/src/service/v1/business/inviteService.js", + "server/src/service/v1/business/diagnosticService.js" + ] + }, + "server/src/service/v1/business/checkService.js": { + "path": "server/src/service/v1/business/checkService.js", + "fileName": "checkService.js", + "cellName": "checkService.js", + "cellId": "f90db867-e1e7-4554-b446-fa78fc6a8578", + "visible": true, + "parentCellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb", + "children": [ + "server/src/service/v1/business/checkService.js-simstep-83fea05d-8306-4a73-a43f-882cdd14f50f" + ] + }, + "server/src/service/v1/business/diagnosticService.js": { + "path": "server/src/service/v1/business/diagnosticService.js", + "fileName": "diagnosticService.js", + "cellName": "diagnosticService.js", + "cellId": "643b26c1-3bfb-4c41-8179-96dfc237d99e", + "visible": true, + "parentCellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb", + "children": [ + "server/src/service/v1/business/diagnosticService.js-simstep-8fb704ca-67d9-437c-90d8-01302634d568" + ] + }, + "server/src/service/v1/business/inviteService.js": { + "path": "server/src/service/v1/business/inviteService.js", + "fileName": "inviteService.js", + "cellName": "inviteService.js", + "cellId": "570278b7-9b72-4299-9fd9-46b253b48e9b", + "visible": true, + "parentCellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb", + "children": [ + "server/src/service/v1/business/inviteService.js-simstep-55fe9963-30d2-47f8-859f-b485a77064c4", + "server/src/service/v1/business/inviteService.js-simstep-7d1f86f2-c8d5-48fb-a8c1-b337dc56271f" + ] + }, + "server/src/service/v1/business/maintenanceWindowService.js": { + "path": "server/src/service/v1/business/maintenanceWindowService.js", + "fileName": "maintenanceWindowService.js", + "cellName": "maintenanceWindowService.js", + "cellId": "26b8daff-aa48-4baa-9d6c-05f771bf0990", + "visible": true, + "parentCellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb", + "children": [ + "server/src/service/v1/business/maintenanceWindowService.js-simstep-6e2cf739-1514-4c37-9874-10b68a47010e" + ] + }, + "server/src/service/v1/business/monitorService.js": { + "path": "server/src/service/v1/business/monitorService.js", + "fileName": "monitorService.js", + "cellName": "monitorService.js", + "cellId": "b4e479df-9b21-4430-9383-84831f503c2e", + "visible": true, + "parentCellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb", + "children": [ + "server/src/service/v1/business/monitorService.js-simstep-2c7c3082-e4b6-4525-860f-2ffcd9e86db1", + "server/src/service/v1/business/monitorService.js-simstep-66fbf913-e662-4bd9-ada6-077be9e37f07", + "server/src/service/v1/business/monitorService.js-simstep-85cd1d5d-7ffb-49c5-aacb-907048003fc5" + ] + }, + "server/src/service/v1/infrastructure": { + "path": "server/src/service/v1/infrastructure", + "fileName": "infrastructure", + "cellName": "infrastructure", + "cellId": "1670815f-860d-4ea4-92e1-b238a799d3dc", + "visible": true, + "parentCellId": "bbbe53d6-f595-498a-ae45-e08611f39c3d", + "children": [ + "server/src/service/v1/infrastructure/SuperSimpleQueue", + "server/src/service/v1/infrastructure/networkService.js", + "server/src/service/v1/infrastructure/statusService.js", + "server/src/service/v1/infrastructure/notificationService.js", + "server/src/service/v1/infrastructure/notificationUtils.js" + ] + }, + "server/src/service/v1/infrastructure/SuperSimpleQueue": { + "path": "server/src/service/v1/infrastructure/SuperSimpleQueue", + "fileName": "SuperSimpleQueue", + "cellName": "SuperSimpleQueue", + "cellId": "dda70a36-2d1d-4e46-8bcc-52d0d474bf7b", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc", + "children": [ + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js", + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js" + ] + }, + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js": { + "path": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js", + "fileName": "SuperSimpleQueue.js", + "cellName": "SuperSimpleQueue.js", + "cellId": "d9c0127e-31aa-4509-b5c9-f7017d10d69a", + "visible": true, + "parentCellId": "dda70a36-2d1d-4e46-8bcc-52d0d474bf7b", + "children": [ + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js-simstep-2702bfca-7611-4562-8d3e-a05af8257871" + ] + }, + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js": { + "path": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "fileName": "SuperSimpleQueueHelper.js", + "cellName": "SuperSimpleQueueHelper.js", + "cellId": "04c12e9a-e025-457c-a634-5f7472a42985", + "visible": true, + "parentCellId": "dda70a36-2d1d-4e46-8bcc-52d0d474bf7b", + "children": [ + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-e22a708a-a348-4775-8ee3-f32a35446e24", + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-a47affaf-b952-46d6-9709-6120a77690e1", + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-2127e749-80b4-499a-ae14-8dc2dcc9db9b", + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-6ba6b9d1-5fc3-454e-8744-2022b5192ed9", + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-e59fe64e-f06b-4855-b631-923bb5a68540" + ] + }, + "server/src/service/v1/infrastructure/networkService.js": { + "path": "server/src/service/v1/infrastructure/networkService.js", + "fileName": "networkService.js", + "cellName": "networkService.js", + "cellId": "00406786-9663-440a-96b6-919d544732c0", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc", + "children": [ + "server/src/service/v1/infrastructure/networkService.js-simstep-84f82fea-e306-4e0b-8222-e9705803d51a", + "server/src/service/v1/infrastructure/networkService.js-simstep-ed6e7faf-69dd-4d0b-bda8-137739ed288b" + ] + }, + "server/src/service/v1/infrastructure/notificationService.js": { + "path": "server/src/service/v1/infrastructure/notificationService.js", + "fileName": "notificationService.js", + "cellName": "notificationService.js", + "cellId": "2ff1f030-2080-48da-bbbe-90302fdfc2f7", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc", + "children": [ + "server/src/service/v1/infrastructure/notificationService.js-simstep-1a260a75-cf50-4f7a-90b0-c280b512a5cb", + "server/src/service/v1/infrastructure/notificationService.js-simstep-f29f9be1-a0cd-43a3-b259-e5105a25ce41" + ] + }, + "server/src/service/v1/infrastructure/notificationUtils.js": { + "path": "server/src/service/v1/infrastructure/notificationUtils.js", + "fileName": "notificationUtils.js", + "cellName": "notificationUtils.js", + "cellId": "4940977f-933d-4bbc-b19e-749647b44d25", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc", + "children": [ + "server/src/service/v1/infrastructure/notificationUtils.js-simstep-07fe8489-1d94-4afb-83f8-7cebb0dff49a" + ] + }, + "server/src/service/v1/infrastructure/statusService.js": { + "path": "server/src/service/v1/infrastructure/statusService.js", + "fileName": "statusService.js", + "cellName": "statusService.js", + "cellId": "fbd7d244-2f37-453c-8c83-a23a260b26cc", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc", + "children": [ + "server/src/service/v1/infrastructure/statusService.js-simstep-c66a04c6-113e-4391-ab71-93fc612279ef", + "server/src/service/v1/infrastructure/statusService.js-simstep-e5b90cf2-9369-4741-9803-c835a81c009b" + ] + }, + "server/src/service/v2": { + "path": "server/src/service/v2", + "fileName": "v2", + "cellName": "v2", + "cellId": "feefae96-61b8-4ac5-a37c-592ed4b2330e", + "visible": true, + "parentCellId": "99e9b746-7809-41f4-a433-1de7cc90574b", + "children": [ + "server/src/service/v2/infrastructure", + "server/src/service/v2/business" + ] + }, + "server/src/service/v2/business": { + "path": "server/src/service/v2/business", + "fileName": "business", + "cellName": "business", + "cellId": "7ef79284-534a-466f-a1f1-4e3a2f56be9d", + "visible": true, + "parentCellId": "feefae96-61b8-4ac5-a37c-592ed4b2330e", + "children": [ + "server/src/service/v2/business/CheckService.ts", + "server/src/service/v2/business/MonitorService.ts" + ] + }, + "server/src/service/v2/business/CheckService.ts": { + "path": "server/src/service/v2/business/CheckService.ts", + "fileName": "CheckService.ts", + "cellName": "CheckService.ts", + "cellId": "f0ebd5da-21bb-4d96-8ff9-ca10409acc80", + "visible": true, + "parentCellId": "7ef79284-534a-466f-a1f1-4e3a2f56be9d", + "children": [ + "server/src/service/v2/business/CheckService.ts-simstep-01206383-2db8-4b18-84d0-be2a0289c196" + ] + }, + "server/src/service/v2/business/MonitorService.ts": { + "path": "server/src/service/v2/business/MonitorService.ts", + "fileName": "MonitorService.ts", + "cellName": "MonitorService.ts", + "cellId": "7a3824cd-24c8-4b32-8b1d-e0bf357262c4", + "visible": true, + "parentCellId": "7ef79284-534a-466f-a1f1-4e3a2f56be9d", + "children": [ + "server/src/service/v2/business/MonitorService.ts-simstep-5616ac6d-c281-4879-82f1-6590c721c8f7" + ] + }, + "server/src/service/v2/infrastructure": { + "path": "server/src/service/v2/infrastructure", + "fileName": "infrastructure", + "cellName": "infrastructure", + "cellId": "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f", + "visible": true, + "parentCellId": "feefae96-61b8-4ac5-a37c-592ed4b2330e", + "children": [ + "server/src/service/v2/infrastructure/JobGenerator.ts", + "server/src/service/v2/infrastructure/NotificationService.ts", + "server/src/service/v2/infrastructure/NetworkService.ts" + ] + }, + "server/src/service/v2/infrastructure/JobGenerator.ts": { + "path": "server/src/service/v2/infrastructure/JobGenerator.ts", + "fileName": "JobGenerator.ts", + "cellName": "JobGenerator.ts", + "cellId": "159e569d-31d8-4883-b443-4ec08b21a044", + "visible": true, + "parentCellId": "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f", + "children": [ + "server/src/service/v2/infrastructure/JobGenerator.ts-simstep-157cec87-9b76-4a1c-9d85-0a6d3decbaa3", + "server/src/service/v2/infrastructure/JobGenerator.ts-simstep-55ad645b-118a-478e-a00c-09ef82357008" + ] + }, + "server/src/service/v2/infrastructure/NetworkService.ts": { + "path": "server/src/service/v2/infrastructure/NetworkService.ts", + "fileName": "NetworkService.ts", + "cellName": "NetworkService.ts", + "cellId": "50aeed0b-bc13-43f7-bb15-cb1b12b2caa7", + "visible": true, + "parentCellId": "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f", + "children": [ + "server/src/service/v2/infrastructure/NetworkService.ts-simstep-53968267-0c6e-42f4-88b3-cd90f44532ae" + ] + }, + "server/src/service/v2/infrastructure/NotificationService.ts": { + "path": "server/src/service/v2/infrastructure/NotificationService.ts", + "fileName": "NotificationService.ts", + "cellName": "NotificationService.ts", + "cellId": "c6d9907f-0a05-4153-a27f-47c3a1c4c0aa", + "visible": true, + "parentCellId": "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f", + "children": [ + "server/src/service/v2/infrastructure/NotificationService.ts-simstep-8d1176f3-c755-401d-9ca8-234689b4b837" + ] + }, + "0b23ea66-3e37-4643-b3b9-acb87ea02bae": { + "path": "0b23ea66-3e37-4643-b3b9-acb87ea02bae", + "cellName": "client", + "cellId": "0b23ea66-3e37-4643-b3b9-acb87ea02bae", + "visible": true + }, + "98af25c7-efc2-43d2-a273-c8da4e1038ad": { + "path": "98af25c7-efc2-43d2-a273-c8da4e1038ad", + "cellName": "server", + "cellId": "98af25c7-efc2-43d2-a273-c8da4e1038ad", + "visible": true + }, + "d3264577-10f0-42fe-9411-7e9df0754d1e": { + "path": "d3264577-10f0-42fe-9411-7e9df0754d1e", + "cellName": "src", + "cellId": "d3264577-10f0-42fe-9411-7e9df0754d1e", + "visible": true, + "parentCellId": "0b23ea66-3e37-4643-b3b9-acb87ea02bae" + }, + "35864a2c-7bd0-4428-84fc-c0aa90c7f1be": { + "path": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be", + "cellName": "src", + "cellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be", + "visible": true, + "parentCellId": "98af25c7-efc2-43d2-a273-c8da4e1038ad" + }, + "f0feab85-316a-41b5-a2f1-629220018846": { + "path": "f0feab85-316a-41b5-a2f1-629220018846", + "cellName": "Pages", + "cellId": "f0feab85-316a-41b5-a2f1-629220018846", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "4295a047-076f-4c07-95dd-b3123b6614f7": { + "path": "4295a047-076f-4c07-95dd-b3123b6614f7", + "cellName": "Hooks", + "cellId": "4295a047-076f-4c07-95dd-b3123b6614f7", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "02b07589-ec21-4366-b7d2-9071e8f3bad8": { + "path": "02b07589-ec21-4366-b7d2-9071e8f3bad8", + "cellName": "Utils", + "cellId": "02b07589-ec21-4366-b7d2-9071e8f3bad8", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "ffaaf471-ac3c-4ea3-a5a9-d5255169e54b": { + "path": "ffaaf471-ac3c-4ea3-a5a9-d5255169e54b", + "cellName": "routes", + "cellId": "ffaaf471-ac3c-4ea3-a5a9-d5255169e54b", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "478e649c-1d1b-41d2-9b7b-6927e1dec7e1": { + "path": "478e649c-1d1b-41d2-9b7b-6927e1dec7e1", + "cellName": "controllers", + "cellId": "478e649c-1d1b-41d2-9b7b-6927e1dec7e1", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "99e9b746-7809-41f4-a433-1de7cc90574b": { + "path": "99e9b746-7809-41f4-a433-1de7cc90574b", + "cellName": "service", + "cellId": "99e9b746-7809-41f4-a433-1de7cc90574b", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "15ee5d81-a0cf-4614-80b8-7a1123ab20b8": { + "path": "15ee5d81-a0cf-4614-80b8-7a1123ab20b8", + "cellName": "db", + "cellId": "15ee5d81-a0cf-4614-80b8-7a1123ab20b8", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "1a083b4a-787c-4eb4-8029-6700de987679": { + "path": "1a083b4a-787c-4eb4-8029-6700de987679", + "cellName": "v1", + "cellId": "1a083b4a-787c-4eb4-8029-6700de987679", + "visible": true, + "parentCellId": "f0feab85-316a-41b5-a2f1-629220018846" + }, + "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961": { + "path": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961", + "cellName": "v1", + "cellId": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961", + "visible": true, + "parentCellId": "4295a047-076f-4c07-95dd-b3123b6614f7" + }, + "16731e9d-3f73-4055-9d79-5ca9be679f68": { + "path": "16731e9d-3f73-4055-9d79-5ca9be679f68", + "cellName": "NetworkService.js", + "cellId": "16731e9d-3f73-4055-9d79-5ca9be679f68", + "visible": true, + "parentCellId": "02b07589-ec21-4366-b7d2-9071e8f3bad8" + }, + "d3375a20-2776-4771-b806-649bbeb541bd": { + "path": "d3375a20-2776-4771-b806-649bbeb541bd", + "cellName": "v1", + "cellId": "d3375a20-2776-4771-b806-649bbeb541bd", + "visible": true, + "parentCellId": "ffaaf471-ac3c-4ea3-a5a9-d5255169e54b" + }, + "fc591f7a-d4ac-4565-949d-a87a58a79bbc": { + "path": "fc591f7a-d4ac-4565-949d-a87a58a79bbc", + "cellName": "v1", + "cellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc", + "visible": true, + "parentCellId": "478e649c-1d1b-41d2-9b7b-6927e1dec7e1" + }, + "bbbe53d6-f595-498a-ae45-e08611f39c3d": { + "path": "bbbe53d6-f595-498a-ae45-e08611f39c3d", + "cellName": "v1", + "cellId": "bbbe53d6-f595-498a-ae45-e08611f39c3d", + "visible": true, + "parentCellId": "99e9b746-7809-41f4-a433-1de7cc90574b" + }, + "58c5a3fd-1e69-4ca4-a64e-314f5644bdaf": { + "path": "58c5a3fd-1e69-4ca4-a64e-314f5644bdaf", + "cellName": "v1", + "cellId": "58c5a3fd-1e69-4ca4-a64e-314f5644bdaf", + "visible": true, + "parentCellId": "15ee5d81-a0cf-4614-80b8-7a1123ab20b8" + }, + "7090fcdb-09be-4c85-86fd-e73553bbb2ff": { + "path": "7090fcdb-09be-4c85-86fd-e73553bbb2ff", + "cellName": "Uptime", + "cellId": "7090fcdb-09be-4c85-86fd-e73553bbb2ff", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679" + }, + "595cf0e8-2f86-4543-b51c-b2aeac307f4b": { + "path": "595cf0e8-2f86-4543-b51c-b2aeac307f4b", + "cellName": "monitorHooks.js", + "cellId": "595cf0e8-2f86-4543-b51c-b2aeac307f4b", + "visible": true, + "parentCellId": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961" + }, + "10611636-ec73-4a26-87a5-654ae64a4843": { + "path": "10611636-ec73-4a26-87a5-654ae64a4843", + "cellName": "monitorRoute.js", + "cellId": "10611636-ec73-4a26-87a5-654ae64a4843", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd" + }, + "341f2784-67ee-4477-9445-a8c240ee6261": { + "path": "341f2784-67ee-4477-9445-a8c240ee6261", + "cellName": "monitorController.js", + "cellId": "341f2784-67ee-4477-9445-a8c240ee6261", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "87d3b101-9443-4ed4-9c4f-f759d41677eb": { + "path": "87d3b101-9443-4ed4-9c4f-f759d41677eb", + "cellName": "business", + "cellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb", + "visible": true, + "parentCellId": "bbbe53d6-f595-498a-ae45-e08611f39c3d" + }, + "1670815f-860d-4ea4-92e1-b238a799d3dc": { + "path": "1670815f-860d-4ea4-92e1-b238a799d3dc", + "cellName": "infrastructure", + "cellId": "1670815f-860d-4ea4-92e1-b238a799d3dc", + "visible": true, + "parentCellId": "bbbe53d6-f595-498a-ae45-e08611f39c3d" + }, + "82cb3dbe-c922-438a-a912-255376f4b380": { + "path": "82cb3dbe-c922-438a-a912-255376f4b380", + "cellName": "modules", + "cellId": "82cb3dbe-c922-438a-a912-255376f4b380", + "visible": true, + "parentCellId": "58c5a3fd-1e69-4ca4-a64e-314f5644bdaf" + }, + "3dba5acc-cf25-40b7-ad21-9cd715454cd5": { + "path": "3dba5acc-cf25-40b7-ad21-9cd715454cd5", + "cellName": "Create", + "cellId": "3dba5acc-cf25-40b7-ad21-9cd715454cd5", + "visible": true, + "parentCellId": "7090fcdb-09be-4c85-86fd-e73553bbb2ff" + }, + "b4e479df-9b21-4430-9383-84831f503c2e": { + "path": "b4e479df-9b21-4430-9383-84831f503c2e", + "cellName": "monitorService.js", + "cellId": "b4e479df-9b21-4430-9383-84831f503c2e", + "visible": true, + "parentCellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb" + }, + "dda70a36-2d1d-4e46-8bcc-52d0d474bf7b": { + "path": "dda70a36-2d1d-4e46-8bcc-52d0d474bf7b", + "cellName": "SuperSimpleQueue", + "cellId": "dda70a36-2d1d-4e46-8bcc-52d0d474bf7b", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc" + }, + "00406786-9663-440a-96b6-919d544732c0": { + "path": "00406786-9663-440a-96b6-919d544732c0", + "cellName": "networkService.js", + "cellId": "00406786-9663-440a-96b6-919d544732c0", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc" + }, + "fbd7d244-2f37-453c-8c83-a23a260b26cc": { + "path": "fbd7d244-2f37-453c-8c83-a23a260b26cc", + "cellName": "statusService.js", + "cellId": "fbd7d244-2f37-453c-8c83-a23a260b26cc", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc" + }, + "2ff1f030-2080-48da-bbbe-90302fdfc2f7": { + "path": "2ff1f030-2080-48da-bbbe-90302fdfc2f7", + "cellName": "notificationService.js", + "cellId": "2ff1f030-2080-48da-bbbe-90302fdfc2f7", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc" + }, + "630b3ece-2648-48d5-99b5-ef9611de2ad5": { + "path": "630b3ece-2648-48d5-99b5-ef9611de2ad5", + "cellName": "monitorModule.js", + "cellId": "630b3ece-2648-48d5-99b5-ef9611de2ad5", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380" + }, + "391ad99e-3114-4f54-a6ca-fd89b33c7e06": { + "path": "391ad99e-3114-4f54-a6ca-fd89b33c7e06", + "cellName": "index.jsx", + "cellId": "391ad99e-3114-4f54-a6ca-fd89b33c7e06", + "visible": true, + "parentCellId": "3dba5acc-cf25-40b7-ad21-9cd715454cd5" + }, + "d9c0127e-31aa-4509-b5c9-f7017d10d69a": { + "path": "d9c0127e-31aa-4509-b5c9-f7017d10d69a", + "cellName": "SuperSimpleQueue.js", + "cellId": "d9c0127e-31aa-4509-b5c9-f7017d10d69a", + "visible": true, + "parentCellId": "dda70a36-2d1d-4e46-8bcc-52d0d474bf7b" + }, + "04c12e9a-e025-457c-a634-5f7472a42985": { + "path": "04c12e9a-e025-457c-a634-5f7472a42985", + "cellName": "SuperSimpleQueueHelper.js", + "cellId": "04c12e9a-e025-457c-a634-5f7472a42985", + "visible": true, + "parentCellId": "dda70a36-2d1d-4e46-8bcc-52d0d474bf7b" + }, + "7a2da928-a65c-49d8-b0a3-c159a6df2bc5": { + "path": "7a2da928-a65c-49d8-b0a3-c159a6df2bc5", + "cellName": "Uptime Monitor Creation: Display Form - index.jsx:L315-802", + "cellId": "7a2da928-a65c-49d8-b0a3-c159a6df2bc5", + "visible": true, + "parentCellId": "391ad99e-3114-4f54-a6ca-fd89b33c7e06" + }, + "client/src/Pages/v1/Uptime/Create/index.jsx-simstep-ab5de0b9-c70d-492c-98d9-8ddee66284ad": { + "path": "client/src/Pages/v1/Uptime/Create/index.jsx-simstep-ab5de0b9-c70d-492c-98d9-8ddee66284ad", + "fileName": "index.jsx", + "wiki": "The user navigates to the 'Create Uptime Monitor' page. The `UptimeCreate` React component renders the form, allowing the user to select the monitor type (HTTP, Ping, Docker, etc.) and input details like URL, name, and check interval.", + "cellName": "Uptime Monitor Creation: Display Form - index.jsx:L315-802", + "cellId": "7a2da928-a65c-49d8-b0a3-c159a6df2bc5", + "visible": true, + "startLine": 315, + "endLine": 802, + "parentCellId": "391ad99e-3114-4f54-a6ca-fd89b33c7e06", + "parentPath": "client/src/Pages/v1/Uptime/Create/index.jsx", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "ab5de0b9-c70d-492c-98d9-8ddee66284ad" + } + ] + }, + "a81f4ae8-41fe-4d1b-95ce-86abcc67b53c": { + "path": "a81f4ae8-41fe-4d1b-95ce-86abcc67b53c", + "cellName": "Uptime Monitor Creation: Handle Form Submission and Validation - index.jsx:L170-248", + "cellId": "a81f4ae8-41fe-4d1b-95ce-86abcc67b53c", + "visible": true, + "parentCellId": "391ad99e-3114-4f54-a6ca-fd89b33c7e06" + }, + "client/src/Pages/v1/Uptime/Create/index.jsx-simstep-e02ddf57-270e-4f71-b4c4-d5183bb7879a": { + "path": "client/src/Pages/v1/Uptime/Create/index.jsx-simstep-e02ddf57-270e-4f71-b4c4-d5183bb7879a", + "fileName": "index.jsx", + "wiki": "The `onSubmit` function in `UptimeCreate.jsx` is executed. It prevents the default form submission, constructs a `form` object with monitor data, validates it using `monitorValidation`, and then calls the `createMonitor` function from the `useCreateMonitor` hook.", + "cellName": "Uptime Monitor Creation: Handle Form Submission and Validation - index.jsx:L170-248", + "cellId": "a81f4ae8-41fe-4d1b-95ce-86abcc67b53c", + "visible": true, + "startLine": 170, + "endLine": 248, + "parentCellId": "391ad99e-3114-4f54-a6ca-fd89b33c7e06", + "parentPath": "client/src/Pages/v1/Uptime/Create/index.jsx", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "e02ddf57-270e-4f71-b4c4-d5183bb7879a" + } + ] + }, + "a30ac765-c546-4993-988e-8c8cfdb7532f": { + "path": "a30ac765-c546-4993-988e-8c8cfdb7532f", + "cellName": "Uptime Monitor Creation: Send API Request - NetworkService.js:L119-121", + "cellId": "a30ac765-c546-4993-988e-8c8cfdb7532f", + "visible": true, + "parentCellId": "16731e9d-3f73-4055-9d79-5ca9be679f68" + }, + "client/src/Utils/NetworkService.js-simstep-14e9986b-c6ce-475e-97ed-da1fa2b6968d": { + "path": "client/src/Utils/NetworkService.js-simstep-14e9986b-c6ce-475e-97ed-da1fa2b6968d", + "fileName": "NetworkService.js", + "wiki": "The `createMonitor` method in `NetworkService.js` makes an HTTP POST request to the `/monitors` endpoint on the server, sending the new monitor's data in the request body.", + "cellName": "Uptime Monitor Creation: Send API Request - NetworkService.js:L119-121", + "cellId": "a30ac765-c546-4993-988e-8c8cfdb7532f", + "visible": true, + "startLine": 119, + "endLine": 121, + "parentCellId": "16731e9d-3f73-4055-9d79-5ca9be679f68", + "parentPath": "client/src/Utils/NetworkService.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "14e9986b-c6ce-475e-97ed-da1fa2b6968d" + } + ] + }, + "f0409948-75dc-457f-a02d-6b9774258e48": { + "path": "f0409948-75dc-457f-a02d-6b9774258e48", + "cellName": "Uptime Monitor Creation: Controller to Service - monitorController.js:L195", + "cellId": "f0409948-75dc-457f-a02d-6b9774258e48", + "visible": true, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261" + }, + "server/src/controllers/v1/monitorController.js-simstep-38fd069d-9c44-4078-954b-3a0899e9acf4": { + "path": "server/src/controllers/v1/monitorController.js-simstep-38fd069d-9c44-4078-954b-3a0899e9acf4", + "fileName": "monitorController.js", + "wiki": "The `MonitorController` extracts the user and team ID from the request and calls the `createMonitor` method in the `MonitorService`, passing along the monitor data from the request body.", + "cellName": "Uptime Monitor Creation: Controller to Service - monitorController.js:L195", + "cellId": "f0409948-75dc-457f-a02d-6b9774258e48", + "visible": true, + "startLine": 195, + "endLine": 195, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261", + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "38fd069d-9c44-4078-954b-3a0899e9acf4" + } + ] + }, + "2d407bfd-e4b0-46b8-99e7-121c507d525f": { + "path": "2d407bfd-e4b0-46b8-99e7-121c507d525f", + "cellName": "Uptime Monitor Creation: Save Monitor to Database - monitorModule.js:L454-455", + "cellId": "2d407bfd-e4b0-46b8-99e7-121c507d525f", + "visible": true, + "parentCellId": "630b3ece-2648-48d5-99b5-ef9611de2ad5" + }, + "server/src/db/v1/modules/monitorModule.js-simstep-ca3ad309-323a-418d-a171-c16f56fae23c": { + "path": "server/src/db/v1/modules/monitorModule.js-simstep-ca3ad309-323a-418d-a171-c16f56fae23c", + "fileName": "monitorModule.js", + "wiki": "The `monitorModule` creates a new `Monitor` model instance with the provided data and saves it to the MongoDB database. The newly created document, including its generated `_id`, is returned.", + "cellName": "Uptime Monitor Creation: Save Monitor to Database - monitorModule.js:L454-455", + "cellId": "2d407bfd-e4b0-46b8-99e7-121c507d525f", + "visible": true, + "startLine": 454, + "endLine": 455, + "parentCellId": "630b3ece-2648-48d5-99b5-ef9611de2ad5", + "parentPath": "server/src/db/v1/modules/monitorModule.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "ca3ad309-323a-418d-a171-c16f56fae23c" + } + ] + }, + "51e8db5f-2fdd-4ddc-9605-43981db9d940": { + "path": "51e8db5f-2fdd-4ddc-9605-43981db9d940", + "cellName": "Uptime Monitor Creation: Schedule Job - SuperSimpleQueue.js:L50-57", + "cellId": "51e8db5f-2fdd-4ddc-9605-43981db9d940", + "visible": true, + "parentCellId": "d9c0127e-31aa-4509-b5c9-f7017d10d69a" + }, + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js-simstep-2702bfca-7611-4562-8d3e-a05af8257871": { + "path": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js-simstep-2702bfca-7611-4562-8d3e-a05af8257871", + "fileName": "SuperSimpleQueue.js", + "wiki": "The `SuperSimpleQueue` receives the new monitor and uses its `scheduler.addJob` method to create a recurring job based on the monitor's interval. The monitor object itself is stored as data for the job.", + "cellName": "Uptime Monitor Creation: Schedule Job - SuperSimpleQueue.js:L50-57", + "cellId": "51e8db5f-2fdd-4ddc-9605-43981db9d940", + "visible": true, + "startLine": 50, + "endLine": 57, + "parentCellId": "d9c0127e-31aa-4509-b5c9-f7017d10d69a", + "parentPath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "2702bfca-7611-4562-8d3e-a05af8257871" + } + ] + }, + "18c13391-0cf7-4303-8180-0a40cfd64b58": { + "path": "18c13391-0cf7-4303-8180-0a40cfd64b58", + "cellName": "Service Monitoring: Job Execution - SuperSimpleQueueHelper.js:L27-73", + "cellId": "18c13391-0cf7-4303-8180-0a40cfd64b58", + "visible": true, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985" + }, + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-e22a708a-a348-4775-8ee3-f32a35446e24": { + "path": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-e22a708a-a348-4775-8ee3-f32a35446e24", + "fileName": "SuperSimpleQueueHelper.js", + "wiki": "The scheduler triggers a monitor job at the specified interval. The `getMonitorJob` function in `SuperSimpleQueueHelper` is executed, which first checks if the monitor is in a maintenance window.", + "cellName": "Service Monitoring: Job Execution - SuperSimpleQueueHelper.js:L27-73", + "cellId": "18c13391-0cf7-4303-8180-0a40cfd64b58", + "visible": true, + "startLine": 27, + "endLine": 73, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985", + "parentPath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "e22a708a-a348-4775-8ee3-f32a35446e24" + } + ] + }, + "5c900373-e6ba-44d5-b341-676a32d659c8": { + "path": "5c900373-e6ba-44d5-b341-676a32d659c8", + "cellName": "Service Monitoring: Perform Network Check - networkService.js:L44-59", + "cellId": "5c900373-e6ba-44d5-b341-676a32d659c8", + "visible": true, + "parentCellId": "00406786-9663-440a-96b6-919d544732c0" + }, + "server/src/service/v1/infrastructure/networkService.js-simstep-84f82fea-e306-4e0b-8222-e9705803d51a": { + "path": "server/src/service/v1/infrastructure/networkService.js-simstep-84f82fea-e306-4e0b-8222-e9705803d51a", + "fileName": "networkService.js", + "wiki": "The `requestStatus` method in the network service acts as a router. Based on the monitor's `type` (e.g., 'http', 'ping', 'docker'), it calls the corresponding specialized request function (e.g., `requestHttp`) to perform the check and constructs a standardized response object.", + "cellName": "Service Monitoring: Perform Network Check - networkService.js:L44-59", + "cellId": "5c900373-e6ba-44d5-b341-676a32d659c8", + "visible": true, + "startLine": 44, + "endLine": 59, + "parentCellId": "00406786-9663-440a-96b6-919d544732c0", + "parentPath": "server/src/service/v1/infrastructure/networkService.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "84f82fea-e306-4e0b-8222-e9705803d51a" + } + ] + }, + "45e38f3a-a7c3-4de7-b3dc-589361d793fd": { + "path": "45e38f3a-a7c3-4de7-b3dc-589361d793fd", + "cellName": "Service Monitoring: Update Monitor Status - statusService.js:L108-183", + "cellId": "45e38f3a-a7c3-4de7-b3dc-589361d793fd", + "visible": true, + "parentCellId": "fbd7d244-2f37-453c-8c83-a23a260b26cc" + }, + "server/src/service/v1/infrastructure/statusService.js-simstep-c66a04c6-113e-4391-ab71-93fc612279ef": { + "path": "server/src/service/v1/infrastructure/statusService.js-simstep-c66a04c6-113e-4391-ab71-93fc612279ef", + "fileName": "statusService.js", + "wiki": "The job handler passes the network response to `statusService.updateStatus`. This service creates a new `Check` document in the database, updates the monitor's status based on a sliding window and threshold, and calculates running statistics.", + "cellName": "Service Monitoring: Update Monitor Status - statusService.js:L108-183", + "cellId": "45e38f3a-a7c3-4de7-b3dc-589361d793fd", + "visible": true, + "startLine": 108, + "endLine": 183, + "parentCellId": "fbd7d244-2f37-453c-8c83-a23a260b26cc", + "parentPath": "server/src/service/v1/infrastructure/statusService.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "c66a04c6-113e-4391-ab71-93fc612279ef" + } + ] + }, + "9f3ea7fc-16d3-4d36-97eb-66f8808425cc": { + "path": "9f3ea7fc-16d3-4d36-97eb-66f8808425cc", + "cellName": "Service Monitoring: Handle Notifications (Conditional) - notificationService.js:L76-92", + "cellId": "9f3ea7fc-16d3-4d36-97eb-66f8808425cc", + "visible": true, + "parentCellId": "2ff1f030-2080-48da-bbbe-90302fdfc2f7" + }, + "server/src/service/v1/infrastructure/notificationService.js-simstep-1a260a75-cf50-4f7a-90b0-c280b512a5cb": { + "path": "server/src/service/v1/infrastructure/notificationService.js-simstep-1a260a75-cf50-4f7a-90b0-c280b512a5cb", + "fileName": "notificationService.js", + "wiki": "If `statusChanged` is true, the job handler calls `notificationService.handleNotifications`. The service identifies the notification channels associated with the monitor and sends out alerts (e.g., email, webhook, Discord) with details about the status change.", + "cellName": "Service Monitoring: Handle Notifications (Conditional) - notificationService.js:L76-92", + "cellId": "9f3ea7fc-16d3-4d36-97eb-66f8808425cc", + "visible": true, + "startLine": 76, + "endLine": 92, + "parentCellId": "2ff1f030-2080-48da-bbbe-90302fdfc2f7", + "parentPath": "server/src/service/v1/infrastructure/notificationService.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "1a260a75-cf50-4f7a-90b0-c280b512a5cb" + } + ] + }, + "0e09aa33-d7c7-4c89-9da8-f0937658f4d3": { + "path": "0e09aa33-d7c7-4c89-9da8-f0937658f4d3", + "cellName": "Uptime Monitor\nCreation: Form\nSubmission", + "cellId": "0e09aa33-d7c7-4c89-9da8-f0937658f4d3", + "visible": true, + "parentCellId": "391ad99e-3114-4f54-a6ca-fd89b33c7e06" + }, + "generated-edge-simstep-888df086-41bb-469d-9c0a-daf74bdecb9f-0e09aa33-d7c7-4c89-9da8-f0937658f4d3": { + "path": "generated-edge-simstep-888df086-41bb-469d-9c0a-daf74bdecb9f-0e09aa33-d7c7-4c89-9da8-f0937658f4d3", + "fileName": "index.jsx", + "cellName": "Uptime Monitor Creation: Form Submission", + "cellId": "0e09aa33-d7c7-4c89-9da8-f0937658f4d3", + "visible": true, + "startLine": 435, + "endLine": 443, + "parentPath": "client/src/Pages/v1/Uptime/Create/index.jsx", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "888df086-41bb-469d-9c0a-daf74bdecb9f" + } + ] + }, + "1f6f04a9-0123-4729-b06a-8f3da6d1b58c": { + "path": "1f6f04a9-0123-4729-b06a-8f3da6d1b58c", + "cellName": "Uptime Monitor\nCreation: Call\nNetwork Service", + "cellId": "1f6f04a9-0123-4729-b06a-8f3da6d1b58c", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "generated-edge-simstep-3fe0afaa-c673-4b52-a0c4-02a3ffd3a075-1f6f04a9-0123-4729-b06a-8f3da6d1b58c": { + "path": "generated-edge-simstep-3fe0afaa-c673-4b52-a0c4-02a3ffd3a075-1f6f04a9-0123-4729-b06a-8f3da6d1b58c", + "fileName": "index.jsx", + "cellName": "Uptime Monitor Creation: Call Network Service", + "cellId": "1f6f04a9-0123-4729-b06a-8f3da6d1b58c", + "visible": true, + "startLine": 306, + "endLine": 306, + "parentPath": "client/src/Pages/v1/Uptime/Create/index.jsx", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "3fe0afaa-c673-4b52-a0c4-02a3ffd3a075" + } + ] + }, + "b6d963d5-c613-407a-9ce9-60a04048dea7": { + "path": "b6d963d5-c613-407a-9ce9-60a04048dea7", + "cellName": "Uptime Monitor\nCreation: API\nRoute to\nController", + "cellId": "b6d963d5-c613-407a-9ce9-60a04048dea7", + "visible": true + }, + "generated-edge-simstep-13dd0564-44a5-43ea-a163-ce1148cc48e7-b6d963d5-c613-407a-9ce9-60a04048dea7": { + "path": "generated-edge-simstep-13dd0564-44a5-43ea-a163-ce1148cc48e7-b6d963d5-c613-407a-9ce9-60a04048dea7", + "fileName": "NetworkService.js", + "cellName": "Uptime Monitor Creation: API Route to Controller", + "cellId": "b6d963d5-c613-407a-9ce9-60a04048dea7", + "visible": true, + "startLine": 35, + "endLine": 35, + "parentPath": "client/src/Utils/NetworkService.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "13dd0564-44a5-43ea-a163-ce1148cc48e7" + } + ] + }, + "a6bccd1d-4272-4df7-a7c8-b3979e37b8d7": { + "path": "a6bccd1d-4272-4df7-a7c8-b3979e37b8d7", + "cellName": "Uptime Monitor\nCreation: Service\nto DB\nModule", + "cellId": "a6bccd1d-4272-4df7-a7c8-b3979e37b8d7", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-a6f4c967-b1a8-4804-bc2e-67f47a7b68f9-a6bccd1d-4272-4df7-a7c8-b3979e37b8d7": { + "path": "generated-edge-simstep-a6f4c967-b1a8-4804-bc2e-67f47a7b68f9-a6bccd1d-4272-4df7-a7c8-b3979e37b8d7", + "fileName": "monitorController.js", + "cellName": "Uptime Monitor Creation: Service to DB Module", + "cellId": "a6bccd1d-4272-4df7-a7c8-b3979e37b8d7", + "visible": true, + "startLine": 74, + "endLine": 78, + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "a6f4c967-b1a8-4804-bc2e-67f47a7b68f9" + } + ] + }, + "de320ac1-0949-4297-be7c-b9d7a14e0fec": { + "path": "de320ac1-0949-4297-be7c-b9d7a14e0fec", + "cellName": "Uptime Monitor\nCreation: Service\nto Job\nQueue", + "cellId": "de320ac1-0949-4297-be7c-b9d7a14e0fec", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-71ec03b6-1166-4c5d-9ada-0875a0866e30-de320ac1-0949-4297-be7c-b9d7a14e0fec": { + "path": "generated-edge-simstep-71ec03b6-1166-4c5d-9ada-0875a0866e30-de320ac1-0949-4297-be7c-b9d7a14e0fec", + "fileName": "monitorModule.js", + "cellName": "Uptime Monitor Creation: Service to Job Queue", + "cellId": "de320ac1-0949-4297-be7c-b9d7a14e0fec", + "visible": true, + "startLine": 80, + "endLine": 80, + "parentPath": "server/src/db/v1/modules/monitorModule.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "71ec03b6-1166-4c5d-9ada-0875a0866e30" + } + ] + }, + "38ba9b22-5b26-432d-a856-0eceb792c737": { + "path": "38ba9b22-5b26-432d-a856-0eceb792c737", + "cellName": "Service Monitoring:\nRequest Service\nStatus", + "cellId": "38ba9b22-5b26-432d-a856-0eceb792c737", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc" + }, + "generated-edge-simstep-1d7580f2-f107-4008-a207-b8ec07998721-38ba9b22-5b26-432d-a856-0eceb792c737": { + "path": "generated-edge-simstep-1d7580f2-f107-4008-a207-b8ec07998721-38ba9b22-5b26-432d-a856-0eceb792c737", + "fileName": "SuperSimpleQueueHelper.js", + "cellName": "Service Monitoring: Request Service Status", + "cellId": "38ba9b22-5b26-432d-a856-0eceb792c737", + "visible": true, + "startLine": 45, + "endLine": 45, + "parentPath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "1d7580f2-f107-4008-a207-b8ec07998721" + } + ] + }, + "5e80734c-7b72-41be-9068-30cda3ffb425": { + "path": "5e80734c-7b72-41be-9068-30cda3ffb425", + "cellName": "Service Monitoring:\nPropagate Network\nResponse", + "cellId": "5e80734c-7b72-41be-9068-30cda3ffb425", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc" + }, + "generated-edge-simstep-9b0ada30-d3e6-4b2c-b3ff-f50615139588-5e80734c-7b72-41be-9068-30cda3ffb425": { + "path": "generated-edge-simstep-9b0ada30-d3e6-4b2c-b3ff-f50615139588-5e80734c-7b72-41be-9068-30cda3ffb425", + "fileName": "networkService.js", + "cellName": "Service Monitoring: Propagate Network Response", + "cellId": "5e80734c-7b72-41be-9068-30cda3ffb425", + "visible": true, + "startLine": 45, + "endLine": 45, + "parentPath": "server/src/service/v1/infrastructure/networkService.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "9b0ada30-d3e6-4b2c-b3ff-f50615139588" + } + ] + }, + "0a7e4a72-b644-4bd8-afa1-ae8a1d6c111c": { + "path": "0a7e4a72-b644-4bd8-afa1-ae8a1d6c111c", + "cellName": "Service Monitoring:\nCheck for\nStatus Change", + "cellId": "0a7e4a72-b644-4bd8-afa1-ae8a1d6c111c", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc" + }, + "generated-edge-simstep-9896c387-4fca-47ad-82eb-7301286decb7-0a7e4a72-b644-4bd8-afa1-ae8a1d6c111c": { + "path": "generated-edge-simstep-9896c387-4fca-47ad-82eb-7301286decb7-0a7e4a72-b644-4bd8-afa1-ae8a1d6c111c", + "fileName": "statusService.js", + "cellName": "Service Monitoring: Check for Status Change", + "cellId": "0a7e4a72-b644-4bd8-afa1-ae8a1d6c111c", + "visible": true, + "startLine": 51, + "endLine": 51, + "parentPath": "server/src/service/v1/infrastructure/statusService.js", + "simSteps": [ + { + "simulationKey": "Comprehensive Uptime Monitoring for Various Services", + "simStepId": "9896c387-4fca-47ad-82eb-7301286decb7" + } + ] + }, + "3f26b33e-477f-41df-a78a-3e3e4f4209ae": { + "path": "3f26b33e-477f-41df-a78a-3e3e4f4209ae", + "cellName": "Infrastructure", + "cellId": "3f26b33e-477f-41df-a78a-3e3e4f4209ae", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679" + }, + "74019e8a-3148-4a32-aae2-5afd16f3eb9b": { + "path": "74019e8a-3148-4a32-aae2-5afd16f3eb9b", + "cellName": "Create", + "cellId": "74019e8a-3148-4a32-aae2-5afd16f3eb9b", + "visible": true, + "parentCellId": "3f26b33e-477f-41df-a78a-3e3e4f4209ae" + }, + "591128f9-0994-4e01-a1ab-f23357f97aff": { + "path": "591128f9-0994-4e01-a1ab-f23357f97aff", + "cellName": "Details", + "cellId": "591128f9-0994-4e01-a1ab-f23357f97aff", + "visible": true, + "parentCellId": "3f26b33e-477f-41df-a78a-3e3e4f4209ae" + }, + "4940977f-933d-4bbc-b19e-749647b44d25": { + "path": "4940977f-933d-4bbc-b19e-749647b44d25", + "cellName": "notificationUtils.js", + "cellId": "4940977f-933d-4bbc-b19e-749647b44d25", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc" + }, + "a0b22a5a-59e1-47e3-8218-a4ad083b1b71": { + "path": "a0b22a5a-59e1-47e3-8218-a4ad083b1b71", + "cellName": "monitorModuleQueries.js", + "cellId": "a0b22a5a-59e1-47e3-8218-a4ad083b1b71", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380" + }, + "ceded1fc-87a9-43c8-a756-746be3d4ad8d": { + "path": "ceded1fc-87a9-43c8-a756-746be3d4ad8d", + "cellName": "Components", + "cellId": "ceded1fc-87a9-43c8-a756-746be3d4ad8d", + "visible": true, + "parentCellId": "74019e8a-3148-4a32-aae2-5afd16f3eb9b" + }, + "e292c552-b3df-4bac-bd9a-8f44c260a5aa": { + "path": "e292c552-b3df-4bac-bd9a-8f44c260a5aa", + "cellName": "index.jsx", + "cellId": "e292c552-b3df-4bac-bd9a-8f44c260a5aa", + "visible": true, + "parentCellId": "591128f9-0994-4e01-a1ab-f23357f97aff" + }, + "e97d4e61-483e-4ac5-85a1-0ff72fcdd449": { + "path": "e97d4e61-483e-4ac5-85a1-0ff72fcdd449", + "cellName": "Components", + "cellId": "e97d4e61-483e-4ac5-85a1-0ff72fcdd449", + "visible": true, + "parentCellId": "591128f9-0994-4e01-a1ab-f23357f97aff" + }, + "83800be3-2a0e-4419-b379-3f995f4050e7": { + "path": "83800be3-2a0e-4419-b379-3f995f4050e7", + "cellName": "CustomAlertsSection.jsx", + "cellId": "83800be3-2a0e-4419-b379-3f995f4050e7", + "visible": true, + "parentCellId": "ceded1fc-87a9-43c8-a756-746be3d4ad8d" + }, + "6a7c7720-a399-4d4d-8225-36e0b2f59731": { + "path": "6a7c7720-a399-4d4d-8225-36e0b2f59731", + "cellName": "GaugeBoxes", + "cellId": "6a7c7720-a399-4d4d-8225-36e0b2f59731", + "visible": true, + "parentCellId": "e97d4e61-483e-4ac5-85a1-0ff72fcdd449" + }, + "8fe3d890-3605-407d-b06c-1d890a354e46": { + "path": "8fe3d890-3605-407d-b06c-1d890a354e46", + "cellName": "index.jsx", + "cellId": "8fe3d890-3605-407d-b06c-1d890a354e46", + "visible": true, + "parentCellId": "6a7c7720-a399-4d4d-8225-36e0b2f59731" + }, + "9651d084-5c16-4417-89c1-f52ae6572a95": { + "path": "9651d084-5c16-4417-89c1-f52ae6572a95", + "cellName": "Configuration: User Sets Alert Thresholds - CustomAlertsSection.jsx:L45-53", + "cellId": "9651d084-5c16-4417-89c1-f52ae6572a95", + "visible": true, + "parentCellId": "83800be3-2a0e-4419-b379-3f995f4050e7" + }, + "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx-simstep-80dd889a-1fc9-4d56-b9cf-5b45bf1110b1": { + "path": "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx-simstep-80dd889a-1fc9-4d56-b9cf-5b45bf1110b1", + "fileName": "CustomAlertsSection.jsx", + "wiki": "A user navigates to the 'Create Infrastructure Monitor' page to set up monitoring for a new server. They enable alerts for CPU, memory, and disk usage, and define specific thresholds for each metric. For example, an alert is configured to trigger if CPU usage exceeds 80%.", + "cellName": "Configuration: User Sets Alert Thresholds - CustomAlertsSection.jsx:L45-53", + "cellId": "9651d084-5c16-4417-89c1-f52ae6572a95", + "visible": true, + "startLine": 45, + "endLine": 53, + "parentCellId": "83800be3-2a0e-4419-b379-3f995f4050e7", + "parentPath": "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "80dd889a-1fc9-4d56-b9cf-5b45bf1110b1" + } + ] + }, + "4c47b5f2-5363-4816-9745-30e61d184c7a": { + "path": "4c47b5f2-5363-4816-9745-30e61d184c7a", + "cellName": "Automated Check: Backend Fetches Hardware Data - networkService.js:L7-14", + "cellId": "4c47b5f2-5363-4816-9745-30e61d184c7a", + "visible": true, + "parentCellId": "00406786-9663-440a-96b6-919d544732c0" + }, + "server/src/service/v1/infrastructure/networkService.js-simstep-ed6e7faf-69dd-4d0b-bda8-137739ed288b": { + "path": "server/src/service/v1/infrastructure/networkService.js-simstep-ed6e7faf-69dd-4d0b-bda8-137739ed288b", + "fileName": "networkService.js", + "wiki": "The Checkmate server's job queue periodically initiates a check for the newly created hardware monitor. It makes an HTTP request to the monitor's URL, which points to the external 'Capture' agent running on the target server.", + "cellName": "Automated Check: Backend Fetches Hardware Data - networkService.js:L7-14", + "cellId": "4c47b5f2-5363-4816-9745-30e61d184c7a", + "visible": true, + "startLine": 7, + "endLine": 14, + "parentCellId": "00406786-9663-440a-96b6-919d544732c0", + "parentPath": "server/src/service/v1/infrastructure/networkService.js", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "ed6e7faf-69dd-4d0b-bda8-137739ed288b" + } + ] + }, + "c1bf10c9-8c1a-44f4-8e23-39708f0301e8": { + "path": "c1bf10c9-8c1a-44f4-8e23-39708f0301e8", + "cellName": "Automated Check: Process and Store Hardware Data - statusService.js:L265-274", + "cellId": "c1bf10c9-8c1a-44f4-8e23-39708f0301e8", + "visible": true, + "parentCellId": "fbd7d244-2f37-453c-8c83-a23a260b26cc" + }, + "server/src/service/v1/infrastructure/statusService.js-simstep-e5b90cf2-9369-4741-9803-c835a81c009b": { + "path": "server/src/service/v1/infrastructure/statusService.js-simstep-e5b90cf2-9369-4741-9803-c835a81c009b", + "fileName": "statusService.js", + "wiki": "The backend's StatusService receives the payload from the Capture agent. It parses the data and constructs a 'check' document. This document, containing the detailed hardware metrics, is then saved to the database for historical tracking and visualization.", + "cellName": "Automated Check: Process and Store Hardware Data - statusService.js:L265-274", + "cellId": "c1bf10c9-8c1a-44f4-8e23-39708f0301e8", + "visible": true, + "startLine": 265, + "endLine": 274, + "parentCellId": "fbd7d244-2f37-453c-8c83-a23a260b26cc", + "parentPath": "server/src/service/v1/infrastructure/statusService.js", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "e5b90cf2-9369-4741-9803-c835a81c009b" + } + ] + }, + "d4f48f1a-61de-4cde-87d2-4b5215464899": { + "path": "d4f48f1a-61de-4cde-87d2-4b5215464899", + "cellName": "Automated Check: Evaluate Data Against Thresholds - notificationUtils.js:L95-109", + "cellId": "d4f48f1a-61de-4cde-87d2-4b5215464899", + "visible": true, + "parentCellId": "4940977f-933d-4bbc-b19e-749647b44d25" + }, + "server/src/service/v1/infrastructure/notificationUtils.js-simstep-07fe8489-1d94-4afb-83f8-7cebb0dff49a": { + "path": "server/src/service/v1/infrastructure/notificationUtils.js-simstep-07fe8489-1d94-4afb-83f8-7cebb0dff49a", + "fileName": "notificationUtils.js", + "wiki": "The notification utility function compares the received hardware metrics against the user-defined thresholds stored in the monitor's configuration. In this case, the CPU usage of 85% is greater than the 80% threshold, so an alert condition is met.", + "cellName": "Automated Check: Evaluate Data Against Thresholds - notificationUtils.js:L95-109", + "cellId": "d4f48f1a-61de-4cde-87d2-4b5215464899", + "visible": true, + "startLine": 95, + "endLine": 109, + "parentCellId": "4940977f-933d-4bbc-b19e-749647b44d25", + "parentPath": "server/src/service/v1/infrastructure/notificationUtils.js", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "07fe8489-1d94-4afb-83f8-7cebb0dff49a" + } + ] + }, + "801fe70a-1a19-406c-8f0d-770b0cb74409": { + "path": "801fe70a-1a19-406c-8f0d-770b0cb74409", + "cellName": "Visualization: User Views Monitor Details - index.jsx:L23-31", + "cellId": "801fe70a-1a19-406c-8f0d-770b0cb74409", + "visible": true, + "parentCellId": "e292c552-b3df-4bac-bd9a-8f44c260a5aa" + }, + "client/src/Pages/v1/Infrastructure/Details/index.jsx-simstep-c27365db-9db6-4cc9-96f0-4f046fd38562": { + "path": "client/src/Pages/v1/Infrastructure/Details/index.jsx-simstep-c27365db-9db6-4cc9-96f0-4f046fd38562", + "fileName": "index.jsx", + "wiki": "A user navigates to the details page for their 'Production Web Server' monitor in the Checkmate UI to view its performance. The React component for the details page mounts and prepares to fetch data.", + "cellName": "Visualization: User Views Monitor Details - index.jsx:L23-31", + "cellId": "801fe70a-1a19-406c-8f0d-770b0cb74409", + "visible": true, + "startLine": 23, + "endLine": 31, + "parentCellId": "e292c552-b3df-4bac-bd9a-8f44c260a5aa", + "parentPath": "client/src/Pages/v1/Infrastructure/Details/index.jsx", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "c27365db-9db6-4cc9-96f0-4f046fd38562" + } + ] + }, + "9ef7c423-f4fc-4caf-9bf5-c1aa3002ca03": { + "path": "9ef7c423-f4fc-4caf-9bf5-c1aa3002ca03", + "cellName": "Backend: Aggregate Historical Data - monitorModule.js:L312-322", + "cellId": "9ef7c423-f4fc-4caf-9bf5-c1aa3002ca03", + "visible": true, + "parentCellId": "630b3ece-2648-48d5-99b5-ef9611de2ad5" + }, + "server/src/db/v1/modules/monitorModule.js-simstep-8502ae1f-f029-446e-9dde-59c2aba87a8a": { + "path": "server/src/db/v1/modules/monitorModule.js-simstep-8502ae1f-f029-446e-9dde-59c2aba87a8a", + "fileName": "monitorModule.js", + "wiki": "The backend receives the request and executes a database aggregation pipeline. This query fetches all 'hardware' checks for the monitor within the given date range and calculates metrics like average CPU usage, average memory usage, and average temperatures, grouped by time intervals.", + "cellName": "Backend: Aggregate Historical Data - monitorModule.js:L312-322", + "cellId": "9ef7c423-f4fc-4caf-9bf5-c1aa3002ca03", + "visible": true, + "startLine": 312, + "endLine": 322, + "parentCellId": "630b3ece-2648-48d5-99b5-ef9611de2ad5", + "parentPath": "server/src/db/v1/modules/monitorModule.js", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "8502ae1f-f029-446e-9dde-59c2aba87a8a" + } + ] + }, + "9ea2e32d-1ea8-42f2-9a17-613844723ae6": { + "path": "9ea2e32d-1ea8-42f2-9a17-613844723ae6", + "cellName": "Visualization: Frontend Renders Charts and Gauges - index.jsx:L22-51", + "cellId": "9ea2e32d-1ea8-42f2-9a17-613844723ae6", + "visible": true, + "parentCellId": "8fe3d890-3605-407d-b06c-1d890a354e46" + }, + "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx-simstep-6eedce82-b9c8-4022-95e5-3a6e00628105": { + "path": "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx-simstep-6eedce82-b9c8-4022-95e5-3a6e00628105", + "fileName": "index.jsx", + "wiki": "The frontend components on the Infrastructure Details page receive the aggregated data. The 'GaugeBoxes' component uses the 'latestCheck' data to render real-time gauges for current CPU and memory usage, while the 'AreaChartBoxes' component uses the historical 'checks' data to render time-series charts.", + "cellName": "Visualization: Frontend Renders Charts and Gauges - index.jsx:L22-51", + "cellId": "9ea2e32d-1ea8-42f2-9a17-613844723ae6", + "visible": true, + "startLine": 22, + "endLine": 51, + "parentCellId": "8fe3d890-3605-407d-b06c-1d890a354e46", + "parentPath": "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "6eedce82-b9c8-4022-95e5-3a6e00628105" + } + ] + }, + "1c5c9857-bf32-40e4-921b-957c252dac2a": { + "path": "1c5c9857-bf32-40e4-921b-957c252dac2a", + "cellName": "API Call:\nSubmit Monitor\nConfiguration", + "cellId": "1c5c9857-bf32-40e4-921b-957c252dac2a", + "visible": true + }, + "generated-edge-simstep-815f96af-c28e-4f80-b4ab-18a6fb23eee0-1c5c9857-bf32-40e4-921b-957c252dac2a": { + "path": "generated-edge-simstep-815f96af-c28e-4f80-b4ab-18a6fb23eee0-1c5c9857-bf32-40e4-921b-957c252dac2a", + "fileName": "CustomAlertsSection.jsx", + "cellName": "API Call: Submit Monitor Configuration", + "cellId": "1c5c9857-bf32-40e4-921b-957c252dac2a", + "visible": true, + "startLine": 386, + "endLine": 389, + "parentPath": "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "815f96af-c28e-4f80-b4ab-18a6fb23eee0" + } + ] + }, + "a64547c9-d9e6-4941-8e5e-f49add06516b": { + "path": "a64547c9-d9e6-4941-8e5e-f49add06516b", + "cellName": "Automated Check:\nData Received\nfrom Capture\nAgent", + "cellId": "a64547c9-d9e6-4941-8e5e-f49add06516b", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc" + }, + "generated-edge-simstep-4434dca1-2048-4458-a70b-b70d569e378d-a64547c9-d9e6-4941-8e5e-f49add06516b": { + "path": "generated-edge-simstep-4434dca1-2048-4458-a70b-b70d569e378d-a64547c9-d9e6-4941-8e5e-f49add06516b", + "fileName": "networkService.js", + "cellName": "Automated Check: Data Received from Capture Agent", + "cellId": "a64547c9-d9e6-4941-8e5e-f49add06516b", + "visible": true, + "startLine": 266, + "endLine": 273, + "parentPath": "server/src/service/v1/infrastructure/networkService.js", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "4434dca1-2048-4458-a70b-b70d569e378d" + } + ] + }, + "7a250d09-50c6-4935-b10c-626b0ebe1fba": { + "path": "7a250d09-50c6-4935-b10c-626b0ebe1fba", + "cellName": "Automated Check:\nPass Data\nfor Alert\nEvaluation", + "cellId": "7a250d09-50c6-4935-b10c-626b0ebe1fba", + "visible": true, + "parentCellId": "1670815f-860d-4ea4-92e1-b238a799d3dc" + }, + "generated-edge-simstep-b019a6d0-a23a-4a1d-bd21-644f01996f14-7a250d09-50c6-4935-b10c-626b0ebe1fba": { + "path": "generated-edge-simstep-b019a6d0-a23a-4a1d-bd21-644f01996f14-7a250d09-50c6-4935-b10c-626b0ebe1fba", + "fileName": "statusService.js", + "cellName": "Automated Check: Pass Data for Alert Evaluation", + "cellId": "7a250d09-50c6-4935-b10c-626b0ebe1fba", + "visible": true, + "startLine": 60, + "endLine": 79, + "parentPath": "server/src/service/v1/infrastructure/statusService.js", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "b019a6d0-a23a-4a1d-bd21-644f01996f14" + } + ] + }, + "3d357de8-9fd2-4d0b-9623-f36fce4cf1de": { + "path": "3d357de8-9fd2-4d0b-9623-f36fce4cf1de", + "cellName": "Automated Check:\nGenerate Alert\nNotification Content", + "cellId": "3d357de8-9fd2-4d0b-9623-f36fce4cf1de", + "visible": true + }, + "generated-edge-simstep-c691b3b9-88b3-4c88-a399-ff9fd5866194-3d357de8-9fd2-4d0b-9623-f36fce4cf1de": { + "path": "generated-edge-simstep-c691b3b9-88b3-4c88-a399-ff9fd5866194-3d357de8-9fd2-4d0b-9623-f36fce4cf1de", + "fileName": "notificationUtils.js", + "cellName": "Automated Check: Generate Alert Notification Content", + "cellId": "3d357de8-9fd2-4d0b-9623-f36fce4cf1de", + "visible": true, + "startLine": 177, + "endLine": 183, + "parentPath": "server/src/service/v1/infrastructure/notificationUtils.js", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "c691b3b9-88b3-4c88-a399-ff9fd5866194" + } + ] + }, + "00921831-b1d9-4a5a-ae6f-5ddee7cfc1e4": { + "path": "00921831-b1d9-4a5a-ae6f-5ddee7cfc1e4", + "cellName": "API Call:\nRequest for\nHistorical Hardware\nData", + "cellId": "00921831-b1d9-4a5a-ae6f-5ddee7cfc1e4", + "visible": true + }, + "generated-edge-simstep-a5d20172-1c95-4c9e-af39-f86a498734bb-00921831-b1d9-4a5a-ae6f-5ddee7cfc1e4": { + "path": "generated-edge-simstep-a5d20172-1c95-4c9e-af39-f86a498734bb-00921831-b1d9-4a5a-ae6f-5ddee7cfc1e4", + "fileName": "index.jsx", + "cellName": "API Call: Request for Historical Hardware Data", + "cellId": "00921831-b1d9-4a5a-ae6f-5ddee7cfc1e4", + "visible": true, + "startLine": 198, + "endLine": 206, + "parentPath": "client/src/Pages/v1/Infrastructure/Details/index.jsx", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "a5d20172-1c95-4c9e-af39-f86a498734bb" + } + ] + }, + "60308696-6f8a-43c8-b9a1-da199ef500cd": { + "path": "60308696-6f8a-43c8-b9a1-da199ef500cd", + "cellName": "API Response:\nSend Aggregated\nData to\nFrontend", + "cellId": "60308696-6f8a-43c8-b9a1-da199ef500cd", + "visible": true + }, + "generated-edge-simstep-627fcb0c-0282-4b4d-8c55-aceb636e4da0-60308696-6f8a-43c8-b9a1-da199ef500cd": { + "path": "generated-edge-simstep-627fcb0c-0282-4b4d-8c55-aceb636e4da0-60308696-6f8a-43c8-b9a1-da199ef500cd", + "fileName": "monitorModule.js", + "cellName": "API Response: Send Aggregated Data to Frontend", + "cellId": "60308696-6f8a-43c8-b9a1-da199ef500cd", + "visible": true, + "startLine": 247, + "endLine": 262, + "parentPath": "server/src/db/v1/modules/monitorModule.js", + "simSteps": [ + { + "simulationKey": "Infrastructure and Hardware Health Monitoring", + "simStepId": "627fcb0c-0282-4b4d-8c55-aceb636e4da0" + } + ] + }, + "feefae96-61b8-4ac5-a37c-592ed4b2330e": { + "path": "feefae96-61b8-4ac5-a37c-592ed4b2330e", + "cellName": "v2", + "cellId": "feefae96-61b8-4ac5-a37c-592ed4b2330e", + "visible": true, + "parentCellId": "99e9b746-7809-41f4-a433-1de7cc90574b" + }, + "b3575a92-df07-4cf7-810f-8e5e0672d666": { + "path": "b3575a92-df07-4cf7-810f-8e5e0672d666", + "cellName": "Notifications", + "cellId": "b3575a92-df07-4cf7-810f-8e5e0672d666", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679" + }, + "cd2428fd-6545-48dd-b685-5261311b7532": { + "path": "cd2428fd-6545-48dd-b685-5261311b7532", + "cellName": "useNotifications.js", + "cellId": "cd2428fd-6545-48dd-b685-5261311b7532", + "visible": true, + "parentCellId": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961" + }, + "96250197-23af-4059-9904-fd6751f70ad5": { + "path": "96250197-23af-4059-9904-fd6751f70ad5", + "cellName": "notificationRoute.js", + "cellId": "96250197-23af-4059-9904-fd6751f70ad5", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd" + }, + "8e5941e1-e577-455f-bbf2-83a418ad5908": { + "path": "8e5941e1-e577-455f-bbf2-83a418ad5908", + "cellName": "notificationController.js", + "cellId": "8e5941e1-e577-455f-bbf2-83a418ad5908", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f": { + "path": "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f", + "cellName": "infrastructure", + "cellId": "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f", + "visible": true, + "parentCellId": "feefae96-61b8-4ac5-a37c-592ed4b2330e" + }, + "a6fb23f0-73bc-40ee-bb3c-ed93605a2bcb": { + "path": "a6fb23f0-73bc-40ee-bb3c-ed93605a2bcb", + "cellName": "create", + "cellId": "a6fb23f0-73bc-40ee-bb3c-ed93605a2bcb", + "visible": true, + "parentCellId": "b3575a92-df07-4cf7-810f-8e5e0672d666" + }, + "62daeac4-9402-448a-972c-cb8cb760671c": { + "path": "62daeac4-9402-448a-972c-cb8cb760671c", + "cellName": "notificationModule.js", + "cellId": "62daeac4-9402-448a-972c-cb8cb760671c", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380" + }, + "159e569d-31d8-4883-b443-4ec08b21a044": { + "path": "159e569d-31d8-4883-b443-4ec08b21a044", + "cellName": "JobGenerator.ts", + "cellId": "159e569d-31d8-4883-b443-4ec08b21a044", + "visible": true, + "parentCellId": "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f" + }, + "c6d9907f-0a05-4153-a27f-47c3a1c4c0aa": { + "path": "c6d9907f-0a05-4153-a27f-47c3a1c4c0aa", + "cellName": "NotificationService.ts", + "cellId": "c6d9907f-0a05-4153-a27f-47c3a1c4c0aa", + "visible": true, + "parentCellId": "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f" + }, + "b71b02fb-d08e-4fcf-b959-de1b9547d9c2": { + "path": "b71b02fb-d08e-4fcf-b959-de1b9547d9c2", + "cellName": "index.jsx", + "cellId": "b71b02fb-d08e-4fcf-b959-de1b9547d9c2", + "visible": true, + "parentCellId": "a6fb23f0-73bc-40ee-bb3c-ed93605a2bcb" + }, + "15486629-84b3-463a-962b-06083f69e4bf": { + "path": "15486629-84b3-463a-962b-06083f69e4bf", + "cellName": "Flow 1: Render Notification Creation Form - index.jsx:L157-321", + "cellId": "15486629-84b3-463a-962b-06083f69e4bf", + "visible": true, + "parentCellId": "b71b02fb-d08e-4fcf-b959-de1b9547d9c2" + }, + "client/src/Pages/v1/Notifications/create/index.jsx-simstep-e924ec15-1bea-4e3b-befb-a8bf30cbf740": { + "path": "client/src/Pages/v1/Notifications/create/index.jsx-simstep-e924ec15-1bea-4e3b-befb-a8bf30cbf740", + "fileName": "index.jsx", + "wiki": "User accesses the `/notifications/create` route, which renders the `CreateNotifications` component. This component displays a form for setting up a new notification channel, including fields for name, type, and provider-specific details.", + "cellName": "Flow 1: Render Notification Creation Form - index.jsx:L157-321", + "cellId": "15486629-84b3-463a-962b-06083f69e4bf", + "visible": true, + "startLine": 157, + "endLine": 321, + "parentCellId": "b71b02fb-d08e-4fcf-b959-de1b9547d9c2", + "parentPath": "client/src/Pages/v1/Notifications/create/index.jsx", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "e924ec15-1bea-4e3b-befb-a8bf30cbf740" + } + ] + }, + "a07b805e-e55f-46cb-b54b-765b7813b0ca": { + "path": "a07b805e-e55f-46cb-b54b-765b7813b0ca", + "cellName": "Flow 1: Frontend API Call - useNotifications.js:L14-17", + "cellId": "a07b805e-e55f-46cb-b54b-765b7813b0ca", + "visible": true, + "parentCellId": "cd2428fd-6545-48dd-b685-5261311b7532" + }, + "client/src/Hooks/v1/useNotifications.js-simstep-383c6be8-aa51-4398-9ae3-21035413f73a": { + "path": "client/src/Hooks/v1/useNotifications.js-simstep-383c6be8-aa51-4398-9ae3-21035413f73a", + "fileName": "useNotifications.js", + "wiki": "The `createNotification` hook executes an asynchronous function that calls `networkService.createNotification` to send the new notification data to the backend API.", + "cellName": "Flow 1: Frontend API Call - useNotifications.js:L14-17", + "cellId": "a07b805e-e55f-46cb-b54b-765b7813b0ca", + "visible": true, + "startLine": 14, + "endLine": 17, + "parentCellId": "cd2428fd-6545-48dd-b685-5261311b7532", + "parentPath": "client/src/Hooks/v1/useNotifications.js", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "383c6be8-aa51-4398-9ae3-21035413f73a" + } + ] + }, + "d0a4737f-715f-4f50-b6d5-5e4b31f86f93": { + "path": "d0a4737f-715f-4f50-b6d5-5e4b31f86f93", + "cellName": "Flow 1: Backend Route Handling - notificationRoute.js:L10", + "cellId": "d0a4737f-715f-4f50-b6d5-5e4b31f86f93", + "visible": true, + "parentCellId": "96250197-23af-4059-9904-fd6751f70ad5" + }, + "server/src/routes/v1/notificationRoute.js-simstep-2f9266c9-4cbc-4b50-a280-cf8b5800d540": { + "path": "server/src/routes/v1/notificationRoute.js-simstep-2f9266c9-4cbc-4b50-a280-cf8b5800d540", + "fileName": "notificationRoute.js", + "wiki": "The backend server receives the POST request. The Express router matches the `/notifications` path and forwards the request to the `createNotification` method of the `NotificationController`.", + "cellName": "Flow 1: Backend Route Handling - notificationRoute.js:L10", + "cellId": "d0a4737f-715f-4f50-b6d5-5e4b31f86f93", + "visible": true, + "startLine": 10, + "endLine": 10, + "parentCellId": "96250197-23af-4059-9904-fd6751f70ad5", + "parentPath": "server/src/routes/v1/notificationRoute.js", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "2f9266c9-4cbc-4b50-a280-cf8b5800d540" + } + ] + }, + "4a796ccf-40a9-4446-a109-6a6eed52e898": { + "path": "4a796ccf-40a9-4446-a109-6a6eed52e898", + "cellName": "Flow 1: Database Insertion - notificationModule.js:L10-13", + "cellId": "4a796ccf-40a9-4446-a109-6a6eed52e898", + "visible": true, + "parentCellId": "62daeac4-9402-448a-972c-cb8cb760671c" + }, + "server/src/db/v1/modules/notificationModule.js-simstep-d29ab4ab-be50-4f17-af6e-2c61cec9d0ab": { + "path": "server/src/db/v1/modules/notificationModule.js-simstep-d29ab4ab-be50-4f17-af6e-2c61cec9d0ab", + "fileName": "notificationModule.js", + "wiki": "The `notificationModule` creates a new instance of the `Notification` model with the provided data and saves it to the MongoDB database.", + "cellName": "Flow 1: Database Insertion - notificationModule.js:L10-13", + "cellId": "4a796ccf-40a9-4446-a109-6a6eed52e898", + "visible": true, + "startLine": 10, + "endLine": 13, + "parentCellId": "62daeac4-9402-448a-972c-cb8cb760671c", + "parentPath": "server/src/db/v1/modules/notificationModule.js", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "d29ab4ab-be50-4f17-af6e-2c61cec9d0ab" + } + ] + }, + "29c2c56e-9dc0-4304-bfbf-e0c0f8915a39": { + "path": "29c2c56e-9dc0-4304-bfbf-e0c0f8915a39", + "cellName": "Flow 1: Backend Receives Test Request - notificationRoute.js:L13", + "cellId": "29c2c56e-9dc0-4304-bfbf-e0c0f8915a39", + "visible": true, + "parentCellId": "96250197-23af-4059-9904-fd6751f70ad5" + }, + "server/src/routes/v1/notificationRoute.js-simstep-c1b17b64-90e9-48bc-8927-25d7ebfc3036": { + "path": "server/src/routes/v1/notificationRoute.js-simstep-c1b17b64-90e9-48bc-8927-25d7ebfc3036", + "fileName": "notificationRoute.js", + "wiki": "The client sends a POST request to `/notifications/test`. The router directs this request to the `testNotification` method in the `NotificationController`.", + "cellName": "Flow 1: Backend Receives Test Request - notificationRoute.js:L13", + "cellId": "29c2c56e-9dc0-4304-bfbf-e0c0f8915a39", + "visible": true, + "startLine": 13, + "endLine": 13, + "parentCellId": "96250197-23af-4059-9904-fd6751f70ad5", + "parentPath": "server/src/routes/v1/notificationRoute.js", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "c1b17b64-90e9-48bc-8927-25d7ebfc3036" + } + ] + }, + "1cc01e08-4a6c-4526-b8a7-5ee8a06c1709": { + "path": "1cc01e08-4a6c-4526-b8a7-5ee8a06c1709", + "cellName": "Flow 1: Send Test Notification - notificationService.js:L120-123", + "cellId": "1cc01e08-4a6c-4526-b8a7-5ee8a06c1709", + "visible": true, + "parentCellId": "2ff1f030-2080-48da-bbbe-90302fdfc2f7" + }, + "server/src/service/v1/infrastructure/notificationService.js-simstep-f29f9be1-a0cd-43a3-b259-e5105a25ce41": { + "path": "server/src/service/v1/infrastructure/notificationService.js-simstep-f29f9be1-a0cd-43a3-b259-e5105a25ce41", + "fileName": "notificationService.js", + "wiki": "The `NotificationService` prepares a standard test message and calls its internal `sendNotification` method. This method identifies the notification type (e.g., email, Slack) and uses the corresponding service (e.g., EmailService, NetworkService for webhooks) to dispatch the test message.", + "cellName": "Flow 1: Send Test Notification - notificationService.js:L120-123", + "cellId": "1cc01e08-4a6c-4526-b8a7-5ee8a06c1709", + "visible": true, + "startLine": 120, + "endLine": 123, + "parentCellId": "2ff1f030-2080-48da-bbbe-90302fdfc2f7", + "parentPath": "server/src/service/v1/infrastructure/notificationService.js", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "f29f9be1-a0cd-43a3-b259-e5105a25ce41" + } + ] + }, + "28f1eabb-4f24-4944-8a0a-04f41ceb8589": { + "path": "28f1eabb-4f24-4944-8a0a-04f41ceb8589", + "cellName": "Flow 2: Job Execution and Status Update - JobGenerator.ts:L43-66", + "cellId": "28f1eabb-4f24-4944-8a0a-04f41ceb8589", + "visible": true, + "parentCellId": "159e569d-31d8-4883-b443-4ec08b21a044" + }, + "server/src/service/v2/infrastructure/JobGenerator.ts-simstep-157cec87-9b76-4a1c-9d85-0a6d3decbaa3": { + "path": "server/src/service/v2/infrastructure/JobGenerator.ts-simstep-157cec87-9b76-4a1c-9d85-0a6d3decbaa3", + "fileName": "JobGenerator.ts", + "wiki": "A background job is generated for a monitor. When executed, it performs a network check, saves the result, and updates the monitor's status via `statusService.updateMonitorStatus`.", + "cellName": "Flow 2: Job Execution and Status Update - JobGenerator.ts:L43-66", + "cellId": "28f1eabb-4f24-4944-8a0a-04f41ceb8589", + "visible": true, + "startLine": 43, + "endLine": 66, + "parentCellId": "159e569d-31d8-4883-b443-4ec08b21a044", + "parentPath": "server/src/service/v2/infrastructure/JobGenerator.ts", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "157cec87-9b76-4a1c-9d85-0a6d3decbaa3" + } + ] + }, + "31fe5a28-0640-4b52-841b-dd02e2ae646e": { + "path": "31fe5a28-0640-4b52-841b-dd02e2ae646e", + "cellName": "Flow 2: Initiate Notification Handling - JobGenerator.ts:L61", + "cellId": "31fe5a28-0640-4b52-841b-dd02e2ae646e", + "visible": true, + "parentCellId": "159e569d-31d8-4883-b443-4ec08b21a044" + }, + "server/src/service/v2/infrastructure/JobGenerator.ts-simstep-55ad645b-118a-478e-a00c-09ef82357008": { + "path": "server/src/service/v2/infrastructure/JobGenerator.ts-simstep-55ad645b-118a-478e-a00c-09ef82357008", + "fileName": "JobGenerator.ts", + "wiki": "If the monitor's status has changed, the `handleNotifications` method of the `NotificationService` is invoked with the updated monitor's data.", + "cellName": "Flow 2: Initiate Notification Handling - JobGenerator.ts:L61", + "cellId": "31fe5a28-0640-4b52-841b-dd02e2ae646e", + "visible": true, + "startLine": 61, + "endLine": 61, + "parentCellId": "159e569d-31d8-4883-b443-4ec08b21a044", + "parentPath": "server/src/service/v2/infrastructure/JobGenerator.ts", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "55ad645b-118a-478e-a00c-09ef82357008" + } + ] + }, + "78e5cb9f-6a97-4d31-a487-fe7dbce5d7d1": { + "path": "78e5cb9f-6a97-4d31-a487-fe7dbce5d7d1", + "cellName": "Flow 2: Dispatch Alerts - NotificationService.ts:L28-57", + "cellId": "78e5cb9f-6a97-4d31-a487-fe7dbce5d7d1", + "visible": true, + "parentCellId": "c6d9907f-0a05-4153-a27f-47c3a1c4c0aa" + }, + "server/src/service/v2/infrastructure/NotificationService.ts-simstep-8d1176f3-c755-401d-9ca8-234689b4b837": { + "path": "server/src/service/v2/infrastructure/NotificationService.ts-simstep-8d1176f3-c755-401d-9ca8-234689b4b837", + "fileName": "NotificationService.ts", + "wiki": "The `handleNotifications` method retrieves the full details for each associated notification channel. It then iterates through them, using specific provider services (e.g., `SlackService`, `EmailService`) to format and send a tailored alert message for each channel, notifying users that the monitor is down.", + "cellName": "Flow 2: Dispatch Alerts - NotificationService.ts:L28-57", + "cellId": "78e5cb9f-6a97-4d31-a487-fe7dbce5d7d1", + "visible": true, + "startLine": 28, + "endLine": 57, + "parentCellId": "c6d9907f-0a05-4153-a27f-47c3a1c4c0aa", + "parentPath": "server/src/service/v2/infrastructure/NotificationService.ts", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "8d1176f3-c755-401d-9ca8-234689b4b837" + } + ] + }, + "54497b9a-4910-4df4-b016-480b9ac250a7": { + "path": "54497b9a-4910-4df4-b016-480b9ac250a7", + "cellName": "Flow 1:\nSubmit Form\nData for\nCreation", + "cellId": "54497b9a-4910-4df4-b016-480b9ac250a7", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "generated-edge-simstep-71dbcd41-c6d4-4514-b5a4-6c966ca7ba6f-54497b9a-4910-4df4-b016-480b9ac250a7": { + "path": "generated-edge-simstep-71dbcd41-c6d4-4514-b5a4-6c966ca7ba6f-54497b9a-4910-4df4-b016-480b9ac250a7", + "fileName": "index.jsx", + "cellName": "Flow 1: Submit Form Data for Creation", + "cellId": "54497b9a-4910-4df4-b016-480b9ac250a7", + "visible": true, + "startLine": 96, + "endLine": 101, + "parentPath": "client/src/Pages/v1/Notifications/create/index.jsx", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "71dbcd41-c6d4-4514-b5a4-6c966ca7ba6f" + } + ] + }, + "dc135d8d-0515-403d-ae58-f3c04647b2ae": { + "path": "dc135d8d-0515-403d-ae58-f3c04647b2ae", + "cellName": "Flow 1:\nHTTP POST\nRequest", + "cellId": "dc135d8d-0515-403d-ae58-f3c04647b2ae", + "visible": true + }, + "generated-edge-simstep-162d8d2f-6cd1-45fa-8711-1ee85c5f86ef-dc135d8d-0515-403d-ae58-f3c04647b2ae": { + "path": "generated-edge-simstep-162d8d2f-6cd1-45fa-8711-1ee85c5f86ef-dc135d8d-0515-403d-ae58-f3c04647b2ae", + "fileName": "useNotifications.js", + "cellName": "Flow 1: HTTP POST Request", + "cellId": "dc135d8d-0515-403d-ae58-f3c04647b2ae", + "visible": true, + "startLine": 1064, + "endLine": 1066, + "parentPath": "client/src/Hooks/v1/useNotifications.js", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "162d8d2f-6cd1-45fa-8711-1ee85c5f86ef" + } + ] + }, + "7b9ebb0e-a6b2-438c-bfed-9c851a1bccbc": { + "path": "7b9ebb0e-a6b2-438c-bfed-9c851a1bccbc", + "cellName": "Flow 1:\nController to\nDatabase Module", + "cellId": "7b9ebb0e-a6b2-438c-bfed-9c851a1bccbc", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-73e63d2f-2b43-488d-9e86-170f511673f5-7b9ebb0e-a6b2-438c-bfed-9c851a1bccbc": { + "path": "generated-edge-simstep-73e63d2f-2b43-488d-9e86-170f511673f5-7b9ebb0e-a6b2-438c-bfed-9c851a1bccbc", + "fileName": "notificationRoute.js", + "cellName": "Flow 1: Controller to Database Module", + "cellId": "7b9ebb0e-a6b2-438c-bfed-9c851a1bccbc", + "visible": true, + "startLine": 55, + "endLine": 55, + "parentPath": "server/src/routes/v1/notificationRoute.js", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "73e63d2f-2b43-488d-9e86-170f511673f5" + } + ] + }, + "279155cd-ff31-41a2-8f5f-a538f01bfa66": { + "path": "279155cd-ff31-41a2-8f5f-a538f01bfa66", + "cellName": "Flow 1:\nUser Clicks\nTest Notification\nButton", + "cellId": "279155cd-ff31-41a2-8f5f-a538f01bfa66", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-d390ef45-f2fe-4806-a45a-cc9c84b48a5a-279155cd-ff31-41a2-8f5f-a538f01bfa66": { + "path": "generated-edge-simstep-d390ef45-f2fe-4806-a45a-cc9c84b48a5a-279155cd-ff31-41a2-8f5f-a538f01bfa66", + "fileName": "notificationModule.js", + "cellName": "Flow 1: User Clicks Test Notification Button", + "cellId": "279155cd-ff31-41a2-8f5f-a538f01bfa66", + "visible": true, + "startLine": 281, + "endLine": 285, + "parentPath": "server/src/db/v1/modules/notificationModule.js", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "d390ef45-f2fe-4806-a45a-cc9c84b48a5a" + } + ] + }, + "57018b30-e2f4-4eb2-a1da-fe75afbb8c2f": { + "path": "57018b30-e2f4-4eb2-a1da-fe75afbb8c2f", + "cellName": "Flow 1:\nController to\nNotification Service", + "cellId": "57018b30-e2f4-4eb2-a1da-fe75afbb8c2f", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-6f2bf027-50d9-472e-9569-808a64fce7cf-57018b30-e2f4-4eb2-a1da-fe75afbb8c2f": { + "path": "generated-edge-simstep-6f2bf027-50d9-472e-9569-808a64fce7cf-57018b30-e2f4-4eb2-a1da-fe75afbb8c2f", + "fileName": "notificationRoute.js", + "cellName": "Flow 1: Controller to Notification Service", + "cellId": "57018b30-e2f4-4eb2-a1da-fe75afbb8c2f", + "visible": true, + "startLine": 22, + "endLine": 22, + "parentPath": "server/src/routes/v1/notificationRoute.js", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "6f2bf027-50d9-472e-9569-808a64fce7cf" + } + ] + }, + "50797935-1447-47a4-891b-9e886e5601fa": { + "path": "50797935-1447-47a4-891b-9e886e5601fa", + "cellName": "Flow 2:\nStatus Change\nDetected", + "cellId": "50797935-1447-47a4-891b-9e886e5601fa", + "visible": true, + "parentCellId": "159e569d-31d8-4883-b443-4ec08b21a044" + }, + "generated-edge-simstep-b7ec7415-fcae-4077-8c19-302a015b00b5-50797935-1447-47a4-891b-9e886e5601fa": { + "path": "generated-edge-simstep-b7ec7415-fcae-4077-8c19-302a015b00b5-50797935-1447-47a4-891b-9e886e5601fa", + "fileName": "JobGenerator.ts", + "cellName": "Flow 2: Status Change Detected", + "cellId": "50797935-1447-47a4-891b-9e886e5601fa", + "visible": true, + "startLine": 60, + "endLine": 60, + "parentPath": "server/src/service/v2/infrastructure/JobGenerator.ts", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "b7ec7415-fcae-4077-8c19-302a015b00b5" + } + ] + }, + "c778c6b9-c12e-45c0-8682-2c9e892867b0": { + "path": "c778c6b9-c12e-45c0-8682-2c9e892867b0", + "cellName": "Flow 2:\nMonitor Data\nPassed to\nNotification Service", + "cellId": "c778c6b9-c12e-45c0-8682-2c9e892867b0", + "visible": true, + "parentCellId": "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f" + }, + "generated-edge-simstep-b7259d77-c8df-4060-ac03-58728ae3eb54-c778c6b9-c12e-45c0-8682-2c9e892867b0": { + "path": "generated-edge-simstep-b7259d77-c8df-4060-ac03-58728ae3eb54-c778c6b9-c12e-45c0-8682-2c9e892867b0", + "fileName": "JobGenerator.ts", + "cellName": "Flow 2: Monitor Data Passed to Notification Service", + "cellId": "c778c6b9-c12e-45c0-8682-2c9e892867b0", + "visible": true, + "startLine": 7, + "endLine": 7, + "parentPath": "server/src/service/v2/infrastructure/JobGenerator.ts", + "simSteps": [ + { + "simulationKey": "Multi-Channel Alerting and Notifications", + "simStepId": "b7259d77-c8df-4060-ac03-58728ae3eb54" + } + ] + }, + "987f145b-5df6-4c30-bf1c-2a2d2b0e5cdc": { + "path": "987f145b-5df6-4c30-bf1c-2a2d2b0e5cdc", + "cellName": "Routes", + "cellId": "987f145b-5df6-4c30-bf1c-2a2d2b0e5cdc", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "c4d7e7dd-75b1-44f1-a34a-6ed290a8e562": { + "path": "c4d7e7dd-75b1-44f1-a34a-6ed290a8e562", + "cellName": "index.jsx", + "cellId": "c4d7e7dd-75b1-44f1-a34a-6ed290a8e562", + "visible": true, + "parentCellId": "987f145b-5df6-4c30-bf1c-2a2d2b0e5cdc" + }, + "9d662180-8b65-4426-ad37-cfed4bc01fb4": { + "path": "9d662180-8b65-4426-ad37-cfed4bc01fb4", + "cellName": "StatusPage", + "cellId": "9d662180-8b65-4426-ad37-cfed4bc01fb4", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679" + }, + "773e1065-b0a5-46a9-b0cc-6b963f69f5cc": { + "path": "773e1065-b0a5-46a9-b0cc-6b963f69f5cc", + "cellName": "statusPageRoute.js", + "cellId": "773e1065-b0a5-46a9-b0cc-6b963f69f5cc", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd" + }, + "17a4f435-5a8e-4433-84a6-9ac17362709a": { + "path": "17a4f435-5a8e-4433-84a6-9ac17362709a", + "cellName": "statusPageController.js", + "cellId": "17a4f435-5a8e-4433-84a6-9ac17362709a", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "08602a28-f073-484e-9403-7eb798126dd4": { + "path": "08602a28-f073-484e-9403-7eb798126dd4", + "cellName": "Create", + "cellId": "08602a28-f073-484e-9403-7eb798126dd4", + "visible": true, + "parentCellId": "9d662180-8b65-4426-ad37-cfed4bc01fb4" + }, + "8ef70566-37d9-48fb-b5e8-37e5b68768a2": { + "path": "8ef70566-37d9-48fb-b5e8-37e5b68768a2", + "cellName": "Status", + "cellId": "8ef70566-37d9-48fb-b5e8-37e5b68768a2", + "visible": true, + "parentCellId": "9d662180-8b65-4426-ad37-cfed4bc01fb4" + }, + "f8541fe3-4bcf-4833-a226-66b5301adf58": { + "path": "f8541fe3-4bcf-4833-a226-66b5301adf58", + "cellName": "statusPageModule.js", + "cellId": "f8541fe3-4bcf-4833-a226-66b5301adf58", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380" + }, + "e2d2bbf6-dffd-4edf-8c5d-34088d9d4745": { + "path": "e2d2bbf6-dffd-4edf-8c5d-34088d9d4745", + "cellName": "index.jsx", + "cellId": "e2d2bbf6-dffd-4edf-8c5d-34088d9d4745", + "visible": true, + "parentCellId": "08602a28-f073-484e-9403-7eb798126dd4" + }, + "56f6775e-9843-4ca2-bc8c-1c263b6c35e9": { + "path": "56f6775e-9843-4ca2-bc8c-1c263b6c35e9", + "cellName": "Hooks", + "cellId": "56f6775e-9843-4ca2-bc8c-1c263b6c35e9", + "visible": true, + "parentCellId": "08602a28-f073-484e-9403-7eb798126dd4" + }, + "e1e6f5e0-fd8c-4260-aa4f-cdb096c6b1c3": { + "path": "e1e6f5e0-fd8c-4260-aa4f-cdb096c6b1c3", + "cellName": "index.jsx", + "cellId": "e1e6f5e0-fd8c-4260-aa4f-cdb096c6b1c3", + "visible": true, + "parentCellId": "8ef70566-37d9-48fb-b5e8-37e5b68768a2" + }, + "79da0721-d810-4f7f-9cf9-3ea462bf0bff": { + "path": "79da0721-d810-4f7f-9cf9-3ea462bf0bff", + "cellName": "Hooks", + "cellId": "79da0721-d810-4f7f-9cf9-3ea462bf0bff", + "visible": true, + "parentCellId": "8ef70566-37d9-48fb-b5e8-37e5b68768a2" + }, + "3945e05f-2d99-4d6d-9a3e-b6c75ea3c45a": { + "path": "3945e05f-2d99-4d6d-9a3e-b6c75ea3c45a", + "cellName": "useCreateStatusPage.jsx", + "cellId": "3945e05f-2d99-4d6d-9a3e-b6c75ea3c45a", + "visible": true, + "parentCellId": "56f6775e-9843-4ca2-bc8c-1c263b6c35e9" + }, + "5b2cdab8-66f1-40a1-9621-0efaf54e04ae": { + "path": "5b2cdab8-66f1-40a1-9621-0efaf54e04ae", + "cellName": "useStatusPageFetch.jsx", + "cellId": "5b2cdab8-66f1-40a1-9621-0efaf54e04ae", + "visible": true, + "parentCellId": "79da0721-d810-4f7f-9cf9-3ea462bf0bff" + }, + "00289d11-0015-4b2a-9c5b-dc9fd0f32b80": { + "path": "00289d11-0015-4b2a-9c5b-dc9fd0f32b80", + "cellName": "Create Status Page: User Initiates Creation - index.jsx:L26-313", + "cellId": "00289d11-0015-4b2a-9c5b-dc9fd0f32b80", + "visible": true, + "parentCellId": "e2d2bbf6-dffd-4edf-8c5d-34088d9d4745" + }, + "client/src/Pages/v1/StatusPage/Create/index.jsx-simstep-de7b5d1f-b370-42a4-a410-4178f1e2d3a6": { + "path": "client/src/Pages/v1/StatusPage/Create/index.jsx-simstep-de7b5d1f-b370-42a4-a410-4178f1e2d3a6", + "fileName": "index.jsx", + "wiki": "The user navigates to the 'Create Status Page' screen. The `CreateStatusPage` component renders the UI, including tabs for configuration, and initializes the form state.", + "cellName": "Create Status Page: User Initiates Creation - index.jsx:L26-313", + "cellId": "00289d11-0015-4b2a-9c5b-dc9fd0f32b80", + "visible": true, + "startLine": 26, + "endLine": 313, + "parentCellId": "e2d2bbf6-dffd-4edf-8c5d-34088d9d4745", + "parentPath": "client/src/Pages/v1/StatusPage/Create/index.jsx", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "de7b5d1f-b370-42a4-a410-4178f1e2d3a6" + } + ] + }, + "aa5d6d07-05f0-45f3-b19a-199af50e35f1": { + "path": "aa5d6d07-05f0-45f3-b19a-199af50e35f1", + "cellName": "Create Status Page: Client Hook Sends API Request - useCreateStatusPage.jsx:L8-11", + "cellId": "aa5d6d07-05f0-45f3-b19a-199af50e35f1", + "visible": true, + "parentCellId": "3945e05f-2d99-4d6d-9a3e-b6c75ea3c45a" + }, + "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx-simstep-3c99ae6a-b671-4f47-bed0-ddccd8ff5d35": { + "path": "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx-simstep-3c99ae6a-b671-4f47-bed0-ddccd8ff5d35", + "fileName": "useCreateStatusPage.jsx", + "wiki": "The `useCreateStatusPage` hook receives the form data and uses the `networkService` to send a request to the backend API to create or update the status page.", + "cellName": "Create Status Page: Client Hook Sends API Request - useCreateStatusPage.jsx:L8-11", + "cellId": "aa5d6d07-05f0-45f3-b19a-199af50e35f1", + "visible": true, + "startLine": 8, + "endLine": 11, + "parentCellId": "3945e05f-2d99-4d6d-9a3e-b6c75ea3c45a", + "parentPath": "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "3c99ae6a-b671-4f47-bed0-ddccd8ff5d35" + } + ] + }, + "3efddcad-588f-4492-9808-b0c81c823c96": { + "path": "3efddcad-588f-4492-9808-b0c81c823c96", + "cellName": "Create Status Page: Backend Route Handling - statusPageRoute.js:L14", + "cellId": "3efddcad-588f-4492-9808-b0c81c823c96", + "visible": true, + "parentCellId": "773e1065-b0a5-46a9-b0cc-6b963f69f5cc" + }, + "server/src/routes/v1/statusPageRoute.js-simstep-84f11567-b3c4-42a2-aa7f-d78c1412099a": { + "path": "server/src/routes/v1/statusPageRoute.js-simstep-84f11567-b3c4-42a2-aa7f-d78c1412099a", + "fileName": "statusPageRoute.js", + "wiki": "The backend router receives the POST request on `/api/v1/status-page`. It uses `multer` to handle the file upload and forwards the request to the `createStatusPage` method in the `StatusPageController`.", + "cellName": "Create Status Page: Backend Route Handling - statusPageRoute.js:L14", + "cellId": "3efddcad-588f-4492-9808-b0c81c823c96", + "visible": true, + "startLine": 14, + "endLine": 14, + "parentCellId": "773e1065-b0a5-46a9-b0cc-6b963f69f5cc", + "parentPath": "server/src/routes/v1/statusPageRoute.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "84f11567-b3c4-42a2-aa7f-d78c1412099a" + } + ] + }, + "f15ef77e-380f-4768-98cb-7f0dd4113841": { + "path": "f15ef77e-380f-4768-98cb-7f0dd4113841", + "cellName": "Create Status Page: Controller Logic - statusPageController.js:L18-24", + "cellId": "f15ef77e-380f-4768-98cb-7f0dd4113841", + "visible": true, + "parentCellId": "17a4f435-5a8e-4433-84a6-9ac17362709a" + }, + "server/src/controllers/v1/statusPageController.js-simstep-01694730-8a75-4949-8815-bb4d6e3e2b42": { + "path": "server/src/controllers/v1/statusPageController.js-simstep-01694730-8a75-4949-8815-bb4d6e3e2b42", + "fileName": "statusPageController.js", + "wiki": "The `StatusPageController` validates the incoming request body and file. It then calls the `statusPageModule` to persist the new status page in the database.", + "cellName": "Create Status Page: Controller Logic - statusPageController.js:L18-24", + "cellId": "f15ef77e-380f-4768-98cb-7f0dd4113841", + "visible": true, + "startLine": 18, + "endLine": 24, + "parentCellId": "17a4f435-5a8e-4433-84a6-9ac17362709a", + "parentPath": "server/src/controllers/v1/statusPageController.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "01694730-8a75-4949-8815-bb4d6e3e2b42" + } + ] + }, + "e234917e-a950-48f5-b864-cd957b2216e1": { + "path": "e234917e-a950-48f5-b864-cd957b2216e1", + "cellName": "Create Status Page: Database Record Creation - statusPageModule.js:L14-34", + "cellId": "e234917e-a950-48f5-b864-cd957b2216e1", + "visible": true, + "parentCellId": "f8541fe3-4bcf-4833-a226-66b5301adf58" + }, + "server/src/db/v1/modules/statusPageModule.js-simstep-b6fa93cd-6fca-4d9e-969a-5400e2437ee8": { + "path": "server/src/db/v1/modules/statusPageModule.js-simstep-b6fa93cd-6fca-4d9e-969a-5400e2437ee8", + "fileName": "statusPageModule.js", + "wiki": "The `StatusPageModule` creates a new instance of the `StatusPage` Mongoose model, populates it with the provided data including the image buffer, and saves it to the MongoDB database. It also handles duplicate URL errors.", + "cellName": "Create Status Page: Database Record Creation - statusPageModule.js:L14-34", + "cellId": "e234917e-a950-48f5-b864-cd957b2216e1", + "visible": true, + "startLine": 14, + "endLine": 34, + "parentCellId": "f8541fe3-4bcf-4833-a226-66b5301adf58", + "parentPath": "server/src/db/v1/modules/statusPageModule.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "b6fa93cd-6fca-4d9e-969a-5400e2437ee8" + } + ] + }, + "058ded1f-83ce-40a7-a99d-d178c81fe557": { + "path": "058ded1f-83ce-40a7-a99d-d178c81fe557", + "cellName": "Create Status Page: Controller Sends Success Response - statusPageController.js:L25-28", + "cellId": "058ded1f-83ce-40a7-a99d-d178c81fe557", + "visible": true, + "parentCellId": "17a4f435-5a8e-4433-84a6-9ac17362709a" + }, + "server/src/controllers/v1/statusPageController.js-simstep-2a115421-5175-4c8f-8cdd-62f3fe148af5": { + "path": "server/src/controllers/v1/statusPageController.js-simstep-2a115421-5175-4c8f-8cdd-62f3fe148af5", + "fileName": "statusPageController.js", + "wiki": "The controller receives the newly created status page object from the database module and sends a successful JSON response back to the client, including a success message and the new data.", + "cellName": "Create Status Page: Controller Sends Success Response - statusPageController.js:L25-28", + "cellId": "058ded1f-83ce-40a7-a99d-d178c81fe557", + "visible": true, + "startLine": 25, + "endLine": 28, + "parentCellId": "17a4f435-5a8e-4433-84a6-9ac17362709a", + "parentPath": "server/src/controllers/v1/statusPageController.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "2a115421-5175-4c8f-8cdd-62f3fe148af5" + } + ] + }, + "30aaee38-f509-4e19-a917-e6d2f4bba61c": { + "path": "30aaee38-f509-4e19-a917-e6d2f4bba61c", + "cellName": "View Status Page: User Navigates to URL - index.jsx:L148-152", + "cellId": "30aaee38-f509-4e19-a917-e6d2f4bba61c", + "visible": true, + "parentCellId": "c4d7e7dd-75b1-44f1-a34a-6ed290a8e562" + }, + "client/src/Routes/index.jsx-simstep-132354d7-df7a-4e03-9d00-16d54f836a1b": { + "path": "client/src/Routes/index.jsx-simstep-132354d7-df7a-4e03-9d00-16d54f836a1b", + "fileName": "index.jsx", + "wiki": "A user accesses a status page URL (e.g., /status/uptime/my-awesome-service). The React router matches the path and renders the `Status` component, which is responsible for fetching and displaying the page details.", + "cellName": "View Status Page: User Navigates to URL - index.jsx:L148-152", + "cellId": "30aaee38-f509-4e19-a917-e6d2f4bba61c", + "visible": true, + "startLine": 148, + "endLine": 152, + "parentCellId": "c4d7e7dd-75b1-44f1-a34a-6ed290a8e562", + "parentPath": "client/src/Routes/index.jsx", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "132354d7-df7a-4e03-9d00-16d54f836a1b" + } + ] + }, + "ecfb2d04-2681-4f16-af75-632f69e1a868": { + "path": "ecfb2d04-2681-4f16-af75-632f69e1a868", + "cellName": "View Status Page: Client Hook Sends API Request - useStatusPageFetch.jsx:L15-18", + "cellId": "ecfb2d04-2681-4f16-af75-632f69e1a868", + "visible": true, + "parentCellId": "5b2cdab8-66f1-40a1-9621-0efaf54e04ae" + }, + "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx-simstep-b57bb249-9066-454e-aa8c-89108fdfcc2e": { + "path": "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx-simstep-b57bb249-9066-454e-aa8c-89108fdfcc2e", + "fileName": "useStatusPageFetch.jsx", + "wiki": "The `useStatusPageFetch` hook uses the `networkService` to send a GET request to the backend API to retrieve the status page data based on its URL.", + "cellName": "View Status Page: Client Hook Sends API Request - useStatusPageFetch.jsx:L15-18", + "cellId": "ecfb2d04-2681-4f16-af75-632f69e1a868", + "visible": true, + "startLine": 15, + "endLine": 18, + "parentCellId": "5b2cdab8-66f1-40a1-9621-0efaf54e04ae", + "parentPath": "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "b57bb249-9066-454e-aa8c-89108fdfcc2e" + } + ] + }, + "2e26cd2f-1413-4c16-8e63-ea7808ab6c52": { + "path": "2e26cd2f-1413-4c16-8e63-ea7808ab6c52", + "cellName": "View Status Page: Backend Route Handling - statusPageRoute.js:L16", + "cellId": "2e26cd2f-1413-4c16-8e63-ea7808ab6c52", + "visible": true, + "parentCellId": "773e1065-b0a5-46a9-b0cc-6b963f69f5cc" + }, + "server/src/routes/v1/statusPageRoute.js-simstep-477c6821-b29b-4b38-8930-ced21b8a75e5": { + "path": "server/src/routes/v1/statusPageRoute.js-simstep-477c6821-b29b-4b38-8930-ced21b8a75e5", + "fileName": "statusPageRoute.js", + "wiki": "The backend router receives the GET request on `/api/v1/status-page/:url` and forwards it to the `getStatusPageByUrl` method in the `StatusPageController`.", + "cellName": "View Status Page: Backend Route Handling - statusPageRoute.js:L16", + "cellId": "2e26cd2f-1413-4c16-8e63-ea7808ab6c52", + "visible": true, + "startLine": 16, + "endLine": 16, + "parentCellId": "773e1065-b0a5-46a9-b0cc-6b963f69f5cc", + "parentPath": "server/src/routes/v1/statusPageRoute.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "477c6821-b29b-4b38-8930-ced21b8a75e5" + } + ] + }, + "9f217674-4cd7-4ef8-b088-d5620042a7ba": { + "path": "9f217674-4cd7-4ef8-b088-d5620042a7ba", + "cellName": "View Status Page: Controller Logic - statusPageController.js:L75-78", + "cellId": "9f217674-4cd7-4ef8-b088-d5620042a7ba", + "visible": true, + "parentCellId": "17a4f435-5a8e-4433-84a6-9ac17362709a" + }, + "server/src/controllers/v1/statusPageController.js-simstep-5bdbd179-ce8f-4171-a073-af72a99b4468": { + "path": "server/src/controllers/v1/statusPageController.js-simstep-5bdbd179-ce8f-4171-a073-af72a99b4468", + "fileName": "statusPageController.js", + "wiki": "The `StatusPageController` validates the request parameters and calls the `statusPageModule` to retrieve the status page data from the database.", + "cellName": "View Status Page: Controller Logic - statusPageController.js:L75-78", + "cellId": "9f217674-4cd7-4ef8-b088-d5620042a7ba", + "visible": true, + "startLine": 75, + "endLine": 78, + "parentCellId": "17a4f435-5a8e-4433-84a6-9ac17362709a", + "parentPath": "server/src/controllers/v1/statusPageController.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "5bdbd179-ce8f-4171-a073-af72a99b4468" + } + ] + }, + "26da60f9-dde4-44ec-be89-91858cc14f90": { + "path": "26da60f9-dde4-44ec-be89-91858cc14f90", + "cellName": "View Status Page: Database Record Retrieval - statusPageModule.js:L89-299", + "cellId": "26da60f9-dde4-44ec-be89-91858cc14f90", + "visible": true, + "parentCellId": "f8541fe3-4bcf-4833-a226-66b5301adf58" + }, + "server/src/db/v1/modules/statusPageModule.js-simstep-ceb044e9-6545-4d04-bf79-ec8146eb8ec9": { + "path": "server/src/db/v1/modules/statusPageModule.js-simstep-ceb044e9-6545-4d04-bf79-ec8146eb8ec9", + "fileName": "statusPageModule.js", + "wiki": "The `StatusPageModule` executes a complex MongoDB aggregation pipeline to find the status page by its URL, and then look up and join data for all associated monitors and their recent checks.", + "cellName": "View Status Page: Database Record Retrieval - statusPageModule.js:L89-299", + "cellId": "26da60f9-dde4-44ec-be89-91858cc14f90", + "visible": true, + "startLine": 89, + "endLine": 299, + "parentCellId": "f8541fe3-4bcf-4833-a226-66b5301adf58", + "parentPath": "server/src/db/v1/modules/statusPageModule.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "ceb044e9-6545-4d04-bf79-ec8146eb8ec9" + } + ] + }, + "d76e70e0-41f9-46cc-b775-b2027c5a0d5b": { + "path": "d76e70e0-41f9-46cc-b775-b2027c5a0d5b", + "cellName": "View Status Page: Controller Sends Success Response - statusPageController.js:L79-82", + "cellId": "d76e70e0-41f9-46cc-b775-b2027c5a0d5b", + "visible": true, + "parentCellId": "17a4f435-5a8e-4433-84a6-9ac17362709a" + }, + "server/src/controllers/v1/statusPageController.js-simstep-83224406-2f1d-4030-9beb-d65c2baf544d": { + "path": "server/src/controllers/v1/statusPageController.js-simstep-83224406-2f1d-4030-9beb-d65c2baf544d", + "fileName": "statusPageController.js", + "wiki": "The controller receives the status page data and sends a successful JSON response back to the client.", + "cellName": "View Status Page: Controller Sends Success Response - statusPageController.js:L79-82", + "cellId": "d76e70e0-41f9-46cc-b775-b2027c5a0d5b", + "visible": true, + "startLine": 79, + "endLine": 82, + "parentCellId": "17a4f435-5a8e-4433-84a6-9ac17362709a", + "parentPath": "server/src/controllers/v1/statusPageController.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "83224406-2f1d-4030-9beb-d65c2baf544d" + } + ] + }, + "f59b91de-5196-4a43-9d57-3f28402c0672": { + "path": "f59b91de-5196-4a43-9d57-3f28402c0672", + "cellName": "View Status Page: Component Renders UI - index.jsx:L145-163", + "cellId": "f59b91de-5196-4a43-9d57-3f28402c0672", + "visible": true, + "parentCellId": "e1e6f5e0-fd8c-4260-aa4f-cdb096c6b1c3" + }, + "client/src/Pages/v1/StatusPage/Status/index.jsx-simstep-3d51e811-a659-442e-97e0-00798cd15647": { + "path": "client/src/Pages/v1/StatusPage/Status/index.jsx-simstep-3d51e811-a659-442e-97e0-00798cd15647", + "fileName": "index.jsx", + "wiki": "The `Status` component re-renders with the fetched data. It displays the company name, logo, overall status bar, and a list of monitors with their individual status and historical uptime charts.", + "cellName": "View Status Page: Component Renders UI - index.jsx:L145-163", + "cellId": "f59b91de-5196-4a43-9d57-3f28402c0672", + "visible": true, + "startLine": 145, + "endLine": 163, + "parentCellId": "e1e6f5e0-fd8c-4260-aa4f-cdb096c6b1c3", + "parentPath": "client/src/Pages/v1/StatusPage/Status/index.jsx", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "3d51e811-a659-442e-97e0-00798cd15647" + } + ] + }, + "de23eee2-b60a-4236-a890-3c5b78388bb3": { + "path": "de23eee2-b60a-4236-a890-3c5b78388bb3", + "cellName": "Create Status\nPage: Form\nSubmission", + "cellId": "de23eee2-b60a-4236-a890-3c5b78388bb3", + "visible": true, + "parentCellId": "08602a28-f073-484e-9403-7eb798126dd4" + }, + "generated-edge-simstep-d419f7b6-7b9b-4494-9fad-83e3ce208ca4-de23eee2-b60a-4236-a890-3c5b78388bb3": { + "path": "generated-edge-simstep-d419f7b6-7b9b-4494-9fad-83e3ce208ca4-de23eee2-b60a-4236-a890-3c5b78388bb3", + "fileName": "index.jsx", + "cellName": "Create Status Page: Form Submission", + "cellId": "de23eee2-b60a-4236-a890-3c5b78388bb3", + "visible": true, + "startLine": 143, + "endLine": 159, + "parentPath": "client/src/Pages/v1/StatusPage/Create/index.jsx", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "d419f7b6-7b9b-4494-9fad-83e3ce208ca4" + } + ] + }, + "d7faa713-4204-4a97-a172-d621da36c287": { + "path": "d7faa713-4204-4a97-a172-d621da36c287", + "cellName": "Create Status\nPage: API\nCall to\nBackend", + "cellId": "d7faa713-4204-4a97-a172-d621da36c287", + "visible": true + }, + "generated-edge-simstep-8a2ab372-73db-4b9f-bc0e-6a20f15a4d79-d7faa713-4204-4a97-a172-d621da36c287": { + "path": "generated-edge-simstep-8a2ab372-73db-4b9f-bc0e-6a20f15a4d79-d7faa713-4204-4a97-a172-d621da36c287", + "fileName": "useCreateStatusPage.jsx", + "cellName": "Create Status Page: API Call to Backend", + "cellId": "d7faa713-4204-4a97-a172-d621da36c287", + "visible": true, + "startLine": 912, + "endLine": 944, + "parentPath": "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "8a2ab372-73db-4b9f-bc0e-6a20f15a4d79" + } + ] + }, + "a3f28fa6-8268-4c13-95a0-83e3b1abdcb0": { + "path": "a3f28fa6-8268-4c13-95a0-83e3b1abdcb0", + "cellName": "Create Status\nPage: Data\nPassed to\nController", + "cellId": "a3f28fa6-8268-4c13-95a0-83e3b1abdcb0", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-0923a533-c572-4da3-ad87-cdfff111f3bb-a3f28fa6-8268-4c13-95a0-83e3b1abdcb0": { + "path": "generated-edge-simstep-0923a533-c572-4da3-ad87-cdfff111f3bb-a3f28fa6-8268-4c13-95a0-83e3b1abdcb0", + "fileName": "statusPageRoute.js", + "cellName": "Create Status Page: Data Passed to Controller", + "cellId": "a3f28fa6-8268-4c13-95a0-83e3b1abdcb0", + "visible": true, + "startLine": 14, + "endLine": 14, + "parentPath": "server/src/routes/v1/statusPageRoute.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "0923a533-c572-4da3-ad87-cdfff111f3bb" + } + ] + }, + "9735611c-924b-49aa-92e8-10d8a86ac288": { + "path": "9735611c-924b-49aa-92e8-10d8a86ac288", + "cellName": "Create Status\nPage: Data\nSent to\nDB Module", + "cellId": "9735611c-924b-49aa-92e8-10d8a86ac288", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-67603f03-c088-4ed0-9f18-072435134cdd-9735611c-924b-49aa-92e8-10d8a86ac288": { + "path": "generated-edge-simstep-67603f03-c088-4ed0-9f18-072435134cdd-9735611c-924b-49aa-92e8-10d8a86ac288", + "fileName": "statusPageController.js", + "cellName": "Create Status Page: Data Sent to DB Module", + "cellId": "9735611c-924b-49aa-92e8-10d8a86ac288", + "visible": true, + "startLine": 21, + "endLine": 21, + "parentPath": "server/src/controllers/v1/statusPageController.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "67603f03-c088-4ed0-9f18-072435134cdd" + } + ] + }, + "40cb7d7b-2888-44de-bb7d-7735af70ba34": { + "path": "40cb7d7b-2888-44de-bb7d-7735af70ba34", + "cellName": "Create Status\nPage: Database\nResult Returned", + "cellId": "40cb7d7b-2888-44de-bb7d-7735af70ba34", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-ea4b3532-baae-4890-a73e-2ca385d0319e-40cb7d7b-2888-44de-bb7d-7735af70ba34": { + "path": "generated-edge-simstep-ea4b3532-baae-4890-a73e-2ca385d0319e-40cb7d7b-2888-44de-bb7d-7735af70ba34", + "fileName": "statusPageModule.js", + "cellName": "Create Status Page: Database Result Returned", + "cellId": "40cb7d7b-2888-44de-bb7d-7735af70ba34", + "visible": true, + "startLine": 27, + "endLine": 27, + "parentPath": "server/src/db/v1/modules/statusPageModule.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "ea4b3532-baae-4890-a73e-2ca385d0319e" + } + ] + }, + "c3f83811-8674-4dbb-9f84-79499f97fb1f": { + "path": "c3f83811-8674-4dbb-9f84-79499f97fb1f", + "cellName": "Create Status\nPage: UI\nReceives Response\nand Redirects", + "cellId": "c3f83811-8674-4dbb-9f84-79499f97fb1f", + "visible": true + }, + "generated-edge-simstep-210acc07-81c3-4394-b566-41f6031e8eeb-c3f83811-8674-4dbb-9f84-79499f97fb1f": { + "path": "generated-edge-simstep-210acc07-81c3-4394-b566-41f6031e8eeb-c3f83811-8674-4dbb-9f84-79499f97fb1f", + "fileName": "statusPageController.js", + "cellName": "Create Status Page: UI Receives Response and Redirects", + "cellId": "c3f83811-8674-4dbb-9f84-79499f97fb1f", + "visible": true, + "startLine": 151, + "endLine": 156, + "parentPath": "server/src/controllers/v1/statusPageController.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "210acc07-81c3-4394-b566-41f6031e8eeb" + } + ] + }, + "0575bf87-5362-4988-b895-7e514148d420": { + "path": "0575bf87-5362-4988-b895-7e514148d420", + "cellName": "View Status\nPage: Component\nTriggers Data\nFetch", + "cellId": "0575bf87-5362-4988-b895-7e514148d420", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "generated-edge-simstep-2ba49e28-1cac-47f5-9118-ab930788736a-0575bf87-5362-4988-b895-7e514148d420": { + "path": "generated-edge-simstep-2ba49e28-1cac-47f5-9118-ab930788736a-0575bf87-5362-4988-b895-7e514148d420", + "fileName": "index.jsx", + "cellName": "View Status Page: Component Triggers Data Fetch", + "cellId": "0575bf87-5362-4988-b895-7e514148d420", + "visible": true, + "startLine": 29, + "endLine": 29, + "parentPath": "client/src/Routes/index.jsx", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "2ba49e28-1cac-47f5-9118-ab930788736a" + } + ] + }, + "ae1c9341-0e39-4d91-a9ac-5cc8a627c247": { + "path": "ae1c9341-0e39-4d91-a9ac-5cc8a627c247", + "cellName": "View Status\nPage: API\nCall to\nBackend", + "cellId": "ae1c9341-0e39-4d91-a9ac-5cc8a627c247", + "visible": true + }, + "generated-edge-simstep-de0714d2-bf1e-4a1a-a389-60793de8b772-ae1c9341-0e39-4d91-a9ac-5cc8a627c247": { + "path": "generated-edge-simstep-de0714d2-bf1e-4a1a-a389-60793de8b772-ae1c9341-0e39-4d91-a9ac-5cc8a627c247", + "fileName": "useStatusPageFetch.jsx", + "cellName": "View Status Page: API Call to Backend", + "cellId": "ae1c9341-0e39-4d91-a9ac-5cc8a627c247", + "visible": true, + "startLine": 896, + "endLine": 902, + "parentPath": "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "de0714d2-bf1e-4a1a-a389-60793de8b772" + } + ] + }, + "5735f30c-b55f-444a-9236-f3d75f68ed27": { + "path": "5735f30c-b55f-444a-9236-f3d75f68ed27", + "cellName": "View Status\nPage: Data\nPassed to\nController", + "cellId": "5735f30c-b55f-444a-9236-f3d75f68ed27", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-b79b8922-4de6-477d-8648-37b4bd9a6767-5735f30c-b55f-444a-9236-f3d75f68ed27": { + "path": "generated-edge-simstep-b79b8922-4de6-477d-8648-37b4bd9a6767-5735f30c-b55f-444a-9236-f3d75f68ed27", + "fileName": "statusPageRoute.js", + "cellName": "View Status Page: Data Passed to Controller", + "cellId": "5735f30c-b55f-444a-9236-f3d75f68ed27", + "visible": true, + "startLine": 16, + "endLine": 16, + "parentPath": "server/src/routes/v1/statusPageRoute.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "b79b8922-4de6-477d-8648-37b4bd9a6767" + } + ] + }, + "dace45d6-fb25-4dae-b940-49ef7d77bbed": { + "path": "dace45d6-fb25-4dae-b940-49ef7d77bbed", + "cellName": "View Status\nPage: URL\nSent to\nDB Module", + "cellId": "dace45d6-fb25-4dae-b940-49ef7d77bbed", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-788307ef-4cfa-4f5a-b2e9-a0bfc7a378d8-dace45d6-fb25-4dae-b940-49ef7d77bbed": { + "path": "generated-edge-simstep-788307ef-4cfa-4f5a-b2e9-a0bfc7a378d8-dace45d6-fb25-4dae-b940-49ef7d77bbed", + "fileName": "statusPageController.js", + "cellName": "View Status Page: URL Sent to DB Module", + "cellId": "dace45d6-fb25-4dae-b940-49ef7d77bbed", + "visible": true, + "startLine": 78, + "endLine": 78, + "parentPath": "server/src/controllers/v1/statusPageController.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "788307ef-4cfa-4f5a-b2e9-a0bfc7a378d8" + } + ] + }, + "137a5ab7-1e49-42ec-869d-58f7f2380cb3": { + "path": "137a5ab7-1e49-42ec-869d-58f7f2380cb3", + "cellName": "View Status\nPage: Database\nResult Returned", + "cellId": "137a5ab7-1e49-42ec-869d-58f7f2380cb3", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-4d3fad92-8fa9-4a8f-be3b-f09eda2f8dd4-137a5ab7-1e49-42ec-869d-58f7f2380cb3": { + "path": "generated-edge-simstep-4d3fad92-8fa9-4a8f-be3b-f09eda2f8dd4-137a5ab7-1e49-42ec-869d-58f7f2380cb3", + "fileName": "statusPageModule.js", + "cellName": "View Status Page: Database Result Returned", + "cellId": "137a5ab7-1e49-42ec-869d-58f7f2380cb3", + "visible": true, + "startLine": 291, + "endLine": 291, + "parentPath": "server/src/db/v1/modules/statusPageModule.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "4d3fad92-8fa9-4a8f-be3b-f09eda2f8dd4" + } + ] + }, + "04a33e4f-1dfe-4c5a-8b30-01ed0591c380": { + "path": "04a33e4f-1dfe-4c5a-8b30-01ed0591c380", + "cellName": "View Status\nPage: UI\nReceives Data", + "cellId": "04a33e4f-1dfe-4c5a-8b30-01ed0591c380", + "visible": true + }, + "generated-edge-simstep-d5f3c300-9777-4e09-bb05-f1281c2ff29a-04a33e4f-1dfe-4c5a-8b30-01ed0591c380": { + "path": "generated-edge-simstep-d5f3c300-9777-4e09-bb05-f1281c2ff29a-04a33e4f-1dfe-4c5a-8b30-01ed0591c380", + "fileName": "statusPageController.js", + "cellName": "View Status Page: UI Receives Data", + "cellId": "04a33e4f-1dfe-4c5a-8b30-01ed0591c380", + "visible": true, + "startLine": 20, + "endLine": 26, + "parentPath": "server/src/controllers/v1/statusPageController.js", + "simSteps": [ + { + "simulationKey": "Customizable Public and Private Status Pages", + "simStepId": "d5f3c300-9777-4e09-bb05-f1281c2ff29a" + } + ] + }, + "933a6cdc-349c-4725-b7ae-14cf780bfde8": { + "path": "933a6cdc-349c-4725-b7ae-14cf780bfde8", + "cellName": "checkHooks.js", + "cellId": "933a6cdc-349c-4725-b7ae-14cf780bfde8", + "visible": true, + "parentCellId": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961" + }, + "d18dbe1d-2f2c-4336-bd21-47c1670f7bab": { + "path": "d18dbe1d-2f2c-4336-bd21-47c1670f7bab", + "cellName": "checkRoute.js", + "cellId": "d18dbe1d-2f2c-4336-bd21-47c1670f7bab", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd" + }, + "d983e54f-2fbe-496a-932d-a827e1124804": { + "path": "d983e54f-2fbe-496a-932d-a827e1124804", + "cellName": "checkController.js", + "cellId": "d983e54f-2fbe-496a-932d-a827e1124804", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "273f3009-69d0-46fe-af57-3cd94a24b55b": { + "path": "273f3009-69d0-46fe-af57-3cd94a24b55b", + "cellName": "Details", + "cellId": "273f3009-69d0-46fe-af57-3cd94a24b55b", + "visible": true, + "parentCellId": "7090fcdb-09be-4c85-86fd-e73553bbb2ff" + }, + "f90db867-e1e7-4554-b446-fa78fc6a8578": { + "path": "f90db867-e1e7-4554-b446-fa78fc6a8578", + "cellName": "checkService.js", + "cellId": "f90db867-e1e7-4554-b446-fa78fc6a8578", + "visible": true, + "parentCellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb" + }, + "860b5eec-a1ce-4e1f-a316-e1586f03af96": { + "path": "860b5eec-a1ce-4e1f-a316-e1586f03af96", + "cellName": "checkModule.js", + "cellId": "860b5eec-a1ce-4e1f-a316-e1586f03af96", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380" + }, + "1807d5b9-54be-430b-8997-cada4dd9b521": { + "path": "1807d5b9-54be-430b-8997-cada4dd9b521", + "cellName": "index.jsx", + "cellId": "1807d5b9-54be-430b-8997-cada4dd9b521", + "visible": true, + "parentCellId": "273f3009-69d0-46fe-af57-3cd94a24b55b" + }, + "6aed78bb-62f1-4ac1-9b9a-9d5977e87404": { + "path": "6aed78bb-62f1-4ac1-9b9a-9d5977e87404", + "cellName": "Components", + "cellId": "6aed78bb-62f1-4ac1-9b9a-9d5977e87404", + "visible": true, + "parentCellId": "273f3009-69d0-46fe-af57-3cd94a24b55b" + }, + "5ac0878f-d238-4763-8464-a85b4885a40f": { + "path": "5ac0878f-d238-4763-8464-a85b4885a40f", + "cellName": "ResponseTable", + "cellId": "5ac0878f-d238-4763-8464-a85b4885a40f", + "visible": true, + "parentCellId": "6aed78bb-62f1-4ac1-9b9a-9d5977e87404" + }, + "d3f74ac7-3b9a-4cae-8d77-a0d5d605bb27": { + "path": "d3f74ac7-3b9a-4cae-8d77-a0d5d605bb27", + "cellName": "index.jsx", + "cellId": "d3f74ac7-3b9a-4cae-8d77-a0d5d605bb27", + "visible": true, + "parentCellId": "5ac0878f-d238-4763-8464-a85b4885a40f" + }, + "78a8a7c5-41d3-4453-a7f7-ac632bbef288": { + "path": "78a8a7c5-41d3-4453-a7f7-ac632bbef288", + "cellName": "Navigate to Monitor Details - index.jsx:L27-188", + "cellId": "78a8a7c5-41d3-4453-a7f7-ac632bbef288", + "visible": true, + "parentCellId": "1807d5b9-54be-430b-8997-cada4dd9b521" + }, + "client/src/Pages/v1/Uptime/Details/index.jsx-simstep-35195328-9847-4678-b014-9331553f0d19": { + "path": "client/src/Pages/v1/Uptime/Details/index.jsx-simstep-35195328-9847-4678-b014-9331553f0d19", + "fileName": "index.jsx", + "wiki": "The user navigates to the details page for a specific uptime monitor. The `UptimeDetails` component mounts and initializes its state and hooks.", + "cellName": "Navigate to Monitor Details - index.jsx:L27-188", + "cellId": "78a8a7c5-41d3-4453-a7f7-ac632bbef288", + "visible": true, + "startLine": 27, + "endLine": 188, + "parentCellId": "1807d5b9-54be-430b-8997-cada4dd9b521", + "parentPath": "client/src/Pages/v1/Uptime/Details/index.jsx", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "35195328-9847-4678-b014-9331553f0d19" + } + ] + }, + "4a5847d0-91a0-4b4c-8517-2f4ff9a7e8d4": { + "path": "4a5847d0-91a0-4b4c-8517-2f4ff9a7e8d4", + "cellName": "Fetch Monitor Checks Hook - checkHooks.js:L89-121", + "cellId": "4a5847d0-91a0-4b4c-8517-2f4ff9a7e8d4", + "visible": true, + "parentCellId": "933a6cdc-349c-4725-b7ae-14cf780bfde8" + }, + "client/src/Hooks/v1/checkHooks.js-simstep-8dc4347c-3e9e-40d7-ae6f-54684f2e6956": { + "path": "client/src/Hooks/v1/checkHooks.js-simstep-8dc4347c-3e9e-40d7-ae6f-54684f2e6956", + "fileName": "checkHooks.js", + "wiki": "The `useFetchChecksByMonitor` custom hook executes. It sets the loading state and prepares to call the network service to fetch the data from the backend API.", + "cellName": "Fetch Monitor Checks Hook - checkHooks.js:L89-121", + "cellId": "4a5847d0-91a0-4b4c-8517-2f4ff9a7e8d4", + "visible": true, + "startLine": 89, + "endLine": 121, + "parentCellId": "933a6cdc-349c-4725-b7ae-14cf780bfde8", + "parentPath": "client/src/Hooks/v1/checkHooks.js", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "8dc4347c-3e9e-40d7-ae6f-54684f2e6956" + } + ] + }, + "cdd090be-0b62-45b4-baa0-fec01fc3ffd1": { + "path": "cdd090be-0b62-45b4-baa0-fec01fc3ffd1", + "cellName": "Route Request to Controller - checkRoute.js:L19", + "cellId": "cdd090be-0b62-45b4-baa0-fec01fc3ffd1", + "visible": true, + "parentCellId": "d18dbe1d-2f2c-4336-bd21-47c1670f7bab" + }, + "server/src/routes/v1/checkRoute.js-simstep-c5fd192f-ea28-48a0-b3e7-5bc04f521192": { + "path": "server/src/routes/v1/checkRoute.js-simstep-c5fd192f-ea28-48a0-b3e7-5bc04f521192", + "fileName": "checkRoute.js", + "wiki": "The backend Express router receives the incoming GET request and matches it to the appropriate route, forwarding the request to the `getChecksByMonitor` method in the `CheckController`.", + "cellName": "Route Request to Controller - checkRoute.js:L19", + "cellId": "cdd090be-0b62-45b4-baa0-fec01fc3ffd1", + "visible": true, + "startLine": 19, + "endLine": 19, + "parentCellId": "d18dbe1d-2f2c-4336-bd21-47c1670f7bab", + "parentPath": "server/src/routes/v1/checkRoute.js", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "c5fd192f-ea28-48a0-b3e7-5bc04f521192" + } + ] + }, + "499b46a2-f2a2-4ccd-aecd-012eade2bd23": { + "path": "499b46a2-f2a2-4ccd-aecd-012eade2bd23", + "cellName": "Fetch Checks from Database - checkService.js:L36-45", + "cellId": "499b46a2-f2a2-4ccd-aecd-012eade2bd23", + "visible": true, + "parentCellId": "f90db867-e1e7-4554-b446-fa78fc6a8578" + }, + "server/src/service/v1/business/checkService.js-simstep-83fea05d-8306-4a73-a43f-882cdd14f50f": { + "path": "server/src/service/v1/business/checkService.js-simstep-83fea05d-8306-4a73-a43f-882cdd14f50f", + "fileName": "checkService.js", + "wiki": "The `CheckService` calls the `checkModule` to interact with the database. It passes the necessary parameters to query for the check history.", + "cellName": "Fetch Checks from Database - checkService.js:L36-45", + "cellId": "499b46a2-f2a2-4ccd-aecd-012eade2bd23", + "visible": true, + "startLine": 36, + "endLine": 45, + "parentCellId": "f90db867-e1e7-4554-b446-fa78fc6a8578", + "parentPath": "server/src/service/v1/business/checkService.js", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "83fea05d-8306-4a73-a43f-882cdd14f50f" + } + ] + }, + "14df9f01-1358-442d-96f5-eab9a4c4abbf": { + "path": "14df9f01-1358-442d-96f5-eab9a4c4abbf", + "cellName": "Return Check Data - checkController.js:L87-90", + "cellId": "14df9f01-1358-442d-96f5-eab9a4c4abbf", + "visible": true, + "parentCellId": "d983e54f-2fbe-496a-932d-a827e1124804" + }, + "server/src/controllers/v1/checkController.js-simstep-5768fdc5-045c-48ef-b0c9-79769403648f": { + "path": "server/src/controllers/v1/checkController.js-simstep-5768fdc5-045c-48ef-b0c9-79769403648f", + "fileName": "checkController.js", + "wiki": "The database returns the query results, which flow back up through the `checkModule` and `checkService`. The `checkController` then formats a successful JSON response.", + "cellName": "Return Check Data - checkController.js:L87-90", + "cellId": "14df9f01-1358-442d-96f5-eab9a4c4abbf", + "visible": true, + "startLine": 87, + "endLine": 90, + "parentCellId": "d983e54f-2fbe-496a-932d-a827e1124804", + "parentPath": "server/src/controllers/v1/checkController.js", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "5768fdc5-045c-48ef-b0c9-79769403648f" + } + ] + }, + "ceb9cda3-968d-44aa-b223-49902adb93b4": { + "path": "ceb9cda3-968d-44aa-b223-49902adb93b4", + "cellName": "Update Frontend State - checkHooks.js:L109-110", + "cellId": "ceb9cda3-968d-44aa-b223-49902adb93b4", + "visible": true, + "parentCellId": "933a6cdc-349c-4725-b7ae-14cf780bfde8" + }, + "client/src/Hooks/v1/checkHooks.js-simstep-faba07f3-7460-47ae-924d-f92f5b2e821d": { + "path": "client/src/Hooks/v1/checkHooks.js-simstep-faba07f3-7460-47ae-924d-f92f5b2e821d", + "fileName": "checkHooks.js", + "wiki": "The `useFetchChecksByMonitor` hook receives the data from the API call and updates the component's state with the new list of checks and the total count.", + "cellName": "Update Frontend State - checkHooks.js:L109-110", + "cellId": "ceb9cda3-968d-44aa-b223-49902adb93b4", + "visible": true, + "startLine": 109, + "endLine": 110, + "parentCellId": "933a6cdc-349c-4725-b7ae-14cf780bfde8", + "parentPath": "client/src/Hooks/v1/checkHooks.js", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "faba07f3-7460-47ae-924d-f92f5b2e821d" + } + ] + }, + "c996f7ca-9384-4252-b343-7bfc049d14bb": { + "path": "c996f7ca-9384-4252-b343-7bfc049d14bb", + "cellName": "Render Historical Data Table - index.jsx:L11-92", + "cellId": "c996f7ca-9384-4252-b343-7bfc049d14bb", + "visible": true, + "parentCellId": "d3f74ac7-3b9a-4cae-8d77-a0d5d605bb27" + }, + "client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx-simstep-2b1651cd-2e70-4cc6-b2bb-23f94fa73d66": { + "path": "client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx-simstep-2b1651cd-2e70-4cc6-b2bb-23f94fa73d66", + "fileName": "index.jsx", + "wiki": "The `ResponseTable` component receives the list of checks and renders them in a data table, allowing the user to view the incident history, including status, response time, and when the check occurred.", + "cellName": "Render Historical Data Table - index.jsx:L11-92", + "cellId": "c996f7ca-9384-4252-b343-7bfc049d14bb", + "visible": true, + "startLine": 11, + "endLine": 92, + "parentCellId": "d3f74ac7-3b9a-4cae-8d77-a0d5d605bb27", + "parentPath": "client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "2b1651cd-2e70-4cc6-b2bb-23f94fa73d66" + } + ] + }, + "e90e4943-6108-46e3-8a2c-a9a1c7361279": { + "path": "e90e4943-6108-46e3-8a2c-a9a1c7361279", + "cellName": "Trigger Check\nHistory Fetch", + "cellId": "e90e4943-6108-46e3-8a2c-a9a1c7361279", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "generated-edge-simstep-49979250-1a32-4da5-95e0-58652fe218f9-e90e4943-6108-46e3-8a2c-a9a1c7361279": { + "path": "generated-edge-simstep-49979250-1a32-4da5-95e0-58652fe218f9-e90e4943-6108-46e3-8a2c-a9a1c7361279", + "fileName": "index.jsx", + "cellName": "Trigger Check History Fetch", + "cellId": "e90e4943-6108-46e3-8a2c-a9a1c7361279", + "visible": true, + "startLine": 69, + "endLine": 77, + "parentPath": "client/src/Pages/v1/Uptime/Details/index.jsx", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "49979250-1a32-4da5-95e0-58652fe218f9" + } + ] + }, + "2b8ae59e-ed8d-4837-9a9b-64eb480f982c": { + "path": "2b8ae59e-ed8d-4837-9a9b-64eb480f982c", + "cellName": "API Request\nfor Check\nHistory", + "cellId": "2b8ae59e-ed8d-4837-9a9b-64eb480f982c", + "visible": true + }, + "generated-edge-simstep-336b534d-8d6a-4639-af8e-889b38f51ab1-2b8ae59e-ed8d-4837-9a9b-64eb480f982c": { + "path": "generated-edge-simstep-336b534d-8d6a-4639-af8e-889b38f51ab1-2b8ae59e-ed8d-4837-9a9b-64eb480f982c", + "fileName": "checkHooks.js", + "cellName": "API Request for Check History", + "cellId": "2b8ae59e-ed8d-4837-9a9b-64eb480f982c", + "visible": true, + "startLine": 551, + "endLine": 562, + "parentPath": "client/src/Hooks/v1/checkHooks.js", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "336b534d-8d6a-4639-af8e-889b38f51ab1" + } + ] + }, + "839ba9f6-8a34-4919-a04a-13fba1795c61": { + "path": "839ba9f6-8a34-4919-a04a-13fba1795c61", + "cellName": "Forward Request\nto Service\nLayer", + "cellId": "839ba9f6-8a34-4919-a04a-13fba1795c61", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-4f73f0fe-b390-4fe1-a354-eb0857835126-839ba9f6-8a34-4919-a04a-13fba1795c61": { + "path": "generated-edge-simstep-4f73f0fe-b390-4fe1-a354-eb0857835126-839ba9f6-8a34-4919-a04a-13fba1795c61", + "fileName": "checkRoute.js", + "cellName": "Forward Request to Service Layer", + "cellId": "839ba9f6-8a34-4919-a04a-13fba1795c61", + "visible": true, + "startLine": 81, + "endLine": 85, + "parentPath": "server/src/routes/v1/checkRoute.js", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "4f73f0fe-b390-4fe1-a354-eb0857835126" + } + ] + }, + "42d3bc70-701a-4be0-96b4-d6c2f0519b49": { + "path": "42d3bc70-701a-4be0-96b4-d6c2f0519b49", + "cellName": "Execute Database\nQuery", + "cellId": "42d3bc70-701a-4be0-96b4-d6c2f0519b49", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-65da1a9e-041c-4656-9c5b-0a92b761a9be-42d3bc70-701a-4be0-96b4-d6c2f0519b49": { + "path": "generated-edge-simstep-65da1a9e-041c-4656-9c5b-0a92b761a9be-42d3bc70-701a-4be0-96b4-d6c2f0519b49", + "fileName": "checkService.js", + "cellName": "Execute Database Query", + "cellId": "42d3bc70-701a-4be0-96b4-d6c2f0519b49", + "visible": true, + "startLine": 28, + "endLine": 108, + "parentPath": "server/src/service/v1/business/checkService.js", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "65da1a9e-041c-4656-9c5b-0a92b761a9be" + } + ] + }, + "6c5b028c-356c-43c8-a35a-b47448cd8695": { + "path": "6c5b028c-356c-43c8-a35a-b47448cd8695", + "cellName": "API Response\nwith Check\nHistory", + "cellId": "6c5b028c-356c-43c8-a35a-b47448cd8695", + "visible": true + }, + "generated-edge-simstep-2c1bdec8-d0e6-4163-a99c-f27899b987c5-6c5b028c-356c-43c8-a35a-b47448cd8695": { + "path": "generated-edge-simstep-2c1bdec8-d0e6-4163-a99c-f27899b987c5-6c5b028c-356c-43c8-a35a-b47448cd8695", + "fileName": "checkController.js", + "cellName": "API Response with Check History", + "cellId": "6c5b028c-356c-43c8-a35a-b47448cd8695", + "visible": true, + "startLine": 560, + "endLine": 560, + "parentPath": "server/src/controllers/v1/checkController.js", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "2c1bdec8-d0e6-4163-a99c-f27899b987c5" + } + ] + }, + "74b0dd89-9fea-4219-af1f-9571316a9d5a": { + "path": "74b0dd89-9fea-4219-af1f-9571316a9d5a", + "cellName": "Pass Data\nto UI\nComponents", + "cellId": "74b0dd89-9fea-4219-af1f-9571316a9d5a", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "generated-edge-simstep-9405dce1-0565-4dfa-9539-a048472acda9-74b0dd89-9fea-4219-af1f-9571316a9d5a": { + "path": "generated-edge-simstep-9405dce1-0565-4dfa-9539-a048472acda9-74b0dd89-9fea-4219-af1f-9571316a9d5a", + "fileName": "checkHooks.js", + "cellName": "Pass Data to UI Components", + "cellId": "74b0dd89-9fea-4219-af1f-9571316a9d5a", + "visible": true, + "startLine": 175, + "endLine": 186, + "parentPath": "client/src/Hooks/v1/checkHooks.js", + "simSteps": [ + { + "simulationKey": "Incident History, Analysis, and Management", + "simStepId": "9405dce1-0565-4dfa-9539-a048472acda9" + } + ] + }, + "e2f99143-54b7-44dc-b566-1c2fb901bd16": { + "path": "e2f99143-54b7-44dc-b566-1c2fb901bd16", + "cellName": "v2", + "cellId": "e2f99143-54b7-44dc-b566-1c2fb901bd16", + "visible": true, + "parentCellId": "15ee5d81-a0cf-4614-80b8-7a1123ab20b8" + }, + "043d3dac-d4bb-449b-a3a7-270d53b66dac": { + "path": "043d3dac-d4bb-449b-a3a7-270d53b66dac", + "cellName": "PageSpeed", + "cellId": "043d3dac-d4bb-449b-a3a7-270d53b66dac", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679" + }, + "7ef79284-534a-466f-a1f1-4e3a2f56be9d": { + "path": "7ef79284-534a-466f-a1f1-4e3a2f56be9d", + "cellName": "business", + "cellId": "7ef79284-534a-466f-a1f1-4e3a2f56be9d", + "visible": true, + "parentCellId": "feefae96-61b8-4ac5-a37c-592ed4b2330e" + }, + "b53384d7-b989-4ac7-bd68-1c3621174ef4": { + "path": "b53384d7-b989-4ac7-bd68-1c3621174ef4", + "cellName": "models", + "cellId": "b53384d7-b989-4ac7-bd68-1c3621174ef4", + "visible": true, + "parentCellId": "e2f99143-54b7-44dc-b566-1c2fb901bd16" + }, + "414c500b-7b68-42ba-8110-e855efbee750": { + "path": "414c500b-7b68-42ba-8110-e855efbee750", + "cellName": "Create", + "cellId": "414c500b-7b68-42ba-8110-e855efbee750", + "visible": true, + "parentCellId": "043d3dac-d4bb-449b-a3a7-270d53b66dac" + }, + "581b4919-8b0c-4f0d-ae67-ad6de11d8c8b": { + "path": "581b4919-8b0c-4f0d-ae67-ad6de11d8c8b", + "cellName": "Details", + "cellId": "581b4919-8b0c-4f0d-ae67-ad6de11d8c8b", + "visible": true, + "parentCellId": "043d3dac-d4bb-449b-a3a7-270d53b66dac" + }, + "50aeed0b-bc13-43f7-bb15-cb1b12b2caa7": { + "path": "50aeed0b-bc13-43f7-bb15-cb1b12b2caa7", + "cellName": "NetworkService.ts", + "cellId": "50aeed0b-bc13-43f7-bb15-cb1b12b2caa7", + "visible": true, + "parentCellId": "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f" + }, + "f0ebd5da-21bb-4d96-8ff9-ca10409acc80": { + "path": "f0ebd5da-21bb-4d96-8ff9-ca10409acc80", + "cellName": "CheckService.ts", + "cellId": "f0ebd5da-21bb-4d96-8ff9-ca10409acc80", + "visible": true, + "parentCellId": "7ef79284-534a-466f-a1f1-4e3a2f56be9d" + }, + "7a3824cd-24c8-4b32-8b1d-e0bf357262c4": { + "path": "7a3824cd-24c8-4b32-8b1d-e0bf357262c4", + "cellName": "MonitorService.ts", + "cellId": "7a3824cd-24c8-4b32-8b1d-e0bf357262c4", + "visible": true, + "parentCellId": "7ef79284-534a-466f-a1f1-4e3a2f56be9d" + }, + "64e3bb0a-67f5-4269-9e7d-94fe91ac78fc": { + "path": "64e3bb0a-67f5-4269-9e7d-94fe91ac78fc", + "cellName": "checks", + "cellId": "64e3bb0a-67f5-4269-9e7d-94fe91ac78fc", + "visible": true, + "parentCellId": "b53384d7-b989-4ac7-bd68-1c3621174ef4" + }, + "5ffe0baa-054a-4e5b-8b29-1254f07ea975": { + "path": "5ffe0baa-054a-4e5b-8b29-1254f07ea975", + "cellName": "index.jsx", + "cellId": "5ffe0baa-054a-4e5b-8b29-1254f07ea975", + "visible": true, + "parentCellId": "414c500b-7b68-42ba-8110-e855efbee750" + }, + "c4a7bb58-1d9c-4690-b6b8-460c1cc33702": { + "path": "c4a7bb58-1d9c-4690-b6b8-460c1cc33702", + "cellName": "index.jsx", + "cellId": "c4a7bb58-1d9c-4690-b6b8-460c1cc33702", + "visible": true, + "parentCellId": "581b4919-8b0c-4f0d-ae67-ad6de11d8c8b" + }, + "1425ec43-f0f4-4c8b-a140-176c079219d6": { + "path": "1425ec43-f0f4-4c8b-a140-176c079219d6", + "cellName": "Check.ts", + "cellId": "1425ec43-f0f4-4c8b-a140-176c079219d6", + "visible": true, + "parentCellId": "64e3bb0a-67f5-4269-9e7d-94fe91ac78fc" + }, + "0622b212-3a4a-484d-9b7a-125864d785cd": { + "path": "0622b212-3a4a-484d-9b7a-125864d785cd", + "cellName": "Create PageSpeed Monitor - index.jsx:L111-143", + "cellId": "0622b212-3a4a-484d-9b7a-125864d785cd", + "visible": true, + "parentCellId": "5ffe0baa-054a-4e5b-8b29-1254f07ea975" + }, + "client/src/Pages/v1/PageSpeed/Create/index.jsx-simstep-b0cc6e6a-961e-4ea9-aa8a-6bfa44d6ba12": { + "path": "client/src/Pages/v1/PageSpeed/Create/index.jsx-simstep-b0cc6e6a-961e-4ea9-aa8a-6bfa44d6ba12", + "fileName": "index.jsx", + "wiki": "The user navigates to the PageSpeed creation page, fills in the details for a new monitor (URL, name, interval), and submits the form. This triggers the `handleSubmit` function, which calls the `createMonitor` hook to send the data to the backend API.", + "cellName": "Create PageSpeed Monitor - index.jsx:L111-143", + "cellId": "0622b212-3a4a-484d-9b7a-125864d785cd", + "visible": true, + "startLine": 111, + "endLine": 143, + "parentCellId": "5ffe0baa-054a-4e5b-8b29-1254f07ea975", + "parentPath": "client/src/Pages/v1/PageSpeed/Create/index.jsx", + "simSteps": [ + { + "simulationKey": "Website Performance and Page Speed Monitoring", + "simStepId": "b0cc6e6a-961e-4ea9-aa8a-6bfa44d6ba12" + } + ] + }, + "6527fb47-157e-4bba-9aed-adde061c5689": { + "path": "6527fb47-157e-4bba-9aed-adde061c5689", + "cellName": "Backend Requests PageSpeed Analysis - NetworkService.ts:L129-162", + "cellId": "6527fb47-157e-4bba-9aed-adde061c5689", + "visible": true, + "parentCellId": "50aeed0b-bc13-43f7-bb15-cb1b12b2caa7" + }, + "server/src/service/v2/infrastructure/NetworkService.ts-simstep-53968267-0c6e-42f4-88b3-cd90f44532ae": { + "path": "server/src/service/v2/infrastructure/NetworkService.ts-simstep-53968267-0c6e-42f4-88b3-cd90f44532ae", + "fileName": "NetworkService.ts", + "wiki": "A background job for the newly created monitor triggers the `NetworkService` to request a performance analysis from the Google PageSpeed Insights API. It constructs the API URL using the monitor's target URL and the configured PageSpeed API key.", + "cellName": "Backend Requests PageSpeed Analysis - NetworkService.ts:L129-162", + "cellId": "6527fb47-157e-4bba-9aed-adde061c5689", + "visible": true, + "startLine": 129, + "endLine": 162, + "parentCellId": "50aeed0b-bc13-43f7-bb15-cb1b12b2caa7", + "parentPath": "server/src/service/v2/infrastructure/NetworkService.ts", + "simSteps": [ + { + "simulationKey": "Website Performance and Page Speed Monitoring", + "simStepId": "53968267-0c6e-42f4-88b3-cd90f44532ae" + } + ] + }, + "f89cbd87-c961-4f8b-92b6-714da57718d5": { + "path": "f89cbd87-c961-4f8b-92b6-714da57718d5", + "cellName": "Backend Processes Lighthouse Data - CheckService.ts:L82-102", + "cellId": "f89cbd87-c961-4f8b-92b6-714da57718d5", + "visible": true, + "parentCellId": "f0ebd5da-21bb-4d96-8ff9-ca10409acc80" + }, + "server/src/service/v2/business/CheckService.ts-simstep-01206383-2db8-4b18-84d0-be2a0289c196": { + "path": "server/src/service/v2/business/CheckService.ts-simstep-01206383-2db8-4b18-84d0-be2a0289c196", + "fileName": "CheckService.ts", + "wiki": "The `CheckService` receives the raw data from the Google API. The `buildPagespeedCheck` function extracts and transforms the relevant scores and audit details into a structured `Check` object, preparing it for database storage.", + "cellName": "Backend Processes Lighthouse Data - CheckService.ts:L82-102", + "cellId": "f89cbd87-c961-4f8b-92b6-714da57718d5", + "visible": true, + "startLine": 82, + "endLine": 102, + "parentCellId": "f0ebd5da-21bb-4d96-8ff9-ca10409acc80", + "parentPath": "server/src/service/v2/business/CheckService.ts", + "simSteps": [ + { + "simulationKey": "Website Performance and Page Speed Monitoring", + "simStepId": "01206383-2db8-4b18-84d0-be2a0289c196" + } + ] + }, + "ca6ee27b-1fbf-49de-978a-d6de259cc3d5": { + "path": "ca6ee27b-1fbf-49de-978a-d6de259cc3d5", + "cellName": "Backend Aggregates Performance Data - MonitorService.ts:L106-120", + "cellId": "ca6ee27b-1fbf-49de-978a-d6de259cc3d5", + "visible": true, + "parentCellId": "7a3824cd-24c8-4b32-8b1d-e0bf357262c4" + }, + "server/src/service/v2/business/MonitorService.ts-simstep-5616ac6d-c281-4879-82f1-6590c721c8f7": { + "path": "server/src/service/v2/business/MonitorService.ts-simstep-5616ac6d-c281-4879-82f1-6590c721c8f7", + "fileName": "MonitorService.ts", + "wiki": "When a user views the details of a PageSpeed monitor, the frontend requests historical data. The `MonitorService` on the backend uses a MongoDB aggregation pipeline to group checks by time intervals and calculate average scores for metrics like performance, SEO, etc. This is defined by helper functions like `getPageSpeedGroup`.", + "cellName": "Backend Aggregates Performance Data - MonitorService.ts:L106-120", + "cellId": "ca6ee27b-1fbf-49de-978a-d6de259cc3d5", + "visible": true, + "startLine": 106, + "endLine": 120, + "parentCellId": "7a3824cd-24c8-4b32-8b1d-e0bf357262c4", + "parentPath": "server/src/service/v2/business/MonitorService.ts", + "simSteps": [ + { + "simulationKey": "Website Performance and Page Speed Monitoring", + "simStepId": "5616ac6d-c281-4879-82f1-6590c721c8f7" + } + ] + }, + "9ef9e218-4742-487a-9a88-5c70e788ccdc": { + "path": "9ef9e218-4742-487a-9a88-5c70e788ccdc", + "cellName": "Frontend Renders Performance Charts - index.jsx:L91-126", + "cellId": "9ef9e218-4742-487a-9a88-5c70e788ccdc", + "visible": true, + "parentCellId": "c4a7bb58-1d9c-4690-b6b8-460c1cc33702" + }, + "client/src/Pages/v1/PageSpeed/Details/index.jsx-simstep-0ec0eb06-05eb-4204-b077-9e47eb2cc316": { + "path": "client/src/Pages/v1/PageSpeed/Details/index.jsx-simstep-0ec0eb06-05eb-4204-b077-9e47eb2cc316", + "fileName": "index.jsx", + "wiki": "The `PageSpeedDetails` component receives the historical data from the backend. It then passes this data to specialized child components like `PageSpeedAreaChart` and `PerformanceReport` to render visualizations of the website's performance over time.", + "cellName": "Frontend Renders Performance Charts - index.jsx:L91-126", + "cellId": "9ef9e218-4742-487a-9a88-5c70e788ccdc", + "visible": true, + "startLine": 91, + "endLine": 126, + "parentCellId": "c4a7bb58-1d9c-4690-b6b8-460c1cc33702", + "parentPath": "client/src/Pages/v1/PageSpeed/Details/index.jsx", + "simSteps": [ + { + "simulationKey": "Website Performance and Page Speed Monitoring", + "simStepId": "0ec0eb06-05eb-4204-b077-9e47eb2cc316" + } + ] + }, + "b8e83b0a-69ff-4c34-a7fe-cb8c9ba85438": { + "path": "b8e83b0a-69ff-4c34-a7fe-cb8c9ba85438", + "cellName": "API Call:\nCreate Monitor", + "cellId": "b8e83b0a-69ff-4c34-a7fe-cb8c9ba85438", + "visible": true + }, + "generated-edge-simstep-f8df1cad-6051-466e-a13e-47d710ba1c14-b8e83b0a-69ff-4c34-a7fe-cb8c9ba85438": { + "path": "generated-edge-simstep-f8df1cad-6051-466e-a13e-47d710ba1c14-b8e83b0a-69ff-4c34-a7fe-cb8c9ba85438", + "fileName": "index.jsx", + "cellName": "API Call: Create Monitor", + "cellId": "b8e83b0a-69ff-4c34-a7fe-cb8c9ba85438", + "visible": true, + "startLine": 119, + "endLine": 119, + "parentPath": "client/src/Pages/v1/PageSpeed/Create/index.jsx", + "simSteps": [ + { + "simulationKey": "Website Performance and Page Speed Monitoring", + "simStepId": "f8df1cad-6051-466e-a13e-47d710ba1c14" + } + ] + }, + "0d3c8091-3d91-4e42-9b46-7e35ed726a7a": { + "path": "0d3c8091-3d91-4e42-9b46-7e35ed726a7a", + "cellName": "Google PageSpeed\nAPI Response", + "cellId": "0d3c8091-3d91-4e42-9b46-7e35ed726a7a", + "visible": true, + "parentCellId": "feefae96-61b8-4ac5-a37c-592ed4b2330e" + }, + "generated-edge-simstep-0b380571-3896-4365-b5dc-187494f02e0d-0d3c8091-3d91-4e42-9b46-7e35ed726a7a": { + "path": "generated-edge-simstep-0b380571-3896-4365-b5dc-187494f02e0d-0d3c8091-3d91-4e42-9b46-7e35ed726a7a", + "fileName": "NetworkService.ts", + "cellName": "Google PageSpeed API Response", + "cellId": "0d3c8091-3d91-4e42-9b46-7e35ed726a7a", + "visible": true, + "startLine": 156, + "endLine": 156, + "parentPath": "server/src/service/v2/infrastructure/NetworkService.ts", + "simSteps": [ + { + "simulationKey": "Website Performance and Page Speed Monitoring", + "simStepId": "0b380571-3896-4365-b5dc-187494f02e0d" + } + ] + }, + "27c7191b-80a2-475b-9371-e106108aff1c": { + "path": "27c7191b-80a2-475b-9371-e106108aff1c", + "cellName": "Store Processed\nCheck Data", + "cellId": "27c7191b-80a2-475b-9371-e106108aff1c", + "visible": true, + "parentCellId": "7ef79284-534a-466f-a1f1-4e3a2f56be9d" + }, + "generated-edge-simstep-a629a102-0df1-4798-a7a2-45c8f6cdad29-27c7191b-80a2-475b-9371-e106108aff1c": { + "path": "generated-edge-simstep-a629a102-0df1-4798-a7a2-45c8f6cdad29-27c7191b-80a2-475b-9371-e106108aff1c", + "fileName": "CheckService.ts", + "cellName": "Store Processed Check Data", + "cellId": "27c7191b-80a2-475b-9371-e106108aff1c", + "visible": true, + "startLine": 237, + "endLine": 268, + "parentPath": "server/src/service/v2/business/CheckService.ts", + "simSteps": [ + { + "simulationKey": "Website Performance and Page Speed Monitoring", + "simStepId": "a629a102-0df1-4798-a7a2-45c8f6cdad29" + } + ] + }, + "7be432cd-2587-4eab-abdc-f7b9c1923597": { + "path": "7be432cd-2587-4eab-abdc-f7b9c1923597", + "cellName": "Aggregated Data\nTransmitted to\nFrontend", + "cellId": "7be432cd-2587-4eab-abdc-f7b9c1923597", + "visible": true + }, + "generated-edge-simstep-4ed43eaf-fa8f-4fce-af87-8a5f74dd5c08-7be432cd-2587-4eab-abdc-f7b9c1923597": { + "path": "generated-edge-simstep-4ed43eaf-fa8f-4fce-af87-8a5f74dd5c08-7be432cd-2587-4eab-abdc-f7b9c1923597", + "fileName": "MonitorService.ts", + "cellName": "Aggregated Data Transmitted to Frontend", + "cellId": "7be432cd-2587-4eab-abdc-f7b9c1923597", + "visible": true, + "startLine": 368, + "endLine": 372, + "parentPath": "server/src/service/v2/business/MonitorService.ts", + "simSteps": [ + { + "simulationKey": "Website Performance and Page Speed Monitoring", + "simStepId": "4ed43eaf-fa8f-4fce-af87-8a5f74dd5c08" + } + ] + }, + "fe47576c-c186-4950-82a5-b2fd24eb2561": { + "path": "fe47576c-c186-4950-82a5-b2fd24eb2561", + "cellName": "config", + "cellId": "fe47576c-c186-4950-82a5-b2fd24eb2561", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "cb53d612-9b89-47da-911c-165d0382a7d8": { + "path": "cb53d612-9b89-47da-911c-165d0382a7d8", + "cellName": "routes.js", + "cellId": "cb53d612-9b89-47da-911c-165d0382a7d8", + "visible": true, + "parentCellId": "fe47576c-c186-4950-82a5-b2fd24eb2561" + }, + "8021b55d-2d3a-4416-b598-e134e76c9434": { + "path": "8021b55d-2d3a-4416-b598-e134e76c9434", + "cellName": "Maintenance", + "cellId": "8021b55d-2d3a-4416-b598-e134e76c9434", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679" + }, + "5f21f109-1967-416f-a649-9b48e78b90f6": { + "path": "5f21f109-1967-416f-a649-9b48e78b90f6", + "cellName": "maintenanceWindowRoute.js", + "cellId": "5f21f109-1967-416f-a649-9b48e78b90f6", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd" + }, + "a0cae18c-3c0e-40d3-ab03-eef7eace65d6": { + "path": "a0cae18c-3c0e-40d3-ab03-eef7eace65d6", + "cellName": "maintenanceWindowController.js", + "cellId": "a0cae18c-3c0e-40d3-ab03-eef7eace65d6", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "6f7119ac-d65a-4fdf-a6f2-31052e0f5549": { + "path": "6f7119ac-d65a-4fdf-a6f2-31052e0f5549", + "cellName": "CreateMaintenance", + "cellId": "6f7119ac-d65a-4fdf-a6f2-31052e0f5549", + "visible": true, + "parentCellId": "8021b55d-2d3a-4416-b598-e134e76c9434" + }, + "1eae6263-aaa2-4206-b92e-1e67f529c1c9": { + "path": "1eae6263-aaa2-4206-b92e-1e67f529c1c9", + "cellName": "index.jsx", + "cellId": "1eae6263-aaa2-4206-b92e-1e67f529c1c9", + "visible": true, + "parentCellId": "8021b55d-2d3a-4416-b598-e134e76c9434" + }, + "5ad4d33f-0932-479c-bb57-22ce6405758f": { + "path": "5ad4d33f-0932-479c-bb57-22ce6405758f", + "cellName": "MaintenanceTable", + "cellId": "5ad4d33f-0932-479c-bb57-22ce6405758f", + "visible": true, + "parentCellId": "8021b55d-2d3a-4416-b598-e134e76c9434" + }, + "26b8daff-aa48-4baa-9d6c-05f771bf0990": { + "path": "26b8daff-aa48-4baa-9d6c-05f771bf0990", + "cellName": "maintenanceWindowService.js", + "cellId": "26b8daff-aa48-4baa-9d6c-05f771bf0990", + "visible": true, + "parentCellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb" + }, + "95ed6c41-ebeb-4c08-98c3-bab8b426789e": { + "path": "95ed6c41-ebeb-4c08-98c3-bab8b426789e", + "cellName": "maintenanceWindowModule.js", + "cellId": "95ed6c41-ebeb-4c08-98c3-bab8b426789e", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380" + }, + "92c244d2-e6a7-4804-a643-1e8d36baf9c6": { + "path": "92c244d2-e6a7-4804-a643-1e8d36baf9c6", + "cellName": "index.jsx", + "cellId": "92c244d2-e6a7-4804-a643-1e8d36baf9c6", + "visible": true, + "parentCellId": "6f7119ac-d65a-4fdf-a6f2-31052e0f5549" + }, + "015a17fc-3fcf-45e0-8936-19f5e2ca86a3": { + "path": "015a17fc-3fcf-45e0-8936-19f5e2ca86a3", + "cellName": "hooks", + "cellId": "015a17fc-3fcf-45e0-8936-19f5e2ca86a3", + "visible": true, + "parentCellId": "6f7119ac-d65a-4fdf-a6f2-31052e0f5549" + }, + "e7ec5ca9-6d25-4bf2-9695-639556f7d141": { + "path": "e7ec5ca9-6d25-4bf2-9695-639556f7d141", + "cellName": "index.jsx", + "cellId": "e7ec5ca9-6d25-4bf2-9695-639556f7d141", + "visible": true, + "parentCellId": "5ad4d33f-0932-479c-bb57-22ce6405758f" + }, + "6ad0396e-9b38-4f5d-9ad9-6c9fcb4434f4": { + "path": "6ad0396e-9b38-4f5d-9ad9-6c9fcb4434f4", + "cellName": "useMaintenanceActions.jsx", + "cellId": "6ad0396e-9b38-4f5d-9ad9-6c9fcb4434f4", + "visible": true, + "parentCellId": "015a17fc-3fcf-45e0-8936-19f5e2ca86a3" + }, + "20335f29-a2ac-4e0c-aba6-28707748ecd4": { + "path": "20335f29-a2ac-4e0c-aba6-28707748ecd4", + "cellName": "Flow: Create Maintenance Window - User Interaction - index.jsx:L46-131", + "cellId": "20335f29-a2ac-4e0c-aba6-28707748ecd4", + "visible": true, + "parentCellId": "92c244d2-e6a7-4804-a643-1e8d36baf9c6" + }, + "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx-simstep-2401ef7a-8d8e-4056-868a-547ab213daf1": { + "path": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx-simstep-2401ef7a-8d8e-4056-868a-547ab213daf1", + "fileName": "index.jsx", + "wiki": "A user navigates to the 'Create Maintenance Window' page. They fill out a form with details such as a name, start date/time, duration, recurrence, and select the monitors to which this window will apply.", + "cellName": "Flow: Create Maintenance Window - User Interaction - index.jsx:L46-131", + "cellId": "20335f29-a2ac-4e0c-aba6-28707748ecd4", + "visible": true, + "startLine": 46, + "endLine": 131, + "parentCellId": "92c244d2-e6a7-4804-a643-1e8d36baf9c6", + "parentPath": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "2401ef7a-8d8e-4056-868a-547ab213daf1" + } + ] + }, + "5bdfdfa4-5d93-49fa-b05c-905813e6cf9d": { + "path": "5bdfdfa4-5d93-49fa-b05c-905813e6cf9d", + "cellName": "Flow: Create Maintenance Window - Prepare API Request - useMaintenanceActions.jsx:L25-58", + "cellId": "5bdfdfa4-5d93-49fa-b05c-905813e6cf9d", + "visible": true, + "parentCellId": "6ad0396e-9b38-4f5d-9ad9-6c9fcb4434f4" + }, + "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx-simstep-5e0d7085-099b-478a-afaa-47ad4dd07043": { + "path": "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx-simstep-5e0d7085-099b-478a-afaa-47ad4dd07043", + "fileName": "useMaintenanceActions.jsx", + "wiki": "The `handleSubmitForm` hook processes the form data, calculates start and end timestamps, and constructs a request object. It then determines whether to call `createMaintenanceWindow` or `editMaintenanceWindow` on the network service.", + "cellName": "Flow: Create Maintenance Window - Prepare API Request - useMaintenanceActions.jsx:L25-58", + "cellId": "5bdfdfa4-5d93-49fa-b05c-905813e6cf9d", + "visible": true, + "startLine": 25, + "endLine": 58, + "parentCellId": "6ad0396e-9b38-4f5d-9ad9-6c9fcb4434f4", + "parentPath": "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "5e0d7085-099b-478a-afaa-47ad4dd07043" + } + ] + }, + "a391e8ea-9dbe-4fe4-90df-bf475d8b7ef8": { + "path": "a391e8ea-9dbe-4fe4-90df-bf475d8b7ef8", + "cellName": "Flow: Create Maintenance Window - API Routing - routes.js:L43", + "cellId": "a391e8ea-9dbe-4fe4-90df-bf475d8b7ef8", + "visible": true, + "parentCellId": "cb53d612-9b89-47da-911c-165d0382a7d8" + }, + "server/src/config/routes.js-simstep-3e93ef65-8e2c-405b-8dc1-56d8e93937b3": { + "path": "server/src/config/routes.js-simstep-3e93ef65-8e2c-405b-8dc1-56d8e93937b3", + "fileName": "routes.js", + "wiki": "The backend Express server receives the request. The routing configuration maps the `/api/v1/maintenance-window` path to the `maintenanceWindowRoutes`, which in turn directs the POST request to the `createMaintenanceWindows` method in the controller.", + "cellName": "Flow: Create Maintenance Window - API Routing - routes.js:L43", + "cellId": "a391e8ea-9dbe-4fe4-90df-bf475d8b7ef8", + "visible": true, + "startLine": 43, + "endLine": 43, + "parentCellId": "cb53d612-9b89-47da-911c-165d0382a7d8", + "parentPath": "server/src/config/routes.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "3e93ef65-8e2c-405b-8dc1-56d8e93937b3" + } + ] + }, + "0fae6e75-38ea-444d-a1bf-0f820ba443b9": { + "path": "0fae6e75-38ea-444d-a1bf-0f820ba443b9", + "cellName": "Flow: Create Maintenance Window - Controller Logic - maintenanceWindowController.js:L27-41", + "cellId": "0fae6e75-38ea-444d-a1bf-0f820ba443b9", + "visible": true, + "parentCellId": "a0cae18c-3c0e-40d3-ab03-eef7eace65d6" + }, + "server/src/controllers/v1/maintenanceWindowController.js-simstep-95fc1919-339d-4659-98cb-3dc50f662075": { + "path": "server/src/controllers/v1/maintenanceWindowController.js-simstep-95fc1919-339d-4659-98cb-3dc50f662075", + "fileName": "maintenanceWindowController.js", + "wiki": "The `maintenanceWindowController` validates the request body using Joi, ensures a `teamId` exists on the user object, and then calls the `maintenanceWindowService` to handle the business logic of creating the window.", + "cellName": "Flow: Create Maintenance Window - Controller Logic - maintenanceWindowController.js:L27-41", + "cellId": "0fae6e75-38ea-444d-a1bf-0f820ba443b9", + "visible": true, + "startLine": 27, + "endLine": 41, + "parentCellId": "a0cae18c-3c0e-40d3-ab03-eef7eace65d6", + "parentPath": "server/src/controllers/v1/maintenanceWindowController.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "95fc1919-339d-4659-98cb-3dc50f662075" + } + ] + }, + "560d268b-ef0f-4cd2-bcfe-9dc0a0e2ce6f": { + "path": "560d268b-ef0f-4cd2-bcfe-9dc0a0e2ce6f", + "cellName": "Flow: Create Maintenance Window - Business Logic Execution - maintenanceWindowService.js:L18-39", + "cellId": "560d268b-ef0f-4cd2-bcfe-9dc0a0e2ce6f", + "visible": true, + "parentCellId": "26b8daff-aa48-4baa-9d6c-05f771bf0990" + }, + "server/src/service/v1/business/maintenanceWindowService.js-simstep-6e2cf739-1514-4c37-9874-10b68a47010e": { + "path": "server/src/service/v1/business/maintenanceWindowService.js-simstep-6e2cf739-1514-4c37-9874-10b68a47010e", + "fileName": "maintenanceWindowService.js", + "wiki": "The `maintenanceWindowService` verifies that all monitors specified in the request belong to the user's team. It then iterates through each monitor ID and prepares a separate database transaction to create a maintenance window record for each one.", + "cellName": "Flow: Create Maintenance Window - Business Logic Execution - maintenanceWindowService.js:L18-39", + "cellId": "560d268b-ef0f-4cd2-bcfe-9dc0a0e2ce6f", + "visible": true, + "startLine": 18, + "endLine": 39, + "parentCellId": "26b8daff-aa48-4baa-9d6c-05f771bf0990", + "parentPath": "server/src/service/v1/business/maintenanceWindowService.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "6e2cf739-1514-4c37-9874-10b68a47010e" + } + ] + }, + "3890b98d-a133-4279-a961-5d2facc6a17a": { + "path": "3890b98d-a133-4279-a961-5d2facc6a17a", + "cellName": "Flow: Create Maintenance Window - Database Record Creation - maintenanceWindowModule.js:L7-22", + "cellId": "3890b98d-a133-4279-a961-5d2facc6a17a", + "visible": true, + "parentCellId": "95ed6c41-ebeb-4c08-98c3-bab8b426789e" + }, + "server/src/db/v1/modules/maintenanceWindowModule.js-simstep-a3c47fd4-c701-46c2-9aff-94a667b3e368": { + "path": "server/src/db/v1/modules/maintenanceWindowModule.js-simstep-a3c47fd4-c701-46c2-9aff-94a667b3e368", + "fileName": "maintenanceWindowModule.js", + "wiki": "The `maintenanceWindowModule` instantiates a new `MaintenanceWindow` Mongoose model. If the window is a one-time event, it sets an `expiry` field to the end time for automatic cleanup by MongoDB's TTL index. Finally, it saves the new document to the database.", + "cellName": "Flow: Create Maintenance Window - Database Record Creation - maintenanceWindowModule.js:L7-22", + "cellId": "3890b98d-a133-4279-a961-5d2facc6a17a", + "visible": true, + "startLine": 7, + "endLine": 22, + "parentCellId": "95ed6c41-ebeb-4c08-98c3-bab8b426789e", + "parentPath": "server/src/db/v1/modules/maintenanceWindowModule.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "a3c47fd4-c701-46c2-9aff-94a667b3e368" + } + ] + }, + "a5a84b08-8c70-4023-8d08-b74e23aab4d7": { + "path": "a5a84b08-8c70-4023-8d08-b74e23aab4d7", + "cellName": "Flow: Create Maintenance Window - UI Update - index.jsx:L111-119", + "cellId": "a5a84b08-8c70-4023-8d08-b74e23aab4d7", + "visible": true, + "parentCellId": "92c244d2-e6a7-4804-a643-1e8d36baf9c6" + }, + "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx-simstep-ebe8fd52-f2a1-4a98-a71b-b04d6f4cdadc": { + "path": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx-simstep-ebe8fd52-f2a1-4a98-a71b-b04d6f4cdadc", + "fileName": "index.jsx", + "wiki": "The frontend receives the success response. A success toast message is displayed, and the user is navigated back to the main maintenance windows table.", + "cellName": "Flow: Create Maintenance Window - UI Update - index.jsx:L111-119", + "cellId": "a5a84b08-8c70-4023-8d08-b74e23aab4d7", + "visible": true, + "startLine": 111, + "endLine": 119, + "parentCellId": "92c244d2-e6a7-4804-a643-1e8d36baf9c6", + "parentPath": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "ebe8fd52-f2a1-4a98-a71b-b04d6f4cdadc" + } + ] + }, + "d145bb4c-e1e0-473a-b6bc-decc932b738b": { + "path": "d145bb4c-e1e0-473a-b6bc-decc932b738b", + "cellName": "Flow: View Maintenance Windows - Page Load - index.jsx:L25-47", + "cellId": "d145bb4c-e1e0-473a-b6bc-decc932b738b", + "visible": true, + "parentCellId": "1eae6263-aaa2-4206-b92e-1e67f529c1c9" + }, + "client/src/Pages/v1/Maintenance/index.jsx-simstep-82b5ea14-bb00-4511-aba4-76196029fd81": { + "path": "client/src/Pages/v1/Maintenance/index.jsx-simstep-82b5ea14-bb00-4511-aba4-76196029fd81", + "fileName": "index.jsx", + "wiki": "User navigates to the '/maintenance' page. The `Maintenance` component mounts and triggers a `useEffect` hook to fetch the list of maintenance windows.", + "cellName": "Flow: View Maintenance Windows - Page Load - index.jsx:L25-47", + "cellId": "d145bb4c-e1e0-473a-b6bc-decc932b738b", + "visible": true, + "startLine": 25, + "endLine": 47, + "parentCellId": "1eae6263-aaa2-4206-b92e-1e67f529c1c9", + "parentPath": "client/src/Pages/v1/Maintenance/index.jsx", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "82b5ea14-bb00-4511-aba4-76196029fd81" + } + ] + }, + "c35a065c-2808-402c-877e-9055d956ce0d": { + "path": "c35a065c-2808-402c-877e-9055d956ce0d", + "cellName": "Flow: View Maintenance Windows - Controller and Service - maintenanceWindowController.js:L75", + "cellId": "c35a065c-2808-402c-877e-9055d956ce0d", + "visible": true, + "parentCellId": "a0cae18c-3c0e-40d3-ab03-eef7eace65d6" + }, + "server/src/controllers/v1/maintenanceWindowController.js-simstep-1251da83-5949-4e24-a1d2-fc74ff785aa9": { + "path": "server/src/controllers/v1/maintenanceWindowController.js-simstep-1251da83-5949-4e24-a1d2-fc74ff785aa9", + "fileName": "maintenanceWindowController.js", + "wiki": "The backend routes the request to `maintenanceWindowController.getMaintenanceWindowsByTeamId`. This controller calls the corresponding service, which in turn calls the database module to query for the data.", + "cellName": "Flow: View Maintenance Windows - Controller and Service - maintenanceWindowController.js:L75", + "cellId": "c35a065c-2808-402c-877e-9055d956ce0d", + "visible": true, + "startLine": 75, + "endLine": 75, + "parentCellId": "a0cae18c-3c0e-40d3-ab03-eef7eace65d6", + "parentPath": "server/src/controllers/v1/maintenanceWindowController.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "1251da83-5949-4e24-a1d2-fc74ff785aa9" + } + ] + }, + "27d9160a-4f8d-4dff-a09b-1678bc28cf2c": { + "path": "27d9160a-4f8d-4dff-a09b-1678bc28cf2c", + "cellName": "Flow: View Maintenance Windows - Data Retrieval - maintenanceWindowModule.js:L37-66", + "cellId": "27d9160a-4f8d-4dff-a09b-1678bc28cf2c", + "visible": true, + "parentCellId": "95ed6c41-ebeb-4c08-98c3-bab8b426789e" + }, + "server/src/db/v1/modules/maintenanceWindowModule.js-simstep-9bfbae5a-f487-40ef-86f5-04a1cc3c7662": { + "path": "server/src/db/v1/modules/maintenanceWindowModule.js-simstep-9bfbae5a-f487-40ef-86f5-04a1cc3c7662", + "fileName": "maintenanceWindowModule.js", + "wiki": "The `maintenanceWindowModule` constructs a MongoDB query to find all maintenance windows for the given `teamId`, applying pagination (skip, limit) and sorting. It executes the query and returns the results along with the total count.", + "cellName": "Flow: View Maintenance Windows - Data Retrieval - maintenanceWindowModule.js:L37-66", + "cellId": "27d9160a-4f8d-4dff-a09b-1678bc28cf2c", + "visible": true, + "startLine": 37, + "endLine": 66, + "parentCellId": "95ed6c41-ebeb-4c08-98c3-bab8b426789e", + "parentPath": "server/src/db/v1/modules/maintenanceWindowModule.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "9bfbae5a-f487-40ef-86f5-04a1cc3c7662" + } + ] + }, + "970e2002-7c0e-40ef-80d8-e5e6d0a60735": { + "path": "970e2002-7c0e-40ef-80d8-e5e6d0a60735", + "cellName": "Flow: View Maintenance Windows - UI Rendering - index.jsx:L195-201", + "cellId": "970e2002-7c0e-40ef-80d8-e5e6d0a60735", + "visible": true, + "parentCellId": "e7ec5ca9-6d25-4bf2-9695-639556f7d141" + }, + "client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx-simstep-acf97639-e291-4ca6-81a6-d0487397f42a": { + "path": "client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx-simstep-acf97639-e291-4ca6-81a6-d0487397f42a", + "fileName": "index.jsx", + "wiki": "The client receives the data and updates its state. The `MaintenanceTable` component then renders the list of maintenance windows in a table, displaying their names, schedules, and providing actions like edit or delete.", + "cellName": "Flow: View Maintenance Windows - UI Rendering - index.jsx:L195-201", + "cellId": "970e2002-7c0e-40ef-80d8-e5e6d0a60735", + "visible": true, + "startLine": 195, + "endLine": 201, + "parentCellId": "e7ec5ca9-6d25-4bf2-9695-639556f7d141", + "parentPath": "client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "acf97639-e291-4ca6-81a6-d0487397f42a" + } + ] + }, + "23630a1f-7452-4373-a864-7ec7821137f2": { + "path": "23630a1f-7452-4373-a864-7ec7821137f2", + "cellName": "Flow: Monitor Suppression - Job Execution Start - SuperSimpleQueueHelper.js:L30-45", + "cellId": "23630a1f-7452-4373-a864-7ec7821137f2", + "visible": true, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985" + }, + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-a47affaf-b952-46d6-9709-6120a77690e1": { + "path": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-a47affaf-b952-46d6-9709-6120a77690e1", + "fileName": "SuperSimpleQueueHelper.js", + "wiki": "The `SuperSimpleQueue` processing system picks up a job to check a specific monitor. The job handler is retrieved from `SuperSimpleQueueHelper`.", + "cellName": "Flow: Monitor Suppression - Job Execution Start - SuperSimpleQueueHelper.js:L30-45", + "cellId": "23630a1f-7452-4373-a864-7ec7821137f2", + "visible": true, + "startLine": 30, + "endLine": 45, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985", + "parentPath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "a47affaf-b952-46d6-9709-6120a77690e1" + } + ] + }, + "fc78b5b6-7fad-4f8f-a5a3-c7c38931daba": { + "path": "fc78b5b6-7fad-4f8f-a5a3-c7c38931daba", + "cellName": "Flow: Monitor Suppression - Fetching Maintenance Windows - SuperSimpleQueueHelper.js:L80-83", + "cellId": "fc78b5b6-7fad-4f8f-a5a3-c7c38931daba", + "visible": true, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985" + }, + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-2127e749-80b4-499a-ae14-8dc2dcc9db9b": { + "path": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-2127e749-80b4-499a-ae14-8dc2dcc9db9b", + "fileName": "SuperSimpleQueueHelper.js", + "wiki": "The `isInMaintenanceWindow` function calls the database module to fetch all maintenance windows associated with the given monitor ID and team ID.", + "cellName": "Flow: Monitor Suppression - Fetching Maintenance Windows - SuperSimpleQueueHelper.js:L80-83", + "cellId": "fc78b5b6-7fad-4f8f-a5a3-c7c38931daba", + "visible": true, + "startLine": 80, + "endLine": 83, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985", + "parentPath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "2127e749-80b4-499a-ae14-8dc2dcc9db9b" + } + ] + }, + "0fe32416-db43-49b9-9438-3796ba1a0add": { + "path": "0fe32416-db43-49b9-9438-3796ba1a0add", + "cellName": "Flow: Monitor Suppression - Time Evaluation - SuperSimpleQueueHelper.js:L85-113", + "cellId": "0fe32416-db43-49b9-9438-3796ba1a0add", + "visible": true, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985" + }, + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-6ba6b9d1-5fc3-454e-8744-2022b5192ed9": { + "path": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-6ba6b9d1-5fc3-454e-8744-2022b5192ed9", + "fileName": "SuperSimpleQueueHelper.js", + "wiki": "The `isInMaintenanceWindow` function iterates through the returned windows. For each active window, it checks if the current time (`new Date()`) is between the `start` and `end` times.", + "cellName": "Flow: Monitor Suppression - Time Evaluation - SuperSimpleQueueHelper.js:L85-113", + "cellId": "0fe32416-db43-49b9-9438-3796ba1a0add", + "visible": true, + "startLine": 85, + "endLine": 113, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985", + "parentPath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "6ba6b9d1-5fc3-454e-8744-2022b5192ed9" + } + ] + }, + "01e042c3-40b4-416a-8d9e-6d6fd323d60b": { + "path": "01e042c3-40b4-416a-8d9e-6d6fd323d60b", + "cellName": "Flow: Monitor Suppression - Conditional Job Termination - SuperSimpleQueueHelper.js:L37-44", + "cellId": "01e042c3-40b4-416a-8d9e-6d6fd323d60b", + "visible": true, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985" + }, + "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-e59fe64e-f06b-4855-b631-923bb5a68540": { + "path": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-e59fe64e-f06b-4855-b631-923bb5a68540", + "fileName": "SuperSimpleQueueHelper.js", + "wiki": "If the `isInMaintenanceWindow` function returns true, the `getMonitorJob` logs that the monitor is in a maintenance window and terminates its execution for this cycle, effectively suppressing the check.", + "cellName": "Flow: Monitor Suppression - Conditional Job Termination - SuperSimpleQueueHelper.js:L37-44", + "cellId": "01e042c3-40b4-416a-8d9e-6d6fd323d60b", + "visible": true, + "startLine": 37, + "endLine": 44, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985", + "parentPath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "e59fe64e-f06b-4855-b631-923bb5a68540" + } + ] + }, + "d3bdc6ff-875e-472e-88c8-eb10b20c526e": { + "path": "d3bdc6ff-875e-472e-88c8-eb10b20c526e", + "cellName": "Flow: Create\nMaintenance Window\n- Form\nSubmission", + "cellId": "d3bdc6ff-875e-472e-88c8-eb10b20c526e", + "visible": true, + "parentCellId": "6f7119ac-d65a-4fdf-a6f2-31052e0f5549" + }, + "generated-edge-simstep-f881435b-3bc2-453d-b6bf-56ad3a12f120-d3bdc6ff-875e-472e-88c8-eb10b20c526e": { + "path": "generated-edge-simstep-f881435b-3bc2-453d-b6bf-56ad3a12f120-d3bdc6ff-875e-472e-88c8-eb10b20c526e", + "fileName": "index.jsx", + "cellName": "Flow: Create Maintenance Window - Form Submission", + "cellId": "d3bdc6ff-875e-472e-88c8-eb10b20c526e", + "visible": true, + "startLine": 108, + "endLine": 109, + "parentPath": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "f881435b-3bc2-453d-b6bf-56ad3a12f120" + } + ] + }, + "fd5e4447-123b-4b2d-8bce-1d1ae7cb00af": { + "path": "fd5e4447-123b-4b2d-8bce-1d1ae7cb00af", + "cellName": "Flow: Create\nMaintenance Window\n- API\nCall", + "cellId": "fd5e4447-123b-4b2d-8bce-1d1ae7cb00af", + "visible": true + }, + "generated-edge-simstep-0660b48b-139f-4ecf-b7d9-f12f544e0ab5-fd5e4447-123b-4b2d-8bce-1d1ae7cb00af": { + "path": "generated-edge-simstep-0660b48b-139f-4ecf-b7d9-f12f544e0ab5-fd5e4447-123b-4b2d-8bce-1d1ae7cb00af", + "fileName": "useMaintenanceActions.jsx", + "cellName": "Flow: Create Maintenance Window - API Call", + "cellId": "fd5e4447-123b-4b2d-8bce-1d1ae7cb00af", + "visible": true, + "startLine": 762, + "endLine": 766, + "parentPath": "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "0660b48b-139f-4ecf-b7d9-f12f544e0ab5" + } + ] + }, + "e2588541-3ffc-47c6-8f8a-41d30ea7fc72": { + "path": "e2588541-3ffc-47c6-8f8a-41d30ea7fc72", + "cellName": "Flow: Create\nMaintenance Window\n- Controller\nInvocation", + "cellId": "e2588541-3ffc-47c6-8f8a-41d30ea7fc72", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-e4cc5ce8-6da9-4281-9204-29c061ab13ce-e2588541-3ffc-47c6-8f8a-41d30ea7fc72": { + "path": "generated-edge-simstep-e4cc5ce8-6da9-4281-9204-29c061ab13ce-e2588541-3ffc-47c6-8f8a-41d30ea7fc72", + "fileName": "routes.js", + "cellName": "Flow: Create Maintenance Window - Controller Invocation", + "cellId": "e2588541-3ffc-47c6-8f8a-41d30ea7fc72", + "visible": true, + "startLine": 10, + "endLine": 10, + "parentPath": "server/src/config/routes.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "e4cc5ce8-6da9-4281-9204-29c061ab13ce" + } + ] + }, + "e1d59cf6-0d96-4e0a-ad17-2a7984a9157f": { + "path": "e1d59cf6-0d96-4e0a-ad17-2a7984a9157f", + "cellName": "Flow: Create\nMaintenance Window\n- Service\nLayer Call", + "cellId": "e1d59cf6-0d96-4e0a-ad17-2a7984a9157f", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-c3d1a95c-e93d-499a-8830-a6d870902414-e1d59cf6-0d96-4e0a-ad17-2a7984a9157f": { + "path": "generated-edge-simstep-c3d1a95c-e93d-499a-8830-a6d870902414-e1d59cf6-0d96-4e0a-ad17-2a7984a9157f", + "fileName": "maintenanceWindowController.js", + "cellName": "Flow: Create Maintenance Window - Service Layer Call", + "cellId": "e1d59cf6-0d96-4e0a-ad17-2a7984a9157f", + "visible": true, + "startLine": 35, + "endLine": 35, + "parentPath": "server/src/controllers/v1/maintenanceWindowController.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "c3d1a95c-e93d-499a-8830-a6d870902414" + } + ] + }, + "12d0ab8a-392f-48fa-9efa-06ffe1f3a252": { + "path": "12d0ab8a-392f-48fa-9efa-06ffe1f3a252", + "cellName": "Flow: Create\nMaintenance Window\n- Database\nModule Call", + "cellId": "12d0ab8a-392f-48fa-9efa-06ffe1f3a252", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-80aed28e-68b6-47bf-8bc9-ededb4824066-12d0ab8a-392f-48fa-9efa-06ffe1f3a252": { + "path": "generated-edge-simstep-80aed28e-68b6-47bf-8bc9-ededb4824066-12d0ab8a-392f-48fa-9efa-06ffe1f3a252", + "fileName": "maintenanceWindowService.js", + "cellName": "Flow: Create Maintenance Window - Database Module Call", + "cellId": "12d0ab8a-392f-48fa-9efa-06ffe1f3a252", + "visible": true, + "startLine": 28, + "endLine": 37, + "parentPath": "server/src/service/v1/business/maintenanceWindowService.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "80aed28e-68b6-47bf-8bc9-ededb4824066" + } + ] + }, + "fcf4eef4-394c-4872-9124-f7a0b32cdc3b": { + "path": "fcf4eef4-394c-4872-9124-f7a0b32cdc3b", + "cellName": "Flow: Create\nMaintenance Window\n- API\nResponse", + "cellId": "fcf4eef4-394c-4872-9124-f7a0b32cdc3b", + "visible": true + }, + "generated-edge-simstep-69fdc2a9-837f-466c-9562-a12319019580-fcf4eef4-394c-4872-9124-f7a0b32cdc3b": { + "path": "generated-edge-simstep-69fdc2a9-837f-466c-9562-a12319019580-fcf4eef4-394c-4872-9124-f7a0b32cdc3b", + "fileName": "maintenanceWindowModule.js", + "cellName": "Flow: Create Maintenance Window - API Response", + "cellId": "fcf4eef4-394c-4872-9124-f7a0b32cdc3b", + "visible": true, + "startLine": 37, + "endLine": 39, + "parentPath": "server/src/db/v1/modules/maintenanceWindowModule.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "69fdc2a9-837f-466c-9562-a12319019580" + } + ] + }, + "7f921125-500f-4902-abc3-936ecac033d8": { + "path": "7f921125-500f-4902-abc3-936ecac033d8", + "cellName": "Flow: View\nMaintenance Windows\n- API\nCall", + "cellId": "7f921125-500f-4902-abc3-936ecac033d8", + "visible": true + }, + "generated-edge-simstep-7e20cd6c-d24e-4540-86d6-d6a9fa087314-7f921125-500f-4902-abc3-936ecac033d8": { + "path": "generated-edge-simstep-7e20cd6c-d24e-4540-86d6-d6a9fa087314-7f921125-500f-4902-abc3-936ecac033d8", + "fileName": "index.jsx", + "cellName": "Flow: View Maintenance Windows - API Call", + "cellId": "7f921125-500f-4902-abc3-936ecac033d8", + "visible": true, + "startLine": 824, + "endLine": 824, + "parentPath": "client/src/Pages/v1/Maintenance/index.jsx", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "7e20cd6c-d24e-4540-86d6-d6a9fa087314" + } + ] + }, + "c3127c74-43f1-42d7-8da6-b0a494c3658f": { + "path": "c3127c74-43f1-42d7-8da6-b0a494c3658f", + "cellName": "Flow: View\nMaintenance Windows\n- Database\nQuery", + "cellId": "c3127c74-43f1-42d7-8da6-b0a494c3658f", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-feb8bd4f-339f-4132-a709-89c63c4efa9c-c3127c74-43f1-42d7-8da6-b0a494c3658f": { + "path": "generated-edge-simstep-feb8bd4f-339f-4132-a709-89c63c4efa9c-c3127c74-43f1-42d7-8da6-b0a494c3658f", + "fileName": "maintenanceWindowController.js", + "cellName": "Flow: View Maintenance Windows - Database Query", + "cellId": "c3127c74-43f1-42d7-8da6-b0a494c3658f", + "visible": true, + "startLine": 48, + "endLine": 48, + "parentPath": "server/src/controllers/v1/maintenanceWindowController.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "feb8bd4f-339f-4132-a709-89c63c4efa9c" + } + ] + }, + "6c9979cd-1cc1-4227-b535-2ec3a10ca27e": { + "path": "6c9979cd-1cc1-4227-b535-2ec3a10ca27e", + "cellName": "Flow: View\nMaintenance Windows\n- Data\nReturn to\nFrontend", + "cellId": "6c9979cd-1cc1-4227-b535-2ec3a10ca27e", + "visible": true + }, + "generated-edge-simstep-eda86dca-dea6-4671-a079-7d212f4f18c4-6c9979cd-1cc1-4227-b535-2ec3a10ca27e": { + "path": "generated-edge-simstep-eda86dca-dea6-4671-a079-7d212f4f18c4-6c9979cd-1cc1-4227-b535-2ec3a10ca27e", + "fileName": "maintenanceWindowModule.js", + "cellName": "Flow: View Maintenance Windows - Data Return to Frontend", + "cellId": "6c9979cd-1cc1-4227-b535-2ec3a10ca27e", + "visible": true, + "startLine": 77, + "endLine": 80, + "parentPath": "server/src/db/v1/modules/maintenanceWindowModule.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "eda86dca-dea6-4671-a079-7d212f4f18c4" + } + ] + }, + "c99291bb-7316-440e-9a7f-323ee39e26b8": { + "path": "c99291bb-7316-440e-9a7f-323ee39e26b8", + "cellName": "Flow: Monitor\nSuppression -\nMaintenance Check\nCall", + "cellId": "c99291bb-7316-440e-9a7f-323ee39e26b8", + "visible": true, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985" + }, + "generated-edge-simstep-87583096-f1ec-4160-ae52-5238f32ed0ab-c99291bb-7316-440e-9a7f-323ee39e26b8": { + "path": "generated-edge-simstep-87583096-f1ec-4160-ae52-5238f32ed0ab-c99291bb-7316-440e-9a7f-323ee39e26b8", + "fileName": "SuperSimpleQueueHelper.js", + "cellName": "Flow: Monitor Suppression - Maintenance Check Call", + "cellId": "c99291bb-7316-440e-9a7f-323ee39e26b8", + "visible": true, + "startLine": 36, + "endLine": 36, + "parentPath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "87583096-f1ec-4160-ae52-5238f32ed0ab" + } + ] + }, + "8a13e585-2310-420b-9e92-211b6d65f201": { + "path": "8a13e585-2310-420b-9e92-211b6d65f201", + "cellName": "Flow: Monitor\nSuppression -\nDatabase Result\nReturn", + "cellId": "8a13e585-2310-420b-9e92-211b6d65f201", + "visible": true, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985" + }, + "generated-edge-simstep-e6542e14-556d-482a-8c35-f7b03997be49-8a13e585-2310-420b-9e92-211b6d65f201": { + "path": "generated-edge-simstep-e6542e14-556d-482a-8c35-f7b03997be49-8a13e585-2310-420b-9e92-211b6d65f201", + "fileName": "SuperSimpleQueueHelper.js", + "cellName": "Flow: Monitor Suppression - Database Result Return", + "cellId": "8a13e585-2310-420b-9e92-211b6d65f201", + "visible": true, + "startLine": 69, + "endLine": 76, + "parentPath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "e6542e14-556d-482a-8c35-f7b03997be49" + } + ] + }, + "40f4d9df-4c0f-4384-901c-8d7117e3b538": { + "path": "40f4d9df-4c0f-4384-901c-8d7117e3b538", + "cellName": "Flow: Monitor\nSuppression -\nReturn Suppression\nFlag", + "cellId": "40f4d9df-4c0f-4384-901c-8d7117e3b538", + "visible": true, + "parentCellId": "04c12e9a-e025-457c-a634-5f7472a42985" + }, + "generated-edge-simstep-a715d98f-60bf-4f32-aae6-21ee4e09f4da-40f4d9df-4c0f-4384-901c-8d7117e3b538": { + "path": "generated-edge-simstep-a715d98f-60bf-4f32-aae6-21ee4e09f4da-40f4d9df-4c0f-4384-901c-8d7117e3b538", + "fileName": "SuperSimpleQueueHelper.js", + "cellName": "Flow: Monitor Suppression - Return Suppression Flag", + "cellId": "40f4d9df-4c0f-4384-901c-8d7117e3b538", + "visible": true, + "startLine": 114, + "endLine": 114, + "parentPath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "simSteps": [ + { + "simulationKey": "Scheduled Maintenance Windows", + "simStepId": "a715d98f-60bf-4f32-aae6-21ee4e09f4da" + } + ] + }, + "a968db5d-8f6d-4fd7-9df0-8b332b521703": { + "path": "a968db5d-8f6d-4fd7-9df0-8b332b521703", + "cellName": "middleware", + "cellId": "a968db5d-8f6d-4fd7-9df0-8b332b521703", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "09082f73-e215-465f-9206-0a963fb4c7cb": { + "path": "09082f73-e215-465f-9206-0a963fb4c7cb", + "cellName": "v1", + "cellId": "09082f73-e215-465f-9206-0a963fb4c7cb", + "visible": true, + "parentCellId": "a968db5d-8f6d-4fd7-9df0-8b332b521703" + }, + "3cad7666-0d1a-4e31-b6ca-70d9c3a46b19": { + "path": "3cad7666-0d1a-4e31-b6ca-70d9c3a46b19", + "cellName": "Account", + "cellId": "3cad7666-0d1a-4e31-b6ca-70d9c3a46b19", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679" + }, + "9edafb8a-3661-4a7e-8510-a47f3cbc7564": { + "path": "9edafb8a-3661-4a7e-8510-a47f3cbc7564", + "cellName": "inviteRoute.js", + "cellId": "9edafb8a-3661-4a7e-8510-a47f3cbc7564", + "visible": true, + "parentCellId": "d3375a20-2776-4771-b806-649bbeb541bd" + }, + "001fa2e4-eadd-4474-b2ec-79d7ea43465f": { + "path": "001fa2e4-eadd-4474-b2ec-79d7ea43465f", + "cellName": "isAllowed.js", + "cellId": "001fa2e4-eadd-4474-b2ec-79d7ea43465f", + "visible": true, + "parentCellId": "09082f73-e215-465f-9206-0a963fb4c7cb" + }, + "6a186805-9d50-4bc3-9d56-e627a7ec2eb8": { + "path": "6a186805-9d50-4bc3-9d56-e627a7ec2eb8", + "cellName": "inviteController.js", + "cellId": "6a186805-9d50-4bc3-9d56-e627a7ec2eb8", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "33960806-1b9e-4d61-a163-e4bebf82d87a": { + "path": "33960806-1b9e-4d61-a163-e4bebf82d87a", + "cellName": "index.jsx", + "cellId": "33960806-1b9e-4d61-a163-e4bebf82d87a", + "visible": true, + "parentCellId": "3cad7666-0d1a-4e31-b6ca-70d9c3a46b19" + }, + "425a4c15-043f-4d05-8832-da00e18f97b9": { + "path": "425a4c15-043f-4d05-8832-da00e18f97b9", + "cellName": "components", + "cellId": "425a4c15-043f-4d05-8832-da00e18f97b9", + "visible": true, + "parentCellId": "3cad7666-0d1a-4e31-b6ca-70d9c3a46b19" + }, + "570278b7-9b72-4299-9fd9-46b253b48e9b": { + "path": "570278b7-9b72-4299-9fd9-46b253b48e9b", + "cellName": "inviteService.js", + "cellId": "570278b7-9b72-4299-9fd9-46b253b48e9b", + "visible": true, + "parentCellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb" + }, + "08853f8a-f596-4df0-9190-dad04f54caef": { + "path": "08853f8a-f596-4df0-9190-dad04f54caef", + "cellName": "inviteModule.js", + "cellId": "08853f8a-f596-4df0-9190-dad04f54caef", + "visible": true, + "parentCellId": "82cb3dbe-c922-438a-a912-255376f4b380" + }, + "1145f351-2257-411d-86f7-30bf8c26ad68": { + "path": "1145f351-2257-411d-86f7-30bf8c26ad68", + "cellName": "TeamPanel.jsx", + "cellId": "1145f351-2257-411d-86f7-30bf8c26ad68", + "visible": true, + "parentCellId": "425a4c15-043f-4d05-8832-da00e18f97b9" + }, + "3de2d413-5e9b-49f4-9409-74be32c52e0f": { + "path": "3de2d413-5e9b-49f4-9409-74be32c52e0f", + "cellName": "Client: Conditional UI Rendering Based on Role - index.jsx:L37-41", + "cellId": "3de2d413-5e9b-49f4-9409-74be32c52e0f", + "visible": true, + "parentCellId": "33960806-1b9e-4d61-a163-e4bebf82d87a" + }, + "client/src/Pages/v1/Account/index.jsx-simstep-2d5a952a-ce62-4948-9309-96b28ad74e59": { + "path": "client/src/Pages/v1/Account/index.jsx-simstep-2d5a952a-ce62-4948-9309-96b28ad74e59", + "fileName": "index.jsx", + "wiki": "A user with 'superadmin' or 'admin' role navigates to the Account page. The application checks the user's role stored in the Redux state to conditionally render the 'Team' management tab. Non-admin users will not see this option.", + "cellName": "Client: Conditional UI Rendering Based on Role - index.jsx:L37-41", + "cellId": "3de2d413-5e9b-49f4-9409-74be32c52e0f", + "visible": true, + "startLine": 37, + "endLine": 41, + "parentCellId": "33960806-1b9e-4d61-a163-e4bebf82d87a", + "parentPath": "client/src/Pages/v1/Account/index.jsx", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "2d5a952a-ce62-4948-9309-96b28ad74e59" + } + ] + }, + "b27f1bba-c655-40c6-a1dc-71e78c590c92": { + "path": "b27f1bba-c655-40c6-a1dc-71e78c590c92", + "cellName": "Client: Initiate User Invitation - TeamPanel.jsx:L348-354", + "cellId": "b27f1bba-c655-40c6-a1dc-71e78c590c92", + "visible": true, + "parentCellId": "1145f351-2257-411d-86f7-30bf8c26ad68" + }, + "client/src/Pages/v1/Account/components/TeamPanel.jsx-simstep-eebaca25-d588-49c4-a35c-9abc82fde580": { + "path": "client/src/Pages/v1/Account/components/TeamPanel.jsx-simstep-eebaca25-d588-49c4-a35c-9abc82fde580", + "fileName": "TeamPanel.jsx", + "wiki": "In the `TeamPanel`, the admin clicks the 'Add Team Member' button, then selects 'Invite a team member'. A dialog opens, prompting for the new user's email and role. After filling the form and clicking 'E-mail token', the `handleInviteMember` function is triggered to start the invitation process.", + "cellName": "Client: Initiate User Invitation - TeamPanel.jsx:L348-354", + "cellId": "b27f1bba-c655-40c6-a1dc-71e78c590c92", + "visible": true, + "startLine": 348, + "endLine": 354, + "parentCellId": "1145f351-2257-411d-86f7-30bf8c26ad68", + "parentPath": "client/src/Pages/v1/Account/components/TeamPanel.jsx", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "eebaca25-d588-49c4-a35c-9abc82fde580" + } + ] + }, + "8b3cb6c2-ed57-4510-be15-decb8e3c89e1": { + "path": "8b3cb6c2-ed57-4510-be15-decb8e3c89e1", + "cellName": "Server: Role-Based Access Control Middleware - isAllowed.js:L39-44", + "cellId": "8b3cb6c2-ed57-4510-be15-decb8e3c89e1", + "visible": true, + "parentCellId": "001fa2e4-eadd-4474-b2ec-79d7ea43465f" + }, + "server/src/middleware/v1/isAllowed.js-simstep-50dd7316-7d01-43f7-9036-d97d51a9c178": { + "path": "server/src/middleware/v1/isAllowed.js-simstep-50dd7316-7d01-43f7-9036-d97d51a9c178", + "fileName": "isAllowed.js", + "wiki": "The server receives the request. Before reaching the main controller, the `isAllowed` middleware intercepts the request. It verifies the JWT from the request header and checks if the user's role (e.g., 'superadmin') is present in the list of allowed roles for this route (`['admin', 'superadmin']`).", + "cellName": "Server: Role-Based Access Control Middleware - isAllowed.js:L39-44", + "cellId": "8b3cb6c2-ed57-4510-be15-decb8e3c89e1", + "visible": true, + "startLine": 39, + "endLine": 44, + "parentCellId": "001fa2e4-eadd-4474-b2ec-79d7ea43465f", + "parentPath": "server/src/middleware/v1/isAllowed.js", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "50dd7316-7d01-43f7-9036-d97d51a9c178" + } + ] + }, + "ae858158-8db6-4917-8fda-0966a953f6ed": { + "path": "ae858158-8db6-4917-8fda-0966a953f6ed", + "cellName": "Server: Process Invitation in Service Layer - inviteService.js:L24-28", + "cellId": "ae858158-8db6-4917-8fda-0966a953f6ed", + "visible": true, + "parentCellId": "570278b7-9b72-4299-9fd9-46b253b48e9b" + }, + "server/src/service/v1/business/inviteService.js-simstep-55fe9963-30d2-47f8-859f-b485a77064c4": { + "path": "server/src/service/v1/business/inviteService.js-simstep-55fe9963-30d2-47f8-859f-b485a77064c4", + "fileName": "inviteService.js", + "wiki": "The `inviteController` calls the `inviteService`. The service layer handles the business logic: it first requests an invitation token from the database module and then instructs the email service to send the invitation.", + "cellName": "Server: Process Invitation in Service Layer - inviteService.js:L24-28", + "cellId": "ae858158-8db6-4917-8fda-0966a953f6ed", + "visible": true, + "startLine": 24, + "endLine": 28, + "parentCellId": "570278b7-9b72-4299-9fd9-46b253b48e9b", + "parentPath": "server/src/service/v1/business/inviteService.js", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "55fe9963-30d2-47f8-859f-b485a77064c4" + } + ] + }, + "a7be2d0b-0227-4228-a0a6-074f7324fd89": { + "path": "a7be2d0b-0227-4228-a0a6-074f7324fd89", + "cellName": "Server: Dispatch Invitation Email - inviteService.js:L26-28", + "cellId": "a7be2d0b-0227-4228-a0a6-074f7324fd89", + "visible": true, + "parentCellId": "570278b7-9b72-4299-9fd9-46b253b48e9b" + }, + "server/src/service/v1/business/inviteService.js-simstep-7d1f86f2-c8d5-48fb-a8c1-b337dc56271f": { + "path": "server/src/service/v1/business/inviteService.js-simstep-7d1f86f2-c8d5-48fb-a8c1-b337dc56271f", + "fileName": "inviteService.js", + "wiki": "After successfully storing the token, the `inviteService` uses the `emailService` to compose and send an email to the new user. The email contains a unique registration link including the generated token.", + "cellName": "Server: Dispatch Invitation Email - inviteService.js:L26-28", + "cellId": "a7be2d0b-0227-4228-a0a6-074f7324fd89", + "visible": true, + "startLine": 26, + "endLine": 28, + "parentCellId": "570278b7-9b72-4299-9fd9-46b253b48e9b", + "parentPath": "server/src/service/v1/business/inviteService.js", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "7d1f86f2-c8d5-48fb-a8c1-b337dc56271f" + } + ] + }, + "02d87257-883c-4822-a421-823c983532ec": { + "path": "02d87257-883c-4822-a421-823c983532ec", + "cellName": "Client: Display Success Feedback - TeamPanel.jsx:L145-149", + "cellId": "02d87257-883c-4822-a421-823c983532ec", + "visible": true, + "parentCellId": "1145f351-2257-411d-86f7-30bf8c26ad68" + }, + "client/src/Pages/v1/Account/components/TeamPanel.jsx-simstep-700f35e0-b39d-417c-b0a8-0612feade977": { + "path": "client/src/Pages/v1/Account/components/TeamPanel.jsx-simstep-700f35e0-b39d-417c-b0a8-0612feade977", + "fileName": "TeamPanel.jsx", + "wiki": "The client application receives the successful API response. It then closes the invitation dialog and displays a toast notification to the admin, confirming that the invitation has been sent.", + "cellName": "Client: Display Success Feedback - TeamPanel.jsx:L145-149", + "cellId": "02d87257-883c-4822-a421-823c983532ec", + "visible": true, + "startLine": 145, + "endLine": 149, + "parentCellId": "1145f351-2257-411d-86f7-30bf8c26ad68", + "parentPath": "client/src/Pages/v1/Account/components/TeamPanel.jsx", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "700f35e0-b39d-417c-b0a8-0612feade977" + } + ] + }, + "c441312f-f3c6-48c6-b1b9-7c12749d91bb": { + "path": "c441312f-f3c6-48c6-b1b9-7c12749d91bb", + "cellName": "Client: User\nNavigates to\nTeam Panel", + "cellId": "c441312f-f3c6-48c6-b1b9-7c12749d91bb", + "visible": true, + "parentCellId": "3cad7666-0d1a-4e31-b6ca-70d9c3a46b19" + }, + "generated-edge-simstep-10f80ada-2d0a-4e98-bd5d-057e7d4fe6de-c441312f-f3c6-48c6-b1b9-7c12749d91bb": { + "path": "generated-edge-simstep-10f80ada-2d0a-4e98-bd5d-057e7d4fe6de-c441312f-f3c6-48c6-b1b9-7c12749d91bb", + "fileName": "index.jsx", + "cellName": "Client: User Navigates to Team Panel", + "cellId": "c441312f-f3c6-48c6-b1b9-7c12749d91bb", + "visible": true, + "startLine": 98, + "endLine": 98, + "parentPath": "client/src/Pages/v1/Account/index.jsx", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "10f80ada-2d0a-4e98-bd5d-057e7d4fe6de" + } + ] + }, + "3530244f-2689-444e-bcbf-611db2e3c000": { + "path": "3530244f-2689-444e-bcbf-611db2e3c000", + "cellName": "API Call:\nSend Invitation\nRequest", + "cellId": "3530244f-2689-444e-bcbf-611db2e3c000", + "visible": true + }, + "generated-edge-simstep-16d87608-4458-430c-b459-cd9b1d413fa6-3530244f-2689-444e-bcbf-611db2e3c000": { + "path": "generated-edge-simstep-16d87608-4458-430c-b459-cd9b1d413fa6-3530244f-2689-444e-bcbf-611db2e3c000", + "fileName": "TeamPanel.jsx", + "cellName": "API Call: Send Invitation Request", + "cellId": "3530244f-2689-444e-bcbf-611db2e3c000", + "visible": true, + "startLine": 14, + "endLine": 14, + "parentPath": "client/src/Pages/v1/Account/components/TeamPanel.jsx", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "16d87608-4458-430c-b459-cd9b1d413fa6" + } + ] + }, + "2ee1c4e8-90fb-4470-ad9f-6ffed5420853": { + "path": "2ee1c4e8-90fb-4470-ad9f-6ffed5420853", + "cellName": "Server: Forward\nRequest to\nController", + "cellId": "2ee1c4e8-90fb-4470-ad9f-6ffed5420853", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-6c024d4c-178b-45fc-8bca-4fc6c00549a4-2ee1c4e8-90fb-4470-ad9f-6ffed5420853": { + "path": "generated-edge-simstep-6c024d4c-178b-45fc-8bca-4fc6c00549a4-2ee1c4e8-90fb-4470-ad9f-6ffed5420853", + "fileName": "isAllowed.js", + "cellName": "Server: Forward Request to Controller", + "cellId": "2ee1c4e8-90fb-4470-ad9f-6ffed5420853", + "visible": true, + "startLine": 61, + "endLine": 68, + "parentPath": "server/src/middleware/v1/isAllowed.js", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "6c024d4c-178b-45fc-8bca-4fc6c00549a4" + } + ] + }, + "bdd52e6f-ddf7-4966-b3af-957f9553082f": { + "path": "bdd52e6f-ddf7-4966-b3af-957f9553082f", + "cellName": "Database Query:\nCreate and\nStore Invite\nToken", + "cellId": "bdd52e6f-ddf7-4966-b3af-957f9553082f", + "visible": true, + "parentCellId": "570278b7-9b72-4299-9fd9-46b253b48e9b" + }, + "generated-edge-simstep-c796f8ad-d548-45f3-8d62-67f59c6c1aa4-bdd52e6f-ddf7-4966-b3af-957f9553082f": { + "path": "generated-edge-simstep-c796f8ad-d548-45f3-8d62-67f59c6c1aa4-bdd52e6f-ddf7-4966-b3af-957f9553082f", + "fileName": "inviteService.js", + "cellName": "Database Query: Create and Store Invite Token", + "cellId": "bdd52e6f-ddf7-4966-b3af-957f9553082f", + "visible": true, + "startLine": 23, + "endLine": 26, + "parentPath": "server/src/service/v1/business/inviteService.js", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "c796f8ad-d548-45f3-8d62-67f59c6c1aa4" + } + ] + }, + "52732a37-82b8-40a7-b12d-dc1b82380b2d": { + "path": "52732a37-82b8-40a7-b12d-dc1b82380b2d", + "cellName": "API Response:\nSend Confirmation\nto Client", + "cellId": "52732a37-82b8-40a7-b12d-dc1b82380b2d", + "visible": true + }, + "generated-edge-simstep-6ba5094d-d1e6-4ec3-8917-28cdb30f2ef9-52732a37-82b8-40a7-b12d-dc1b82380b2d": { + "path": "generated-edge-simstep-6ba5094d-d1e6-4ec3-8917-28cdb30f2ef9-52732a37-82b8-40a7-b12d-dc1b82380b2d", + "fileName": "inviteService.js", + "cellName": "API Response: Send Confirmation to Client", + "cellId": "52732a37-82b8-40a7-b12d-dc1b82380b2d", + "visible": true, + "startLine": 69, + "endLine": 72, + "parentPath": "server/src/service/v1/business/inviteService.js", + "simSteps": [ + { + "simulationKey": "User and Team Management with Role-Based Access", + "simStepId": "6ba5094d-d1e6-4ec3-8917-28cdb30f2ef9" + } + ] + }, + "56dae13e-5735-4089-a9a4-e18be315aff7": { + "path": "56dae13e-5735-4089-a9a4-e18be315aff7", + "cellName": "BulkImport", + "cellId": "56dae13e-5735-4089-a9a4-e18be315aff7", + "visible": true, + "parentCellId": "7090fcdb-09be-4c85-86fd-e73553bbb2ff" + }, + "29b6ad3c-9538-47bc-8796-aab942dc4b71": { + "path": "29b6ad3c-9538-47bc-8796-aab942dc4b71", + "cellName": "index.jsx", + "cellId": "29b6ad3c-9538-47bc-8796-aab942dc4b71", + "visible": true, + "parentCellId": "56dae13e-5735-4089-a9a4-e18be315aff7" + }, + "cd84476a-1c8b-4781-811f-58fb9944365b": { + "path": "cd84476a-1c8b-4781-811f-58fb9944365b", + "cellName": "Upload.jsx", + "cellId": "cd84476a-1c8b-4781-811f-58fb9944365b", + "visible": true, + "parentCellId": "56dae13e-5735-4089-a9a4-e18be315aff7" + }, + "c4caeba8-ed9a-4cbc-9888-38cc8ca55362": { + "path": "c4caeba8-ed9a-4cbc-9888-38cc8ca55362", + "cellName": "Flow 1: CSV Import - Navigate to Bulk Import Page - index.jsx:L88-91", + "cellId": "c4caeba8-ed9a-4cbc-9888-38cc8ca55362", + "visible": true, + "parentCellId": "c4d7e7dd-75b1-44f1-a34a-6ed290a8e562" + }, + "client/src/Routes/index.jsx-simstep-a29da01e-8c5a-4e6d-8d3c-e49af528e4fa": { + "path": "client/src/Routes/index.jsx-simstep-a29da01e-8c5a-4e6d-8d3c-e49af528e4fa", + "fileName": "index.jsx", + "wiki": "The user navigates to the \"/uptime/bulk-import\" URL, which is routed to the BulkImport component for rendering the UI.", + "cellName": "Flow 1: CSV Import - Navigate to Bulk Import Page - index.jsx:L88-91", + "cellId": "c4caeba8-ed9a-4cbc-9888-38cc8ca55362", + "visible": true, + "startLine": 88, + "endLine": 91, + "parentCellId": "c4d7e7dd-75b1-44f1-a34a-6ed290a8e562", + "parentPath": "client/src/Routes/index.jsx", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "a29da01e-8c5a-4e6d-8d3c-e49af528e4fa" + } + ] + }, + "6f1948fa-e84a-4f8e-8f3b-8f24c511b3d8": { + "path": "6f1948fa-e84a-4f8e-8f3b-8f24c511b3d8", + "cellName": "Flow 1: CSV Import - User Selects File - Upload.jsx:L23-31", + "cellId": "6f1948fa-e84a-4f8e-8f3b-8f24c511b3d8", + "visible": true, + "parentCellId": "cd84476a-1c8b-4781-811f-58fb9944365b" + }, + "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx-simstep-bbd04d55-fc6a-4ba8-bcde-5a4dc263d6eb": { + "path": "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx-simstep-bbd04d55-fc6a-4ba8-bcde-5a4dc263d6eb", + "fileName": "Upload.jsx", + "wiki": "The user clicks the 'Select File' button, triggering the file input. When a file is chosen, the `handleFileChange` function validates that it's a CSV file and updates the component's state.", + "cellName": "Flow 1: CSV Import - User Selects File - Upload.jsx:L23-31", + "cellId": "6f1948fa-e84a-4f8e-8f3b-8f24c511b3d8", + "visible": true, + "startLine": 23, + "endLine": 31, + "parentCellId": "cd84476a-1c8b-4781-811f-58fb9944365b", + "parentPath": "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "bbd04d55-fc6a-4ba8-bcde-5a4dc263d6eb" + } + ] + }, + "dd2f9356-fad3-47d4-81d5-a5e038298e2b": { + "path": "dd2f9356-fad3-47d4-81d5-a5e038298e2b", + "cellName": "Flow 1: CSV Import - Submit Upload - index.jsx:L32-45", + "cellId": "dd2f9356-fad3-47d4-81d5-a5e038298e2b", + "visible": true, + "parentCellId": "29b6ad3c-9538-47bc-8796-aab942dc4b71" + }, + "client/src/Pages/v1/Uptime/BulkImport/index.jsx-simstep-0139fcaa-eb46-4176-b7bd-1cb15e7f1f4c": { + "path": "client/src/Pages/v1/Uptime/BulkImport/index.jsx-simstep-0139fcaa-eb46-4176-b7bd-1cb15e7f1f4c", + "fileName": "index.jsx", + "wiki": "The user clicks the submit button, which executes the `handleSubmit` function. This function calls the `createBulkMonitors` function provided by the `useCreateBulkMonitors` custom hook.", + "cellName": "Flow 1: CSV Import - Submit Upload - index.jsx:L32-45", + "cellId": "dd2f9356-fad3-47d4-81d5-a5e038298e2b", + "visible": true, + "startLine": 32, + "endLine": 45, + "parentCellId": "29b6ad3c-9538-47bc-8796-aab942dc4b71", + "parentPath": "client/src/Pages/v1/Uptime/BulkImport/index.jsx", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "0139fcaa-eb46-4176-b7bd-1cb15e7f1f4c" + } + ] + }, + "5967e576-f542-4c0b-a3f4-9446ae992721": { + "path": "5967e576-f542-4c0b-a3f4-9446ae992721", + "cellName": "Flow 1: CSV Import - Send API Request - NetworkService.js:L978-983", + "cellId": "5967e576-f542-4c0b-a3f4-9446ae992721", + "visible": true, + "parentCellId": "16731e9d-3f73-4055-9d79-5ca9be679f68" + }, + "client/src/Utils/NetworkService.js-simstep-2fc087b3-033d-4cf5-81f2-e35b99a14bce": { + "path": "client/src/Utils/NetworkService.js-simstep-2fc087b3-033d-4cf5-81f2-e35b99a14bce", + "fileName": "NetworkService.js", + "wiki": "The NetworkService sends the `FormData` containing the CSV file to the backend via a POST request to the `/monitors/bulk` endpoint. The `Content-Type` is set to `multipart/form-data`.", + "cellName": "Flow 1: CSV Import - Send API Request - NetworkService.js:L978-983", + "cellId": "5967e576-f542-4c0b-a3f4-9446ae992721", + "visible": true, + "startLine": 978, + "endLine": 983, + "parentCellId": "16731e9d-3f73-4055-9d79-5ca9be679f68", + "parentPath": "client/src/Utils/NetworkService.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "2fc087b3-033d-4cf5-81f2-e35b99a14bce" + } + ] + }, + "c1ac3c26-996a-43ef-9c34-4c2776188784": { + "path": "c1ac3c26-996a-43ef-9c34-4c2776188784", + "cellName": "Flow 1: CSV Import - Server Receives Request - monitorRoute.js:L46", + "cellId": "c1ac3c26-996a-43ef-9c34-4c2776188784", + "visible": true, + "parentCellId": "10611636-ec73-4a26-87a5-654ae64a4843" + }, + "server/src/routes/v1/monitorRoute.js-simstep-07bf3d83-047d-4e51-9f96-8c9c64848500": { + "path": "server/src/routes/v1/monitorRoute.js-simstep-07bf3d83-047d-4e51-9f96-8c9c64848500", + "fileName": "monitorRoute.js", + "wiki": "The server's monitor route receives the request. The `multer` middleware processes the file upload, making the file available in `req.file`. The request is then passed to the `createBulkMonitors` controller method.", + "cellName": "Flow 1: CSV Import - Server Receives Request - monitorRoute.js:L46", + "cellId": "c1ac3c26-996a-43ef-9c34-4c2776188784", + "visible": true, + "startLine": 46, + "endLine": 46, + "parentCellId": "10611636-ec73-4a26-87a5-654ae64a4843", + "parentPath": "server/src/routes/v1/monitorRoute.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "07bf3d83-047d-4e51-9f96-8c9c64848500" + } + ] + }, + "0e520a6d-34bf-467b-a894-e73979364478": { + "path": "0e520a6d-34bf-467b-a894-e73979364478", + "cellName": "Flow 1: CSV Import - Controller Processes Data - monitorController.js:L228-232", + "cellId": "0e520a6d-34bf-467b-a894-e73979364478", + "visible": true, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261" + }, + "server/src/controllers/v1/monitorController.js-simstep-a3799570-64bf-481e-9ffe-31dbad8a36e4": { + "path": "server/src/controllers/v1/monitorController.js-simstep-a3799570-64bf-481e-9ffe-31dbad8a36e4", + "fileName": "monitorController.js", + "wiki": "The controller validates the presence of the file and extracts the file data (buffer), `userId`, and `teamId`. It then calls the `monitorService.createBulkMonitors` method to handle the core logic.", + "cellName": "Flow 1: CSV Import - Controller Processes Data - monitorController.js:L228-232", + "cellId": "0e520a6d-34bf-467b-a894-e73979364478", + "visible": true, + "startLine": 228, + "endLine": 232, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261", + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "a3799570-64bf-481e-9ffe-31dbad8a36e4" + } + ] + }, + "6139e487-1dd5-405a-ba7d-ededec4b282e": { + "path": "6139e487-1dd5-405a-ba7d-ededec4b282e", + "cellName": "Flow 1: CSV Import - Service Parses CSV - monitorService.js:L85-134", + "cellId": "6139e487-1dd5-405a-ba7d-ededec4b282e", + "visible": true, + "parentCellId": "b4e479df-9b21-4430-9383-84831f503c2e" + }, + "server/src/service/v1/business/monitorService.js-simstep-2c7c3082-e4b6-4525-860f-2ffcd9e86db1": { + "path": "server/src/service/v1/business/monitorService.js-simstep-2c7c3082-e4b6-4525-860f-2ffcd9e86db1", + "fileName": "monitorService.js", + "wiki": "The monitor service uses the `papaparse` library to parse the CSV file buffer into a JSON object array. In the `complete` callback, it enriches each record with `userId` and `teamId`.", + "cellName": "Flow 1: CSV Import - Service Parses CSV - monitorService.js:L85-134", + "cellId": "6139e487-1dd5-405a-ba7d-ededec4b282e", + "visible": true, + "startLine": 85, + "endLine": 134, + "parentCellId": "b4e479df-9b21-4430-9383-84831f503c2e", + "parentPath": "server/src/service/v1/business/monitorService.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "2c7c3082-e4b6-4525-860f-2ffcd9e86db1" + } + ] + }, + "d9d519af-5c4d-4494-929e-a869581c1dc4": { + "path": "d9d519af-5c4d-4494-929e-a869581c1dc4", + "cellName": "Flow 1: CSV Import - Database Bulk Save - monitorModule.js:L465-468", + "cellId": "d9d519af-5c4d-4494-929e-a869581c1dc4", + "visible": true, + "parentCellId": "630b3ece-2648-48d5-99b5-ef9611de2ad5" + }, + "server/src/db/v1/modules/monitorModule.js-simstep-70f18c21-2918-402e-9c35-1bdce8bc82d4": { + "path": "server/src/db/v1/modules/monitorModule.js-simstep-70f18c21-2918-402e-9c35-1bdce8bc82d4", + "fileName": "monitorModule.js", + "wiki": "The `monitorModule` uses `Monitor.bulkSave` to efficiently insert all the new monitor documents into the MongoDB database in a single operation.", + "cellName": "Flow 1: CSV Import - Database Bulk Save - monitorModule.js:L465-468", + "cellId": "d9d519af-5c4d-4494-929e-a869581c1dc4", + "visible": true, + "startLine": 465, + "endLine": 468, + "parentCellId": "630b3ece-2648-48d5-99b5-ef9611de2ad5", + "parentPath": "server/src/db/v1/modules/monitorModule.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "70f18c21-2918-402e-9c35-1bdce8bc82d4" + } + ] + }, + "1e1d975f-7f0f-4132-bd71-2563b35e99b3": { + "path": "1e1d975f-7f0f-4132-bd71-2563b35e99b3", + "cellName": "Flow 1: CSV Import - Enqueue Monitor Jobs - monitorService.js:L128-132", + "cellId": "1e1d975f-7f0f-4132-bd71-2563b35e99b3", + "visible": true, + "parentCellId": "b4e479df-9b21-4430-9383-84831f503c2e" + }, + "server/src/service/v1/business/monitorService.js-simstep-66fbf913-e662-4bd9-ada6-077be9e37f07": { + "path": "server/src/service/v1/business/monitorService.js-simstep-66fbf913-e662-4bd9-ada6-077be9e37f07", + "fileName": "monitorService.js", + "wiki": "After the monitors are saved, the service iterates through them and adds a new job to the `jobQueue` for each one, scheduling them for their first check. It then resolves the promise, returning the data up the call stack.", + "cellName": "Flow 1: CSV Import - Enqueue Monitor Jobs - monitorService.js:L128-132", + "cellId": "1e1d975f-7f0f-4132-bd71-2563b35e99b3", + "visible": true, + "startLine": 128, + "endLine": 132, + "parentCellId": "b4e479df-9b21-4430-9383-84831f503c2e", + "parentPath": "server/src/service/v1/business/monitorService.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "66fbf913-e662-4bd9-ada6-077be9e37f07" + } + ] + }, + "72b55fa1-d944-4e46-ac98-f6a6cb201afa": { + "path": "72b55fa1-d944-4e46-ac98-f6a6cb201afa", + "cellName": "Flow 1: CSV Import - Send Success Response - monitorController.js:L232-235", + "cellId": "72b55fa1-d944-4e46-ac98-f6a6cb201afa", + "visible": true, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261" + }, + "server/src/controllers/v1/monitorController.js-simstep-f5377571-156f-4c5c-8bab-b6dacdf0fea6": { + "path": "server/src/controllers/v1/monitorController.js-simstep-f5377571-156f-4c5c-8bab-b6dacdf0fea6", + "fileName": "monitorController.js", + "wiki": "The controller constructs a successful JSON response containing a message and the data for the newly created monitors, and sends it back to the client.", + "cellName": "Flow 1: CSV Import - Send Success Response - monitorController.js:L232-235", + "cellId": "72b55fa1-d944-4e46-ac98-f6a6cb201afa", + "visible": true, + "startLine": 232, + "endLine": 235, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261", + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "f5377571-156f-4c5c-8bab-b6dacdf0fea6" + } + ] + }, + "ae3b7e23-ed64-40aa-9760-389f0a869f8d": { + "path": "ae3b7e23-ed64-40aa-9760-389f0a869f8d", + "cellName": "Flow 1: CSV Import - Client Handles Success - index.jsx:L37-41", + "cellId": "ae3b7e23-ed64-40aa-9760-389f0a869f8d", + "visible": true, + "parentCellId": "29b6ad3c-9538-47bc-8796-aab942dc4b71" + }, + "client/src/Pages/v1/Uptime/BulkImport/index.jsx-simstep-72b3f16d-290d-4a61-8748-2ad90d63ec68": { + "path": "client/src/Pages/v1/Uptime/BulkImport/index.jsx-simstep-72b3f16d-290d-4a61-8748-2ad90d63ec68", + "fileName": "index.jsx", + "wiki": "The client-side promise resolves. The `handleSubmit` function in the `BulkImport` component receives the success status, displays a success toast message, and navigates the user to the main uptime monitoring page.", + "cellName": "Flow 1: CSV Import - Client Handles Success - index.jsx:L37-41", + "cellId": "ae3b7e23-ed64-40aa-9760-389f0a869f8d", + "visible": true, + "startLine": 37, + "endLine": 41, + "parentCellId": "29b6ad3c-9538-47bc-8796-aab942dc4b71", + "parentPath": "client/src/Pages/v1/Uptime/BulkImport/index.jsx", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "72b3f16d-290d-4a61-8748-2ad90d63ec68" + } + ] + }, + "26d76516-fd7e-4ae8-956a-d077de4257fa": { + "path": "26d76516-fd7e-4ae8-956a-d077de4257fa", + "cellName": "Flow 2: CSV Export - Initiate Export - monitorRoute.js:L45", + "cellId": "26d76516-fd7e-4ae8-956a-d077de4257fa", + "visible": true, + "parentCellId": "10611636-ec73-4a26-87a5-654ae64a4843" + }, + "server/src/routes/v1/monitorRoute.js-simstep-63d8d81f-f436-4594-a6eb-8c0fd2110bc1": { + "path": "server/src/routes/v1/monitorRoute.js-simstep-63d8d81f-f436-4594-a6eb-8c0fd2110bc1", + "fileName": "monitorRoute.js", + "wiki": "An admin user clicks an 'Export to CSV' button in the UI. This action triggers a call to the `/monitors/export` API endpoint to begin the export process.", + "cellName": "Flow 2: CSV Export - Initiate Export - monitorRoute.js:L45", + "cellId": "26d76516-fd7e-4ae8-956a-d077de4257fa", + "visible": true, + "startLine": 45, + "endLine": 45, + "parentCellId": "10611636-ec73-4a26-87a5-654ae64a4843", + "parentPath": "server/src/routes/v1/monitorRoute.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "63d8d81f-f436-4594-a6eb-8c0fd2110bc1" + } + ] + }, + "a3106388-6a3d-4b68-a3fc-3761a0d54f98": { + "path": "a3106388-6a3d-4b68-a3fc-3761a0d54f98", + "cellName": "Flow 2: CSV Export - Controller Handles Request - monitorController.js:L427-434", + "cellId": "a3106388-6a3d-4b68-a3fc-3761a0d54f98", + "visible": true, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261" + }, + "server/src/controllers/v1/monitorController.js-simstep-d9db3f40-8add-4551-9323-274348b9800f": { + "path": "server/src/controllers/v1/monitorController.js-simstep-d9db3f40-8add-4551-9323-274348b9800f", + "fileName": "monitorController.js", + "wiki": "The `exportMonitorsToCSV` method in the monitor controller is invoked. It retrieves the `teamId` from the authenticated user's session and calls the monitor service to perform the export.", + "cellName": "Flow 2: CSV Export - Controller Handles Request - monitorController.js:L427-434", + "cellId": "a3106388-6a3d-4b68-a3fc-3761a0d54f98", + "visible": true, + "startLine": 427, + "endLine": 434, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261", + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "d9db3f40-8add-4551-9323-274348b9800f" + } + ] + }, + "ab9cb862-0ac7-45a3-9888-f64e87bdda54": { + "path": "ab9cb862-0ac7-45a3-9888-f64e87bdda54", + "cellName": "Flow 2: CSV Export - Generate CSV - monitorService.js:L245-266", + "cellId": "ab9cb862-0ac7-45a3-9888-f64e87bdda54", + "visible": true, + "parentCellId": "b4e479df-9b21-4430-9383-84831f503c2e" + }, + "server/src/service/v1/business/monitorService.js-simstep-85cd1d5d-7ffb-49c5-aacb-907048003fc5": { + "path": "server/src/service/v1/business/monitorService.js-simstep-85cd1d5d-7ffb-49c5-aacb-907048003fc5", + "fileName": "monitorService.js", + "wiki": "The monitor service fetches all monitors for the given `teamId` from the database. It then maps the data into the required format and uses `papaparse` to convert the JSON array into a CSV-formatted string.", + "cellName": "Flow 2: CSV Export - Generate CSV - monitorService.js:L245-266", + "cellId": "ab9cb862-0ac7-45a3-9888-f64e87bdda54", + "visible": true, + "startLine": 245, + "endLine": 266, + "parentCellId": "b4e479df-9b21-4430-9383-84831f503c2e", + "parentPath": "server/src/service/v1/business/monitorService.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "85cd1d5d-7ffb-49c5-aacb-907048003fc5" + } + ] + }, + "f151f89d-6399-48f0-aecd-5eb1572b48cc": { + "path": "f151f89d-6399-48f0-aecd-5eb1572b48cc", + "cellName": "Flow 2: CSV Export - Send File Response - monitorController.js:L434-440", + "cellId": "f151f89d-6399-48f0-aecd-5eb1572b48cc", + "visible": true, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261" + }, + "server/src/controllers/v1/monitorController.js-simstep-256095b8-5e3a-4c20-abd9-be9e6a9d0bad": { + "path": "server/src/controllers/v1/monitorController.js-simstep-256095b8-5e3a-4c20-abd9-be9e6a9d0bad", + "fileName": "monitorController.js", + "wiki": "The controller receives the CSV string and uses the `res.file` method to send it back to the client as a file. It sets the `Content-Type` to `text/csv` and `Content-Disposition` to trigger a download with the filename `monitors.csv`.", + "cellName": "Flow 2: CSV Export - Send File Response - monitorController.js:L434-440", + "cellId": "f151f89d-6399-48f0-aecd-5eb1572b48cc", + "visible": true, + "startLine": 434, + "endLine": 440, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261", + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "256095b8-5e3a-4c20-abd9-be9e6a9d0bad" + } + ] + }, + "d8877474-7203-484f-bd47-fc69e3c7c482": { + "path": "d8877474-7203-484f-bd47-fc69e3c7c482", + "cellName": "Flow 2: CSV Export - Browser Prompts Download - monitorController.js:L434-440", + "cellId": "d8877474-7203-484f-bd47-fc69e3c7c482", + "visible": true, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261" + }, + "server/src/controllers/v1/monitorController.js-simstep-60abdb27-4078-4678-a010-0f6938d4be48": { + "path": "server/src/controllers/v1/monitorController.js-simstep-60abdb27-4078-4678-a010-0f6938d4be48", + "fileName": "monitorController.js", + "wiki": "The browser receives the server's response. The `Content-Disposition` header instructs the browser to prompt the user to save the received data as a file named `monitors.csv`, completing the export process.", + "cellName": "Flow 2: CSV Export - Browser Prompts Download - monitorController.js:L434-440", + "cellId": "d8877474-7203-484f-bd47-fc69e3c7c482", + "visible": true, + "startLine": 434, + "endLine": 440, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261", + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "60abdb27-4078-4678-a010-0f6938d4be48" + } + ] + }, + "c9a012bc-d53f-494f-82f2-1ee3f40ce646": { + "path": "c9a012bc-d53f-494f-82f2-1ee3f40ce646", + "cellName": "Flow 1:\nCSV Import\n- Render\nUI", + "cellId": "c9a012bc-d53f-494f-82f2-1ee3f40ce646", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "generated-edge-simstep-ef0bbcff-9535-4c0e-a5ca-96c457f89139-c9a012bc-d53f-494f-82f2-1ee3f40ce646": { + "path": "generated-edge-simstep-ef0bbcff-9535-4c0e-a5ca-96c457f89139-c9a012bc-d53f-494f-82f2-1ee3f40ce646", + "fileName": "index.jsx", + "cellName": "Flow 1: CSV Import - Render UI", + "cellId": "c9a012bc-d53f-494f-82f2-1ee3f40ce646", + "visible": true, + "startLine": 16, + "endLine": 50, + "parentPath": "client/src/Routes/index.jsx", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "ef0bbcff-9535-4c0e-a5ca-96c457f89139" + } + ] + }, + "e03a8ce2-c2cd-4147-8bab-63d6c2cf0a48": { + "path": "e03a8ce2-c2cd-4147-8bab-63d6c2cf0a48", + "cellName": "Flow 1:\nCSV Import\n- File\nState Update", + "cellId": "e03a8ce2-c2cd-4147-8bab-63d6c2cf0a48", + "visible": true, + "parentCellId": "56dae13e-5735-4089-a9a4-e18be315aff7" + }, + "generated-edge-simstep-610120d2-0521-4444-b245-feb97239a389-e03a8ce2-c2cd-4147-8bab-63d6c2cf0a48": { + "path": "generated-edge-simstep-610120d2-0521-4444-b245-feb97239a389-e03a8ce2-c2cd-4147-8bab-63d6c2cf0a48", + "fileName": "Upload.jsx", + "cellName": "Flow 1: CSV Import - File State Update", + "cellId": "e03a8ce2-c2cd-4147-8bab-63d6c2cf0a48", + "visible": true, + "startLine": 22, + "endLine": 22, + "parentPath": "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "610120d2-0521-4444-b245-feb97239a389" + } + ] + }, + "b8c0292d-afbb-4648-b484-1c27be96d52f": { + "path": "b8c0292d-afbb-4648-b484-1c27be96d52f", + "cellName": "Flow 1:\nCSV Import\n- Prepare\nForm Data", + "cellId": "b8c0292d-afbb-4648-b484-1c27be96d52f", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "generated-edge-simstep-a365d018-e80c-419c-80b3-5ffc8f14dc5c-b8c0292d-afbb-4648-b484-1c27be96d52f": { + "path": "generated-edge-simstep-a365d018-e80c-419c-80b3-5ffc8f14dc5c-b8c0292d-afbb-4648-b484-1c27be96d52f", + "fileName": "index.jsx", + "cellName": "Flow 1: CSV Import - Prepare Form Data", + "cellId": "b8c0292d-afbb-4648-b484-1c27be96d52f", + "visible": true, + "startLine": 490, + "endLine": 493, + "parentPath": "client/src/Pages/v1/Uptime/BulkImport/index.jsx", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "a365d018-e80c-419c-80b3-5ffc8f14dc5c" + } + ] + }, + "23715cce-c83a-44c0-9853-4c95b85e5ecd": { + "path": "23715cce-c83a-44c0-9853-4c95b85e5ecd", + "cellName": "Flow 1:\nCSV Import\n- Transmit\nData to\nServer", + "cellId": "23715cce-c83a-44c0-9853-4c95b85e5ecd", + "visible": true + }, + "generated-edge-simstep-e6dec1dc-33cb-4669-a3b0-7a3c0b233185-23715cce-c83a-44c0-9853-4c95b85e5ecd": { + "path": "generated-edge-simstep-e6dec1dc-33cb-4669-a3b0-7a3c0b233185-23715cce-c83a-44c0-9853-4c95b85e5ecd", + "fileName": "NetworkService.js", + "cellName": "Flow 1: CSV Import - Transmit Data to Server", + "cellId": "23715cce-c83a-44c0-9853-4c95b85e5ecd", + "visible": true, + "startLine": 46, + "endLine": 46, + "parentPath": "client/src/Utils/NetworkService.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "e6dec1dc-33cb-4669-a3b0-7a3c0b233185" + } + ] + }, + "20d0e690-a569-4c6f-8238-916cac920553": { + "path": "20d0e690-a569-4c6f-8238-916cac920553", + "cellName": "Flow 1:\nCSV Import\n- Route\nto Controller", + "cellId": "20d0e690-a569-4c6f-8238-916cac920553", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-c53d3f62-73fe-4fad-9835-ef2c8cf52894-20d0e690-a569-4c6f-8238-916cac920553": { + "path": "generated-edge-simstep-c53d3f62-73fe-4fad-9835-ef2c8cf52894-20d0e690-a569-4c6f-8238-916cac920553", + "fileName": "monitorRoute.js", + "cellName": "Flow 1: CSV Import - Route to Controller", + "cellId": "20d0e690-a569-4c6f-8238-916cac920553", + "visible": true, + "startLine": 206, + "endLine": 212, + "parentPath": "server/src/routes/v1/monitorRoute.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "c53d3f62-73fe-4fad-9835-ef2c8cf52894" + } + ] + }, + "7292bf6e-431d-4a9d-94ce-2bcfabcaa79d": { + "path": "7292bf6e-431d-4a9d-94ce-2bcfabcaa79d", + "cellName": "Flow 1:\nCSV Import\n- Controller\nto Service", + "cellId": "7292bf6e-431d-4a9d-94ce-2bcfabcaa79d", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-6038422c-ab83-4fc6-a58b-c75734276118-7292bf6e-431d-4a9d-94ce-2bcfabcaa79d": { + "path": "generated-edge-simstep-6038422c-ab83-4fc6-a58b-c75734276118-7292bf6e-431d-4a9d-94ce-2bcfabcaa79d", + "fileName": "monitorController.js", + "cellName": "Flow 1: CSV Import - Controller to Service", + "cellId": "7292bf6e-431d-4a9d-94ce-2bcfabcaa79d", + "visible": true, + "startLine": 84, + "endLine": 84, + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "6038422c-ab83-4fc6-a58b-c75734276118" + } + ] + }, + "b55a0dc6-f596-4e5f-9265-63588b46b519": { + "path": "b55a0dc6-f596-4e5f-9265-63588b46b519", + "cellName": "Flow 1:\nCSV Import\n- Service\nto Database\nModule", + "cellId": "b55a0dc6-f596-4e5f-9265-63588b46b519", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-0115fb69-4959-4b32-bd90-1dcd3c2231f6-b55a0dc6-f596-4e5f-9265-63588b46b519": { + "path": "generated-edge-simstep-0115fb69-4959-4b32-bd90-1dcd3c2231f6-b55a0dc6-f596-4e5f-9265-63588b46b519", + "fileName": "monitorService.js", + "cellName": "Flow 1: CSV Import - Service to Database Module", + "cellId": "b55a0dc6-f596-4e5f-9265-63588b46b519", + "visible": true, + "startLine": 126, + "endLine": 126, + "parentPath": "server/src/service/v1/business/monitorService.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "0115fb69-4959-4b32-bd90-1dcd3c2231f6" + } + ] + }, + "f27c60ef-fb40-43e2-9750-c2617bdeaf43": { + "path": "f27c60ef-fb40-43e2-9750-c2617bdeaf43", + "cellName": "Flow 1:\nCSV Import\n- Database\nto Service", + "cellId": "f27c60ef-fb40-43e2-9750-c2617bdeaf43", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-2ee7bd94-d131-4dd8-bdf8-68433d00472d-f27c60ef-fb40-43e2-9750-c2617bdeaf43": { + "path": "generated-edge-simstep-2ee7bd94-d131-4dd8-bdf8-68433d00472d-f27c60ef-fb40-43e2-9750-c2617bdeaf43", + "fileName": "monitorModule.js", + "cellName": "Flow 1: CSV Import - Database to Service", + "cellId": "f27c60ef-fb40-43e2-9750-c2617bdeaf43", + "visible": true, + "startLine": 467, + "endLine": 467, + "parentPath": "server/src/db/v1/modules/monitorModule.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "2ee7bd94-d131-4dd8-bdf8-68433d00472d" + } + ] + }, + "c449f81a-d6af-4f7a-b2d8-050a9a9ee0f9": { + "path": "c449f81a-d6af-4f7a-b2d8-050a9a9ee0f9", + "cellName": "Flow 1:\nCSV Import\n- Service\nto Controller", + "cellId": "c449f81a-d6af-4f7a-b2d8-050a9a9ee0f9", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-94d5772d-3f46-4d43-b764-3493db5c4971-c449f81a-d6af-4f7a-b2d8-050a9a9ee0f9": { + "path": "generated-edge-simstep-94d5772d-3f46-4d43-b764-3493db5c4971-c449f81a-d6af-4f7a-b2d8-050a9a9ee0f9", + "fileName": "monitorService.js", + "cellName": "Flow 1: CSV Import - Service to Controller", + "cellId": "c449f81a-d6af-4f7a-b2d8-050a9a9ee0f9", + "visible": true, + "startLine": 230, + "endLine": 230, + "parentPath": "server/src/service/v1/business/monitorService.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "94d5772d-3f46-4d43-b764-3493db5c4971" + } + ] + }, + "926a330c-a9c5-4983-a4b5-70359813e49a": { + "path": "926a330c-a9c5-4983-a4b5-70359813e49a", + "cellName": "Flow 1:\nCSV Import\n- Transmit\nSuccess to\nClient", + "cellId": "926a330c-a9c5-4983-a4b5-70359813e49a", + "visible": true + }, + "generated-edge-simstep-c60a3adb-dd81-41b0-95e4-a0a6d23f9dd0-926a330c-a9c5-4983-a4b5-70359813e49a": { + "path": "generated-edge-simstep-c60a3adb-dd81-41b0-95e4-a0a6d23f9dd0-926a330c-a9c5-4983-a4b5-70359813e49a", + "fileName": "monitorController.js", + "cellName": "Flow 1: CSV Import - Transmit Success to Client", + "cellId": "926a330c-a9c5-4983-a4b5-70359813e49a", + "visible": true, + "startLine": 494, + "endLine": 494, + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "c60a3adb-dd81-41b0-95e4-a0a6d23f9dd0" + } + ] + }, + "febea87e-5033-479c-ba7a-eb35d98db899": { + "path": "febea87e-5033-479c-ba7a-eb35d98db899", + "cellName": "Flow 2:\nCSV Export\n- Transmit\nRequest to\nServer", + "cellId": "febea87e-5033-479c-ba7a-eb35d98db899", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-53cebb8e-cf45-49eb-a7d3-98fd87af21b4-febea87e-5033-479c-ba7a-eb35d98db899": { + "path": "generated-edge-simstep-53cebb8e-cf45-49eb-a7d3-98fd87af21b4-febea87e-5033-479c-ba7a-eb35d98db899", + "fileName": "monitorRoute.js", + "cellName": "Flow 2: CSV Export - Transmit Request to Server", + "cellId": "febea87e-5033-479c-ba7a-eb35d98db899", + "visible": true, + "startLine": 45, + "endLine": 45, + "parentPath": "server/src/routes/v1/monitorRoute.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "53cebb8e-cf45-49eb-a7d3-98fd87af21b4" + } + ] + }, + "3e0f0966-5c2f-41af-9bcc-e8f615e03a00": { + "path": "3e0f0966-5c2f-41af-9bcc-e8f615e03a00", + "cellName": "Flow 2:\nCSV Export\n- Controller\nto Service", + "cellId": "3e0f0966-5c2f-41af-9bcc-e8f615e03a00", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-74e95fb5-863c-4137-a41b-d7a32efa04a5-3e0f0966-5c2f-41af-9bcc-e8f615e03a00": { + "path": "generated-edge-simstep-74e95fb5-863c-4137-a41b-d7a32efa04a5-3e0f0966-5c2f-41af-9bcc-e8f615e03a00", + "fileName": "monitorController.js", + "cellName": "Flow 2: CSV Export - Controller to Service", + "cellId": "3e0f0966-5c2f-41af-9bcc-e8f615e03a00", + "visible": true, + "startLine": 245, + "endLine": 245, + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "74e95fb5-863c-4137-a41b-d7a32efa04a5" + } + ] + }, + "ad300b1c-6748-40f9-a7c9-5895e0915991": { + "path": "ad300b1c-6748-40f9-a7c9-5895e0915991", + "cellName": "Flow 2:\nCSV Export\n- Service\nto Controller", + "cellId": "ad300b1c-6748-40f9-a7c9-5895e0915991", + "visible": true, + "parentCellId": "35864a2c-7bd0-4428-84fc-c0aa90c7f1be" + }, + "generated-edge-simstep-4deb5e47-2ac4-4638-9ee6-48295d027bcd-ad300b1c-6748-40f9-a7c9-5895e0915991": { + "path": "generated-edge-simstep-4deb5e47-2ac4-4638-9ee6-48295d027bcd-ad300b1c-6748-40f9-a7c9-5895e0915991", + "fileName": "monitorService.js", + "cellName": "Flow 2: CSV Export - Service to Controller", + "cellId": "ad300b1c-6748-40f9-a7c9-5895e0915991", + "visible": true, + "startLine": 432, + "endLine": 432, + "parentPath": "server/src/service/v1/business/monitorService.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "4deb5e47-2ac4-4638-9ee6-48295d027bcd" + } + ] + }, + "3dfb432a-2ce4-4f92-98ce-5d114b5f7f03": { + "path": "3dfb432a-2ce4-4f92-98ce-5d114b5f7f03", + "cellName": "Flow 2:\nCSV Export\n- Transmit\nFile to\nClient", + "cellId": "3dfb432a-2ce4-4f92-98ce-5d114b5f7f03", + "visible": true, + "parentCellId": "341f2784-67ee-4477-9445-a8c240ee6261" + }, + "generated-edge-simstep-605f4cbd-1f7f-4e89-b028-f18737ac7fb1-3dfb432a-2ce4-4f92-98ce-5d114b5f7f03": { + "path": "generated-edge-simstep-605f4cbd-1f7f-4e89-b028-f18737ac7fb1-3dfb432a-2ce4-4f92-98ce-5d114b5f7f03", + "fileName": "monitorController.js", + "cellName": "Flow 2: CSV Export - Transmit File to Client", + "cellId": "3dfb432a-2ce4-4f92-98ce-5d114b5f7f03", + "visible": true, + "startLine": 434, + "endLine": 440, + "parentPath": "server/src/controllers/v1/monitorController.js", + "simSteps": [ + { + "simulationKey": "Bulk Monitor Management", + "simStepId": "605f4cbd-1f7f-4e89-b028-f18737ac7fb1" + } + ] + }, + "50f77798-f41b-4f5b-b096-7afc921a0419": { + "path": "50f77798-f41b-4f5b-b096-7afc921a0419", + "cellName": "Settings", + "cellId": "50f77798-f41b-4f5b-b096-7afc921a0419", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679" + }, + "1716ec41-8b42-4386-9850-55b8a607638e": { + "path": "1716ec41-8b42-4386-9850-55b8a607638e", + "cellName": "Logs", + "cellId": "1716ec41-8b42-4386-9850-55b8a607638e", + "visible": true, + "parentCellId": "1a083b4a-787c-4eb4-8029-6700de987679" + }, + "8ee5d524-e61d-4e39-a729-babf54929def": { + "path": "8ee5d524-e61d-4e39-a729-babf54929def", + "cellName": "settingsHooks.js", + "cellId": "8ee5d524-e61d-4e39-a729-babf54929def", + "visible": true, + "parentCellId": "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961" + }, + "a1fea5b0-fb7c-4189-9484-9d3312660767": { + "path": "a1fea5b0-fb7c-4189-9484-9d3312660767", + "cellName": "settingsController.js", + "cellId": "a1fea5b0-fb7c-4189-9484-9d3312660767", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "5882419e-8a92-4edd-a71a-1252983f9af7": { + "path": "5882419e-8a92-4edd-a71a-1252983f9af7", + "cellName": "diagnosticController.js", + "cellId": "5882419e-8a92-4edd-a71a-1252983f9af7", + "visible": true, + "parentCellId": "fc591f7a-d4ac-4565-949d-a87a58a79bbc" + }, + "d4b34195-7e1e-47a0-9303-603987c81f95": { + "path": "d4b34195-7e1e-47a0-9303-603987c81f95", + "cellName": "index.jsx", + "cellId": "d4b34195-7e1e-47a0-9303-603987c81f95", + "visible": true, + "parentCellId": "50f77798-f41b-4f5b-b096-7afc921a0419" + }, + "d8aa5b9e-f2a7-44c8-8813-d79c3f3238ee": { + "path": "d8aa5b9e-f2a7-44c8-8813-d79c3f3238ee", + "cellName": "SettingsEmail.jsx", + "cellId": "d8aa5b9e-f2a7-44c8-8813-d79c3f3238ee", + "visible": true, + "parentCellId": "50f77798-f41b-4f5b-b096-7afc921a0419" + }, + "e0cb598c-e300-4fd3-8554-db7ffd6201f1": { + "path": "e0cb598c-e300-4fd3-8554-db7ffd6201f1", + "cellName": "Diagnostics", + "cellId": "e0cb598c-e300-4fd3-8554-db7ffd6201f1", + "visible": true, + "parentCellId": "1716ec41-8b42-4386-9850-55b8a607638e" + }, + "643b26c1-3bfb-4c41-8179-96dfc237d99e": { + "path": "643b26c1-3bfb-4c41-8179-96dfc237d99e", + "cellName": "diagnosticService.js", + "cellId": "643b26c1-3bfb-4c41-8179-96dfc237d99e", + "visible": true, + "parentCellId": "87d3b101-9443-4ed4-9c4f-f759d41677eb" + }, + "87e0e2ea-4a6d-460d-95bc-31e2efd9c9cf": { + "path": "87e0e2ea-4a6d-460d-95bc-31e2efd9c9cf", + "cellName": "index.jsx", + "cellId": "87e0e2ea-4a6d-460d-95bc-31e2efd9c9cf", + "visible": true, + "parentCellId": "e0cb598c-e300-4fd3-8554-db7ffd6201f1" + }, + "97908daf-d071-4df5-8366-1f5f4d8d534a": { + "path": "97908daf-d071-4df5-8366-1f5f4d8d534a", + "cellName": "Flow 1: Render Settings Page - index.jsx:L25-52", + "cellId": "97908daf-d071-4df5-8366-1f5f4d8d534a", + "visible": true, + "parentCellId": "d4b34195-7e1e-47a0-9303-603987c81f95" + }, + "client/src/Pages/v1/Settings/index.jsx-simstep-64e9f1af-13b7-4361-a2a9-3f45760e316d": { + "path": "client/src/Pages/v1/Settings/index.jsx-simstep-64e9f1af-13b7-4361-a2a9-3f45760e316d", + "fileName": "index.jsx", + "wiki": "The user navigates to the settings page. The main `Settings` component fetches existing settings using the `useFetchSettings` hook and renders various sub-components for different configuration areas like TimeZone, UI, PageSpeed, Email, etc.", + "cellName": "Flow 1: Render Settings Page - index.jsx:L25-52", + "cellId": "97908daf-d071-4df5-8366-1f5f4d8d534a", + "visible": true, + "startLine": 25, + "endLine": 52, + "parentCellId": "d4b34195-7e1e-47a0-9303-603987c81f95", + "parentPath": "client/src/Pages/v1/Settings/index.jsx", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "64e9f1af-13b7-4361-a2a9-3f45760e316d" + } + ] + }, + "83596bd1-dfca-4196-b52e-955f6824fafb": { + "path": "83596bd1-dfca-4196-b52e-955f6824fafb", + "cellName": "Backend: Retrieve Settings from Database - settingsController.js:L40-49", + "cellId": "83596bd1-dfca-4196-b52e-955f6824fafb", + "visible": true, + "parentCellId": "a1fea5b0-fb7c-4189-9484-9d3312660767" + }, + "server/src/controllers/v1/settingsController.js-simstep-441b0c45-5f27-478d-8ca8-58fbd5f460ea": { + "path": "server/src/controllers/v1/settingsController.js-simstep-441b0c45-5f27-478d-8ca8-58fbd5f460ea", + "fileName": "settingsController.js", + "wiki": "The `settingsController` handles the GET request. It calls the `settingsService` to fetch settings from the database. Sensitive data like passwords is sanitized, and flags like `emailPasswordSet` are added before sending the data to the client.", + "cellName": "Backend: Retrieve Settings from Database - settingsController.js:L40-49", + "cellId": "83596bd1-dfca-4196-b52e-955f6824fafb", + "visible": true, + "startLine": 40, + "endLine": 49, + "parentCellId": "a1fea5b0-fb7c-4189-9484-9d3312660767", + "parentPath": "server/src/controllers/v1/settingsController.js", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "441b0c45-5f27-478d-8ca8-58fbd5f460ea" + } + ] + }, + "09cb194a-0ea4-46e2-b32b-36b62fa87f40": { + "path": "09cb194a-0ea4-46e2-b32b-36b62fa87f40", + "cellName": "User Interaction: Modify Email Settings - SettingsEmail.jsx:L119-167", + "cellId": "09cb194a-0ea4-46e2-b32b-36b62fa87f40", + "visible": true, + "parentCellId": "d8aa5b9e-f2a7-44c8-8813-d79c3f3238ee" + }, + "client/src/Pages/v1/Settings/SettingsEmail.jsx-simstep-ae5c5146-4dbb-4612-9d23-086e57f72474": { + "path": "client/src/Pages/v1/Settings/SettingsEmail.jsx-simstep-ae5c5146-4dbb-4612-9d23-086e57f72474", + "fileName": "SettingsEmail.jsx", + "wiki": "The user interacts with the `SettingsEmail` component, filling in SMTP server details. The component's state is updated via the `handleChange` function passed down from the parent `Settings` component, which prepares the new configuration data.", + "cellName": "User Interaction: Modify Email Settings - SettingsEmail.jsx:L119-167", + "cellId": "09cb194a-0ea4-46e2-b32b-36b62fa87f40", + "visible": true, + "startLine": 119, + "endLine": 167, + "parentCellId": "d8aa5b9e-f2a7-44c8-8813-d79c3f3238ee", + "parentPath": "client/src/Pages/v1/Settings/SettingsEmail.jsx", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "ae5c5146-4dbb-4612-9d23-086e57f72474" + } + ] + }, + "1b2cf182-9ee3-4218-a7ec-b74e427ccddf": { + "path": "1b2cf182-9ee3-4218-a7ec-b74e427ccddf", + "cellName": "API Call: Update Application Settings - settingsHooks.js:L41-67", + "cellId": "1b2cf182-9ee3-4218-a7ec-b74e427ccddf", + "visible": true, + "parentCellId": "8ee5d524-e61d-4e39-a729-babf54929def" + }, + "client/src/Hooks/v1/settingsHooks.js-simstep-d7098072-9a83-4369-8176-9646ba34e186": { + "path": "client/src/Hooks/v1/settingsHooks.js-simstep-d7098072-9a83-4369-8176-9646ba34e186", + "fileName": "settingsHooks.js", + "wiki": "The `handleSave` function calls `saveSettings` from the `useSaveSettings` hook. This triggers a PUT request to `/api/v1/settings` with the new configuration in the request body.", + "cellName": "API Call: Update Application Settings - settingsHooks.js:L41-67", + "cellId": "1b2cf182-9ee3-4218-a7ec-b74e427ccddf", + "visible": true, + "startLine": 41, + "endLine": 67, + "parentCellId": "8ee5d524-e61d-4e39-a729-babf54929def", + "parentPath": "client/src/Hooks/v1/settingsHooks.js", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "d7098072-9a83-4369-8176-9646ba34e186" + } + ] + }, + "cce3b9c8-f3b7-434b-b3c6-d8d164714f86": { + "path": "cce3b9c8-f3b7-434b-b3c6-d8d164714f86", + "cellName": "Backend: Persist Updated Settings - settingsController.js:L54-64", + "cellId": "cce3b9c8-f3b7-434b-b3c6-d8d164714f86", + "visible": true, + "parentCellId": "a1fea5b0-fb7c-4189-9484-9d3312660767" + }, + "server/src/controllers/v1/settingsController.js-simstep-7632e8d2-4086-4169-ae3f-bfbf8d657987": { + "path": "server/src/controllers/v1/settingsController.js-simstep-7632e8d2-4086-4169-ae3f-bfbf8d657987", + "fileName": "settingsController.js", + "wiki": "The `settingsController` receives the PUT request, validates the body, and calls the `db.settingsModule.updateAppSettings` method to update the settings document in the database.", + "cellName": "Backend: Persist Updated Settings - settingsController.js:L54-64", + "cellId": "cce3b9c8-f3b7-434b-b3c6-d8d164714f86", + "visible": true, + "startLine": 54, + "endLine": 64, + "parentCellId": "a1fea5b0-fb7c-4189-9484-9d3312660767", + "parentPath": "server/src/controllers/v1/settingsController.js", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "7632e8d2-4086-4169-ae3f-bfbf8d657987" + } + ] + }, + "fff5dde4-8eb8-4c21-9c6d-0845a9749ff3": { + "path": "fff5dde4-8eb8-4c21-9c6d-0845a9749ff3", + "cellName": "UI Update: Display Success Confirmation - settingsHooks.js:L58", + "cellId": "fff5dde4-8eb8-4c21-9c6d-0845a9749ff3", + "visible": true, + "parentCellId": "8ee5d524-e61d-4e39-a729-babf54929def" + }, + "client/src/Hooks/v1/settingsHooks.js-simstep-a157bd07-b969-44f8-914c-3d8393c1605a": { + "path": "client/src/Hooks/v1/settingsHooks.js-simstep-a157bd07-b969-44f8-914c-3d8393c1605a", + "fileName": "settingsHooks.js", + "wiki": "The frontend network service receives the successful API response. The `saveSettings` hook then calls `createToast` with a success message, which is displayed to the user.", + "cellName": "UI Update: Display Success Confirmation - settingsHooks.js:L58", + "cellId": "fff5dde4-8eb8-4c21-9c6d-0845a9749ff3", + "visible": true, + "startLine": 58, + "endLine": 58, + "parentCellId": "8ee5d524-e61d-4e39-a729-babf54929def", + "parentPath": "client/src/Hooks/v1/settingsHooks.js", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "a157bd07-b969-44f8-914c-3d8393c1605a" + } + ] + }, + "704e2ec8-9f2f-47e5-852f-c6ada67a02e0": { + "path": "704e2ec8-9f2f-47e5-852f-c6ada67a02e0", + "cellName": "Flow 2: Navigate to Diagnostics Page - index.jsx:L14-15", + "cellId": "704e2ec8-9f2f-47e5-852f-c6ada67a02e0", + "visible": true, + "parentCellId": "87e0e2ea-4a6d-460d-95bc-31e2efd9c9cf" + }, + "client/src/Pages/v1/Logs/Diagnostics/index.jsx-simstep-a9b24b5f-534b-4baf-a185-bac7c32a1c42": { + "path": "client/src/Pages/v1/Logs/Diagnostics/index.jsx-simstep-a9b24b5f-534b-4baf-a185-bac7c32a1c42", + "fileName": "index.jsx", + "wiki": "The user navigates to the 'Logs' page and selects the 'Diagnostics' tab. The `Diagnostics` component renders and immediately calls the `useFetchDiagnostics` hook to retrieve system health data.", + "cellName": "Flow 2: Navigate to Diagnostics Page - index.jsx:L14-15", + "cellId": "704e2ec8-9f2f-47e5-852f-c6ada67a02e0", + "visible": true, + "startLine": 14, + "endLine": 15, + "parentCellId": "87e0e2ea-4a6d-460d-95bc-31e2efd9c9cf", + "parentPath": "client/src/Pages/v1/Logs/Diagnostics/index.jsx", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "a9b24b5f-534b-4baf-a185-bac7c32a1c42" + } + ] + }, + "09f07e80-185e-4547-aab4-edaf8ae7501e": { + "path": "09f07e80-185e-4547-aab4-edaf8ae7501e", + "cellName": "Backend: Gather System Statistics - diagnosticService.js:L39-95", + "cellId": "09f07e80-185e-4547-aab4-edaf8ae7501e", + "visible": true, + "parentCellId": "643b26c1-3bfb-4c41-8179-96dfc237d99e" + }, + "server/src/service/v1/business/diagnosticService.js-simstep-8fb704ca-67d9-437c-90d8-01302634d568": { + "path": "server/src/service/v1/business/diagnosticService.js-simstep-8fb704ca-67d9-437c-90d8-01302634d568", + "fileName": "diagnosticService.js", + "wiki": "The `diagnosticController` receives the request and calls `diagnosticService.getSystemStats()`. This service uses native Node.js modules like `os` and `v8` to collect metrics on memory, CPU, heap statistics, and process uptime.", + "cellName": "Backend: Gather System Statistics - diagnosticService.js:L39-95", + "cellId": "09f07e80-185e-4547-aab4-edaf8ae7501e", + "visible": true, + "startLine": 39, + "endLine": 95, + "parentCellId": "643b26c1-3bfb-4c41-8179-96dfc237d99e", + "parentPath": "server/src/service/v1/business/diagnosticService.js", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "8fb704ca-67d9-437c-90d8-01302634d568" + } + ] + }, + "4b418736-6b72-47e0-917a-d86d4bed9ecd": { + "path": "4b418736-6b72-47e0-917a-d86d4bed9ecd", + "cellName": "UI Update: Display System Health - index.jsx:L59-63", + "cellId": "4b418736-6b72-47e0-917a-d86d4bed9ecd", + "visible": true, + "parentCellId": "87e0e2ea-4a6d-460d-95bc-31e2efd9c9cf" + }, + "client/src/Pages/v1/Logs/Diagnostics/index.jsx-simstep-e6c94f52-a599-4568-a06f-2db18f963817": { + "path": "client/src/Pages/v1/Logs/Diagnostics/index.jsx-simstep-e6c94f52-a599-4568-a06f-2db18f963817", + "fileName": "index.jsx", + "wiki": "The frontend receives the diagnostic data. The `Diagnostics` component passes this data to its children, `Gauges` and `StatusBoxes`, which then render the information as interactive gauges and statistical boxes.", + "cellName": "UI Update: Display System Health - index.jsx:L59-63", + "cellId": "4b418736-6b72-47e0-917a-d86d4bed9ecd", + "visible": true, + "startLine": 59, + "endLine": 63, + "parentCellId": "87e0e2ea-4a6d-460d-95bc-31e2efd9c9cf", + "parentPath": "client/src/Pages/v1/Logs/Diagnostics/index.jsx", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "e6c94f52-a599-4568-a06f-2db18f963817" + } + ] + }, + "f6d80f09-ae83-4c95-aba0-4efffeda38e0": { + "path": "f6d80f09-ae83-4c95-aba0-4efffeda38e0", + "cellName": "API Call:\nFetch Application\nSettings", + "cellId": "f6d80f09-ae83-4c95-aba0-4efffeda38e0", + "visible": true + }, + "generated-edge-simstep-dca4a33c-b666-47b8-896b-370ed9d6f9c5-f6d80f09-ae83-4c95-aba0-4efffeda38e0": { + "path": "generated-edge-simstep-dca4a33c-b666-47b8-896b-370ed9d6f9c5-f6d80f09-ae83-4c95-aba0-4efffeda38e0", + "fileName": "index.jsx", + "cellName": "API Call: Fetch Application Settings", + "cellId": "f6d80f09-ae83-4c95-aba0-4efffeda38e0", + "visible": true, + "startLine": 692, + "endLine": 698, + "parentPath": "client/src/Pages/v1/Settings/index.jsx", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "dca4a33c-b666-47b8-896b-370ed9d6f9c5" + } + ] + }, + "195ad532-9858-493e-876a-6ec457811d1d": { + "path": "195ad532-9858-493e-876a-6ec457811d1d", + "cellName": "API Response:\nSend Settings\nto Client", + "cellId": "195ad532-9858-493e-876a-6ec457811d1d", + "visible": true + }, + "generated-edge-simstep-aacc9302-6ca8-45c6-acb8-956d8d0242c2-195ad532-9858-493e-876a-6ec457811d1d": { + "path": "generated-edge-simstep-aacc9302-6ca8-45c6-acb8-956d8d0242c2-195ad532-9858-493e-876a-6ec457811d1d", + "fileName": "settingsController.js", + "cellName": "API Response: Send Settings to Client", + "cellId": "195ad532-9858-493e-876a-6ec457811d1d", + "visible": true, + "startLine": 45, + "endLine": 48, + "parentPath": "server/src/controllers/v1/settingsController.js", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "aacc9302-6ca8-45c6-acb8-956d8d0242c2" + } + ] + }, + "8c107d79-f796-49eb-ab88-f553dd6f9d95": { + "path": "8c107d79-f796-49eb-ab88-f553dd6f9d95", + "cellName": "User Action:\nSave Settings", + "cellId": "8c107d79-f796-49eb-ab88-f553dd6f9d95", + "visible": true, + "parentCellId": "d3264577-10f0-42fe-9411-7e9df0754d1e" + }, + "generated-edge-simstep-e7e82a97-1409-4bc5-ae27-b4ff5ba746df-8c107d79-f796-49eb-ab88-f553dd6f9d95": { + "path": "generated-edge-simstep-e7e82a97-1409-4bc5-ae27-b4ff5ba746df-8c107d79-f796-49eb-ab88-f553dd6f9d95", + "fileName": "SettingsEmail.jsx", + "cellName": "User Action: Save Settings", + "cellId": "8c107d79-f796-49eb-ab88-f553dd6f9d95", + "visible": true, + "startLine": 258, + "endLine": 261, + "parentPath": "client/src/Pages/v1/Settings/SettingsEmail.jsx", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "e7e82a97-1409-4bc5-ae27-b4ff5ba746df" + } + ] + }, + "ce3ed3ba-a35b-4881-8dfd-f8425b9bf812": { + "path": "ce3ed3ba-a35b-4881-8dfd-f8425b9bf812", + "cellName": "Data Transmission:\nSend Updated\nSettings to\nServer", + "cellId": "ce3ed3ba-a35b-4881-8dfd-f8425b9bf812", + "visible": true + }, + "generated-edge-simstep-383387e5-4623-44ab-b2c4-d9ea046f4547-ce3ed3ba-a35b-4881-8dfd-f8425b9bf812": { + "path": "generated-edge-simstep-383387e5-4623-44ab-b2c4-d9ea046f4547-ce3ed3ba-a35b-4881-8dfd-f8425b9bf812", + "fileName": "settingsHooks.js", + "cellName": "Data Transmission: Send Updated Settings to Server", + "cellId": "ce3ed3ba-a35b-4881-8dfd-f8425b9bf812", + "visible": true, + "startLine": 711, + "endLine": 717, + "parentPath": "client/src/Hooks/v1/settingsHooks.js", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "383387e5-4623-44ab-b2c4-d9ea046f4547" + } + ] + }, + "fbe9b160-307e-4f51-8f72-713fda5373c8": { + "path": "fbe9b160-307e-4f51-8f72-713fda5373c8", + "cellName": "API Response:\nConfirm Settings\nUpdate", + "cellId": "fbe9b160-307e-4f51-8f72-713fda5373c8", + "visible": true + }, + "generated-edge-simstep-0afcde35-7d51-4939-aae8-bac5ac83c49d-fbe9b160-307e-4f51-8f72-713fda5373c8": { + "path": "generated-edge-simstep-0afcde35-7d51-4939-aae8-bac5ac83c49d-fbe9b160-307e-4f51-8f72-713fda5373c8", + "fileName": "settingsController.js", + "cellName": "API Response: Confirm Settings Update", + "cellId": "fbe9b160-307e-4f51-8f72-713fda5373c8", + "visible": true, + "startLine": 60, + "endLine": 63, + "parentPath": "server/src/controllers/v1/settingsController.js", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "0afcde35-7d51-4939-aae8-bac5ac83c49d" + } + ] + }, + "499dfe1f-77d4-4b26-a12c-45240691109e": { + "path": "499dfe1f-77d4-4b26-a12c-45240691109e", + "cellName": "API Call:\nFetch System\nDiagnostics", + "cellId": "499dfe1f-77d4-4b26-a12c-45240691109e", + "visible": true + }, + "generated-edge-simstep-23b8ad7d-8967-4e48-814c-1a7bf5b4dfac-499dfe1f-77d4-4b26-a12c-45240691109e": { + "path": "generated-edge-simstep-23b8ad7d-8967-4e48-814c-1a7bf5b4dfac-499dfe1f-77d4-4b26-a12c-45240691109e", + "fileName": "index.jsx", + "cellName": "API Call: Fetch System Diagnostics", + "cellId": "499dfe1f-77d4-4b26-a12c-45240691109e", + "visible": true, + "startLine": 1113, + "endLine": 1119, + "parentPath": "client/src/Pages/v1/Logs/Diagnostics/index.jsx", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "23b8ad7d-8967-4e48-814c-1a7bf5b4dfac" + } + ] + }, + "d0f92604-a6fa-4d85-9716-ea8f32db78e0": { + "path": "d0f92604-a6fa-4d85-9716-ea8f32db78e0", + "cellName": "API Response:\nSend Diagnostics\nData to\nClient", + "cellId": "d0f92604-a6fa-4d85-9716-ea8f32db78e0", + "visible": true + }, + "generated-edge-simstep-44a4372f-a903-4c43-a9a5-c7549c875e11-d0f92604-a6fa-4d85-9716-ea8f32db78e0": { + "path": "generated-edge-simstep-44a4372f-a903-4c43-a9a5-c7549c875e11-d0f92604-a6fa-4d85-9716-ea8f32db78e0", + "fileName": "diagnosticService.js", + "cellName": "API Response: Send Diagnostics Data to Client", + "cellId": "d0f92604-a6fa-4d85-9716-ea8f32db78e0", + "visible": true, + "startLine": 52, + "endLine": 55, + "parentPath": "server/src/service/v1/business/diagnosticService.js", + "simSteps": [ + { + "simulationKey": "System Administration and Configuration", + "simStepId": "44a4372f-a903-4c43-a9a5-c7549c875e11" + } + ] + } + }, + "simulations": { + "Comprehensive Uptime Monitoring for Various Services": { + "name": "Comprehensive Uptime Monitoring for Various Services", + "simSteps": [ + { + "simStepId": "ab5de0b9-c70d-492c-98d9-8ddee66284ad", + "diagramNodeId": "7a2da928-a65c-49d8-b0a3-c159a6df2bc5", + "simStepLabel": "Uptime Monitor Creation: Display Form", + "simStepDescription": "The user navigates to the 'Create Uptime Monitor' page. The `UptimeCreate` React component renders the form, allowing the user to select the monitor type (HTTP, Ping, Docker, etc.) and input details like URL, name, and check interval.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/Create/index.jsx", + "startLine": "315", + "endLine": "802", + "relevantVariables": [ + "UptimeCreate", + "monitor", + "setMonitor", + "onChange", + "onSubmit" + ] + }, + "inputDataExample": "{}", + "outputDataExample": "{\n \"type\": \"http\",\n \"statusWindowSize\": 5,\n \"statusWindowThreshold\": 60,\n \"matchMethod\": \"equal\",\n \"interval\": 60000,\n \"ignoreTlsErrors\": false,\n \"notifications\": [],\n \"url\": \"\",\n \"name\": \"\"\n}" + }, + { + "simStepId": "888df086-41bb-469d-9c0a-daf74bdecb9f", + "diagramNodeId": "0e09aa33-d7c7-4c89-9da8-f0937658f4d3", + "simStepLabel": "Uptime Monitor Creation: Form Submission", + "simStepDescription": "After filling the form, the user clicks the 'Create monitor' button, which triggers the `onSubmit` event handler, passing the current state of the form.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/Create/index.jsx", + "startLine": "435", + "endLine": "443", + "relevantVariables": [ + "onSubmit" + ] + }, + "inputDataExample": "{\n \"type\": \"http\",\n \"url\": \"https://google.com\",\n \"name\": \"My Google Monitor\",\n \"interval\": 60000,\n \"notifications\": []\n}", + "outputDataExample": "{\n \"type\": \"http\",\n \"url\": \"https://google.com\",\n \"name\": \"My Google Monitor\",\n \"interval\": 60000,\n \"notifications\": []\n}" + }, + { + "simStepId": "e02ddf57-270e-4f71-b4c4-d5183bb7879a", + "diagramNodeId": "a81f4ae8-41fe-4d1b-95ce-86abcc67b53c", + "simStepLabel": "Uptime Monitor Creation: Handle Form Submission and Validation", + "simStepDescription": "The `onSubmit` function in `UptimeCreate.jsx` is executed. It prevents the default form submission, constructs a `form` object with monitor data, validates it using `monitorValidation`, and then calls the `createMonitor` function from the `useCreateMonitor` hook.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/Create/index.jsx", + "startLine": "170", + "endLine": "248", + "relevantVariables": [ + "onSubmit", + "monitorValidation", + "createMonitor" + ] + }, + "inputDataExample": "{\n \"type\": \"http\",\n \"url\": \"google.com\",\n \"name\": \"My Google Monitor\",\n \"interval\": 60000,\n \"notifications\": []\n}", + "outputDataExample": "{\n \"monitor\": {\n \"url\": \"http://google.com\",\n \"name\": \"My Google Monitor\",\n \"statusWindowSize\": 5,\n \"statusWindowThreshold\": 60,\n \"type\": \"http\",\n \"interval\": 60000,\n \"description\": \"My Google Monitor\",\n \"notifications\": []\n },\n \"redirect\": \"/uptime\"\n}" + }, + { + "simStepId": "3fe0afaa-c673-4b52-a0c4-02a3ffd3a075", + "diagramNodeId": "1f6f04a9-0123-4729-b06a-8f3da6d1b58c", + "simStepLabel": "Uptime Monitor Creation: Call Network Service", + "simStepDescription": "The `createMonitor` function from the `useCreateMonitor` hook is called, which in turn calls `networkService.createMonitor` to send the monitor data to the backend API.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Hooks/v1/monitorHooks.js", + "startLine": "306", + "endLine": "306", + "relevantVariables": [ + "networkService.createMonitor" + ] + }, + "inputDataExample": "{\n \"monitor\": {\n \"url\": \"http://google.com\",\n \"name\": \"My Google Monitor\",\n \"type\": \"http\",\n \"interval\": 60000,\n \"notifications\": []\n }\n}", + "outputDataExample": "{\n \"monitor\": {\n \"url\": \"http://google.com\",\n \"name\": \"My Google Monitor\",\n \"type\": \"http\",\n \"interval\": 60000,\n \"notifications\": []\n }\n}" + }, + { + "simStepId": "14e9986b-c6ce-475e-97ed-da1fa2b6968d", + "diagramNodeId": "a30ac765-c546-4993-988e-8c8cfdb7532f", + "simStepLabel": "Uptime Monitor Creation: Send API Request", + "simStepDescription": "The `createMonitor` method in `NetworkService.js` makes an HTTP POST request to the `/monitors` endpoint on the server, sending the new monitor's data in the request body.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "119", + "endLine": "121", + "relevantVariables": [ + "createMonitor", + "this.axiosInstance.post" + ] + }, + "inputDataExample": "{\n \"monitor\": {\n \"url\": \"http://google.com\",\n \"name\": \"My Google Monitor\",\n \"type\": \"http\",\n \"interval\": 60000,\n \"notifications\": []\n }\n}", + "outputDataExample": "{\n \"status\": 200,\n \"data\": {\n \"_id\": \"60d21b4667d0d8992e610c85\",\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000,\n \"isActive\": true\n }\n}" + }, + { + "simStepId": "13dd0564-44a5-43ea-a163-ce1148cc48e7", + "diagramNodeId": "b6d963d5-c613-407a-9ce9-60a04048dea7", + "simStepLabel": "Uptime Monitor Creation: API Route to Controller", + "simStepDescription": "The server's router receives the POST request on `/api/v1/monitors` and forwards it to the `createMonitor` method in the `MonitorController`.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/monitorRoute.js", + "startLine": "35", + "endLine": "35", + "relevantVariables": [ + "this.router.post", + "this.monitorController.createMonitor" + ] + }, + "inputDataExample": "{\n \"headers\": {\n \"Authorization\": \"Bearer \"\n },\n \"body\": {\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000\n }\n}", + "outputDataExample": "{\n \"headers\": {\n \"Authorization\": \"Bearer \"\n },\n \"body\": {\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000\n }\n}" + }, + { + "simStepId": "38fd069d-9c44-4078-954b-3a0899e9acf4", + "diagramNodeId": "f0409948-75dc-457f-a02d-6b9774258e48", + "simStepLabel": "Uptime Monitor Creation: Controller to Service", + "simStepDescription": "The `MonitorController` extracts the user and team ID from the request and calls the `createMonitor` method in the `MonitorService`, passing along the monitor data from the request body.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/monitorController.js", + "startLine": "195", + "endLine": "195", + "relevantVariables": [ + "this.monitorService.createMonitor" + ] + }, + "inputDataExample": "{\n \"user\": {\n \"_id\": \"user-123\",\n \"teamId\": \"team-abc\"\n },\n \"body\": {\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\"\n }\n}", + "outputDataExample": "{\n \"teamId\": \"team-abc\",\n \"userId\": \"user-123\",\n \"body\": {\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\"\n }\n}" + }, + { + "simStepId": "a6f4c967-b1a8-4804-bc2e-67f47a7b68f9", + "diagramNodeId": "a6bccd1d-4272-4df7-a7c8-b3979e37b8d7", + "simStepLabel": "Uptime Monitor Creation: Service to DB Module", + "simStepDescription": "The `MonitorService` calls the `createMonitor` method of the `monitorModule` to handle the database interaction.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/monitorService.js", + "startLine": "74", + "endLine": "78", + "relevantVariables": [ + "this.db.monitorModule.createMonitor" + ] + }, + "inputDataExample": "{\n \"teamId\": \"team-abc\",\n \"userId\": \"user-123\",\n \"body\": {\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\"\n }\n}", + "outputDataExample": "{\n \"teamId\": \"team-abc\",\n \"userId\": \"user-123\",\n \"body\": {\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\"\n }\n}" + }, + { + "simStepId": "ca3ad309-323a-418d-a171-c16f56fae23c", + "diagramNodeId": "2d407bfd-e4b0-46b8-99e7-121c507d525f", + "simStepLabel": "Uptime Monitor Creation: Save Monitor to Database", + "simStepDescription": "The `monitorModule` creates a new `Monitor` model instance with the provided data and saves it to the MongoDB database. The newly created document, including its generated `_id`, is returned.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/monitorModule.js", + "startLine": "454", + "endLine": "455", + "relevantVariables": [ + "this.Monitor", + "monitor.save" + ] + }, + "inputDataExample": "{\n \"teamId\": \"team-abc\",\n \"userId\": \"user-123\",\n \"body\": {\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\"\n }\n}", + "outputDataExample": "{\n \"_id\": \"60d21b4667d0d8992e610c85\",\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000,\n \"userId\": \"user-123\",\n \"teamId\": \"team-abc\",\n \"isActive\": true,\n \"createdAt\": \"2023-01-01T12:00:00Z\",\n \"updatedAt\": \"2023-01-01T12:00:00Z\"\n}" + }, + { + "simStepId": "71ec03b6-1166-4c5d-9ada-0875a0866e30", + "diagramNodeId": "de320ac1-0949-4297-be7c-b9d7a14e0fec", + "simStepLabel": "Uptime Monitor Creation: Service to Job Queue", + "simStepDescription": "After successfully saving the monitor to the database, the `MonitorService` returns the new monitor object and then calls `jobQueue.addJob` to schedule the first and subsequent checks for this monitor.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/monitorService.js", + "startLine": "80", + "endLine": "80", + "relevantVariables": [ + "this.jobQueue.addJob" + ] + }, + "inputDataExample": "{\n \"_id\": \"60d21b4667d0d8992e610c85\",\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000,\n \"isActive\": true\n}", + "outputDataExample": "{\n \"_id\": \"60d21b4667d0d8992e610c85\",\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000,\n \"isActive\": true\n}" + }, + { + "simStepId": "2702bfca-7611-4562-8d3e-a05af8257871", + "diagramNodeId": "51e8db5f-2fdd-4ddc-9605-43981db9d940", + "simStepLabel": "Uptime Monitor Creation: Schedule Job", + "simStepDescription": "The `SuperSimpleQueue` receives the new monitor and uses its `scheduler.addJob` method to create a recurring job based on the monitor's interval. The monitor object itself is stored as data for the job.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js", + "startLine": "50", + "endLine": "57", + "relevantVariables": [ + "addJob", + "this.scheduler.addJob" + ] + }, + "inputDataExample": "{\n \"_id\": \"60d21b4667d0d8992e610c85\",\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000,\n \"isActive\": true\n}", + "outputDataExample": "{\n \"id\": \"60d21b4667d0d8992e610c85\",\n \"template\": \"monitor-job\",\n \"repeat\": 60000,\n \"active\": true,\n \"data\": { ...monitor object ... }\n}" + }, + { + "simStepId": "e22a708a-a348-4775-8ee3-f32a35446e24", + "diagramNodeId": "18c13391-0cf7-4303-8180-0a40cfd64b58", + "simStepLabel": "Service Monitoring: Job Execution", + "simStepDescription": "The scheduler triggers a monitor job at the specified interval. The `getMonitorJob` function in `SuperSimpleQueueHelper` is executed, which first checks if the monitor is in a maintenance window.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "startLine": "27", + "endLine": "73", + "relevantVariables": [ + "getMonitorJob", + "isInMaintenanceWindow" + ] + }, + "inputDataExample": "{\n \"id\": \"60d21b4667d0d8992e610c85\",\n \"data\": {\n \"_id\": \"60d21b4667d0d8992e610c85\",\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000\n }\n}", + "outputDataExample": "{\n \"_id\": \"60d21b4667d0d8992e610c85\",\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000\n}" + }, + { + "simStepId": "1d7580f2-f107-4008-a207-b8ec07998721", + "diagramNodeId": "38ba9b22-5b26-432d-a856-0eceb792c737", + "simStepLabel": "Service Monitoring: Request Service Status", + "simStepDescription": "Assuming no active maintenance, the job helper calls `networkService.requestStatus`, passing the monitor object to perform the actual availability check.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "startLine": "45", + "endLine": "45", + "relevantVariables": [ + "this.networkService.requestStatus" + ] + }, + "inputDataExample": "{\n \"_id\": \"60d21b4667d0d8992e610c85\",\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000\n}", + "outputDataExample": "{\n \"_id\": \"60d21b4667d0d8992e610c85\",\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\",\n \"interval\": 60000\n}" + }, + { + "simStepId": "84f82fea-e306-4e0b-8222-e9705803d51a", + "diagramNodeId": "5c900373-e6ba-44d5-b341-676a32d659c8", + "simStepLabel": "Service Monitoring: Perform Network Check", + "simStepDescription": "The `requestStatus` method in the network service acts as a router. Based on the monitor's `type` (e.g., 'http', 'ping', 'docker'), it calls the corresponding specialized request function (e.g., `requestHttp`) to perform the check and constructs a standardized response object.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/networkService.js", + "startLine": "44", + "endLine": "59", + "relevantVariables": [ + "requestStatus", + "requestHttp", + "requestPing", + "requestDocker", + "requestPort", + "requestGame" + ] + }, + "inputDataExample": "{\n \"_id\": \"60d21b4667d0d8992e610c85\",\n \"name\": \"My Google Monitor\",\n \"url\": \"http://google.com\",\n \"type\": \"http\"\n}", + "outputDataExample": "{\n \"monitorId\": \"60d21b4667d0d8992e610c85\",\n \"type\": \"http\",\n \"status\": true,\n \"code\": 200,\n \"responseTime\": 150,\n \"message\": \"OK\"\n}" + }, + { + "simStepId": "9b0ada30-d3e6-4b2c-b3ff-f50615139588", + "diagramNodeId": "5e80734c-7b72-41be-9068-30cda3ffb425", + "simStepLabel": "Service Monitoring: Propagate Network Response", + "simStepDescription": "The network response object, containing the result of the check (status, response time, status code), is returned to the job handler in `SuperSimpleQueueHelper`.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "startLine": "45", + "endLine": "45", + "relevantVariables": [ + "networkResponse" + ] + }, + "inputDataExample": "{\n \"monitorId\": \"60d21b4667d0d8992e610c85\",\n \"type\": \"http\",\n \"status\": true,\n \"code\": 200,\n \"responseTime\": 150,\n \"message\": \"OK\"\n}", + "outputDataExample": "{\n \"monitorId\": \"60d21b4667d0d8992e610c85\",\n \"type\": \"http\",\n \"status\": true,\n \"code\": 200,\n \"responseTime\": 150,\n \"message\": \"OK\"\n}" + }, + { + "simStepId": "c66a04c6-113e-4391-ab71-93fc612279ef", + "diagramNodeId": "45e38f3a-a7c3-4de7-b3dc-589361d793fd", + "simStepLabel": "Service Monitoring: Update Monitor Status", + "simStepDescription": "The job handler passes the network response to `statusService.updateStatus`. This service creates a new `Check` document in the database, updates the monitor's status based on a sliding window and threshold, and calculates running statistics.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/statusService.js", + "startLine": "108", + "endLine": "183", + "relevantVariables": [ + "updateStatus", + "buildCheck", + "insertCheck", + "updateRunningStats" + ] + }, + "inputDataExample": "{\n \"monitorId\": \"60d21b4667d0d8992e610c85\",\n \"status\": true,\n \"responseTime\": 150,\n \"code\": 200\n}", + "outputDataExample": "{\n \"monitor\": { ...updated monitor object ... },\n \"statusChanged\": false,\n \"prevStatus\": true\n}" + }, + { + "simStepId": "9896c387-4fca-47ad-82eb-7301286decb7", + "diagramNodeId": "0a7e4a72-b644-4bd8-afa1-ae8a1d6c111c", + "simStepLabel": "Service Monitoring: Check for Status Change", + "simStepDescription": "The result from `statusService`, indicating whether the monitor's overall status changed (e.g., from 'up' to 'down'), is returned to the job handler.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "startLine": "51", + "endLine": "51", + "relevantVariables": [ + "updatedMonitor", + "statusChanged", + "prevStatus" + ] + }, + "inputDataExample": "{\n \"monitor\": { ...updated monitor object ... },\n \"statusChanged\": false,\n \"prevStatus\": true\n}", + "outputDataExample": "{\n \"monitor\": { ...updated monitor object ... },\n \"statusChanged\": false,\n \"prevStatus\": true\n}" + }, + { + "simStepId": "1a260a75-cf50-4f7a-90b0-c280b512a5cb", + "diagramNodeId": "9f3ea7fc-16d3-4d36-97eb-66f8808425cc", + "simStepLabel": "Service Monitoring: Handle Notifications (Conditional)", + "simStepDescription": "If `statusChanged` is true, the job handler calls `notificationService.handleNotifications`. The service identifies the notification channels associated with the monitor and sends out alerts (e.g., email, webhook, Discord) with details about the status change.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/notificationService.js", + "startLine": "76", + "endLine": "92", + "relevantVariables": [ + "handleNotifications", + "notifyAll", + "buildStatusEmail", + "buildWebhookMessage", + "buildHardwareAlerts" + ] + }, + "inputDataExample": "{\n \"monitorId\": \"60d21b4667d0d8992e610c85\",\n \"status\": false,\n \"monitor\": { ... },\n \"statusChanged\": true,\n \"prevStatus\": true\n}", + "outputDataExample": "{}" + } + ], + "description": "
  • The core feature of Checkmate is to monitor the availability and response time of services
  • Users can configure different types of monitors for various needs
  • - Supports multiple protocols including HTTP(s), Ping, Port, and Docker containers
  • - Users can set the check frequency, from one to five minutes, to define how often the service is checked
  • - Advanced HTTP matching is available, allowing checks against JSON responses using JMESPath, regex, or simple inclusion/equality
  • - Game server monitoring is also a specific type of monitor supported by the application
", + "simulationNodesAndEdges": { + "7a2da928-a65c-49d8-b0a3-c159a6df2bc5": { + "simStepIds": [ + "ab5de0b9-c70d-492c-98d9-8ddee66284ad" + ] + }, + "a81f4ae8-41fe-4d1b-95ce-86abcc67b53c": { + "simStepIds": [ + "e02ddf57-270e-4f71-b4c4-d5183bb7879a" + ] + }, + "a30ac765-c546-4993-988e-8c8cfdb7532f": { + "simStepIds": [ + "14e9986b-c6ce-475e-97ed-da1fa2b6968d" + ] + }, + "f0409948-75dc-457f-a02d-6b9774258e48": { + "simStepIds": [ + "38fd069d-9c44-4078-954b-3a0899e9acf4" + ] + }, + "2d407bfd-e4b0-46b8-99e7-121c507d525f": { + "simStepIds": [ + "ca3ad309-323a-418d-a171-c16f56fae23c" + ] + }, + "51e8db5f-2fdd-4ddc-9605-43981db9d940": { + "simStepIds": [ + "2702bfca-7611-4562-8d3e-a05af8257871" + ] + }, + "18c13391-0cf7-4303-8180-0a40cfd64b58": { + "simStepIds": [ + "e22a708a-a348-4775-8ee3-f32a35446e24" + ] + }, + "5c900373-e6ba-44d5-b341-676a32d659c8": { + "simStepIds": [ + "84f82fea-e306-4e0b-8222-e9705803d51a" + ] + }, + "45e38f3a-a7c3-4de7-b3dc-589361d793fd": { + "simStepIds": [ + "c66a04c6-113e-4391-ab71-93fc612279ef" + ] + }, + "9f3ea7fc-16d3-4d36-97eb-66f8808425cc": { + "simStepIds": [ + "1a260a75-cf50-4f7a-90b0-c280b512a5cb" + ] + }, + "0e09aa33-d7c7-4c89-9da8-f0937658f4d3": { + "simStepIds": [ + "888df086-41bb-469d-9c0a-daf74bdecb9f" + ] + }, + "1f6f04a9-0123-4729-b06a-8f3da6d1b58c": { + "simStepIds": [ + "3fe0afaa-c673-4b52-a0c4-02a3ffd3a075" + ] + }, + "b6d963d5-c613-407a-9ce9-60a04048dea7": { + "simStepIds": [ + "13dd0564-44a5-43ea-a163-ce1148cc48e7" + ] + }, + "a6bccd1d-4272-4df7-a7c8-b3979e37b8d7": { + "simStepIds": [ + "a6f4c967-b1a8-4804-bc2e-67f47a7b68f9" + ] + }, + "de320ac1-0949-4297-be7c-b9d7a14e0fec": { + "simStepIds": [ + "71ec03b6-1166-4c5d-9ada-0875a0866e30" + ] + }, + "38ba9b22-5b26-432d-a856-0eceb792c737": { + "simStepIds": [ + "1d7580f2-f107-4008-a207-b8ec07998721" + ] + }, + "5e80734c-7b72-41be-9068-30cda3ffb425": { + "simStepIds": [ + "9b0ada30-d3e6-4b2c-b3ff-f50615139588" + ] + }, + "0a7e4a72-b644-4bd8-afa1-ae8a1d6c111c": { + "simStepIds": [ + "9896c387-4fca-47ad-82eb-7301286decb7" + ] + } + }, + "isAIGenerated": true, + "keywords": "Monitor, checkService, UptimeCreate", + "generationPrompt": "Comprehensive Uptime Monitoring for Various Services", + "generationKeywords": "Monitor, checkService, UptimeCreate" + }, + "Infrastructure and Hardware Health Monitoring": { + "name": "Infrastructure and Hardware Health Monitoring", + "simSteps": [ + { + "simStepId": "80dd889a-1fc9-4d56-b9cf-5b45bf1110b1", + "diagramNodeId": "9651d084-5c16-4417-89c1-f52ae6572a95", + "simStepLabel": "Configuration: User Sets Alert Thresholds", + "simStepDescription": "A user navigates to the 'Create Infrastructure Monitor' page to set up monitoring for a new server. They enable alerts for CPU, memory, and disk usage, and define specific thresholds for each metric. For example, an alert is configured to trigger if CPU usage exceeds 80%.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx", + "startLine": "45", + "endLine": "53", + "relevantVariables": [ + "CustomThreshold", + "METRICS", + "METRIC_PREFIX", + "handleCheckboxChange", + "infrastructureMonitor" + ] + }, + "inputDataExample": "{\n \"infrastructureMonitor\": {\n \"url\": \"my-server.example.com\",\n \"name\": \"Production Web Server\",\n \"cpu\": false,\n \"usage_cpu\": \"\",\n \"memory\": false,\n \"usage_memory\": \"\",\n \"disk\": false,\n \"usage_disk\": \"\"\n }\n}", + "outputDataExample": "{\n \"infrastructureMonitor\": {\n \"url\": \"my-server.example.com\",\n \"name\": \"Production Web Server\",\n \"cpu\": true,\n \"usage_cpu\": \"80\",\n \"memory\": true,\n \"usage_memory\": \"75\",\n \"disk\": true,\n \"usage_disk\": \"90\"\n }\n}" + }, + { + "simStepId": "815f96af-c28e-4f80-b4ab-18a6fb23eee0", + "diagramNodeId": "1c5c9857-bf32-40e4-921b-957c252dac2a", + "simStepLabel": "API Call: Submit Monitor Configuration", + "simStepDescription": "After configuring the monitor and its alert thresholds, the user saves the configuration. The frontend application constructs a monitor object with `type: 'hardware'` and sends it to the backend via a POST request to create the new monitor.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Hooks/v1/monitorHooks.js", + "startLine": "386", + "endLine": "389", + "relevantVariables": [ + "updatedFields", + "monitor.type", + "monitor.thresholds", + "monitor.secret" + ] + }, + "inputDataExample": "{\n \"monitor\": {\n \"name\": \"Production Web Server\",\n \"url\": \"http://my-server.example.com\",\n \"type\": \"hardware\",\n \"interval\": 15000,\n \"secret\": \"super-secret-key\",\n \"thresholds\": {\n \"usage_cpu\": 0.8,\n \"usage_memory\": 0.75,\n \"usage_disk\": 0.9\n }\n }\n}", + "outputDataExample": "{\n \"monitor\": {\n \"name\": \"Production Web Server\",\n \"url\": \"http://my-server.example.com\",\n \"type\": \"hardware\",\n \"interval\": 15000,\n \"secret\": \"super-secret-key\",\n \"thresholds\": {\n \"usage_cpu\": 0.8,\n \"usage_memory\": 0.75,\n \"usage_disk\": 0.9\n }\n }\n}" + }, + { + "simStepId": "ed6e7faf-69dd-4d0b-bda8-137739ed288b", + "diagramNodeId": "4c47b5f2-5363-4816-9745-30e61d184c7a", + "simStepLabel": "Automated Check: Backend Fetches Hardware Data", + "simStepDescription": "The Checkmate server's job queue periodically initiates a check for the newly created hardware monitor. It makes an HTTP request to the monitor's URL, which points to the external 'Capture' agent running on the target server.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/networkService.js", + "startLine": "7", + "endLine": "14", + "relevantVariables": [ + "TYPE_HARDWARE" + ] + }, + "inputDataExample": "{\n \"monitor\": {\n \"_id\": \"65ada27a51442afd52857053\",\n \"name\": \"Production Web Server\",\n \"url\": \"http://my-server.example.com\",\n \"type\": \"hardware\",\n \"interval\": 15000\n }\n}", + "outputDataExample": "{\n \"networkResponse\": {\n \"ok\": true,\n \"status\": 200,\n \"statusText\": \"OK\",\n \"payload\": {\n \"data\": {\n \"cpu\": {\n \"usage_percent\": 0.85\n },\n \"memory\": {\n \"usage_percent\": 0.65\n },\n \"disk\": [\n {\n \"usage_percent\": 0.5\n }\n ]\n }\n }\n }\n}" + }, + { + "simStepId": "4434dca1-2048-4458-a70b-b70d569e378d", + "diagramNodeId": "a64547c9-d9e6-4941-8e5e-f49add06516b", + "simStepLabel": "Automated Check: Data Received from Capture Agent", + "simStepDescription": "The 'Capture' agent responds to the server's request with a JSON payload containing detailed hardware metrics, including CPU usage, memory consumption, disk space, and network statistics.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/statusService.js", + "startLine": "266", + "endLine": "273", + "relevantVariables": [ + "payload", + "cpu", + "memory", + "disk", + "host" + ] + }, + "inputDataExample": "{\n \"data\": {\n \"cpu\": {\n \"physical_core\": 4,\n \"logical_core\": 8,\n \"frequency\": 3400,\n \"usage_percent\": 0.85,\n \"temperature\": [65, 66, 65, 67]\n },\n \"memory\": {\n \"total_bytes\": 17179869184,\n \"used_bytes\": 11166914970,\n \"usage_percent\": 0.65\n },\n \"disk\": [\n {\n \"name\": \"/dev/sda1\",\n \"total_bytes\": 250685575168,\n \"used_bytes\": 125342787584,\n \"usage_percent\": 0.5\n }\n ],\n \"host\": {\n \"os\": \"Ubuntu 22.04 LTS\"\n }\n },\n \"errors\": []\n}", + "outputDataExample": "{\n \"data\": {\n \"cpu\": {\n \"physical_core\": 4,\n \"logical_core\": 8,\n \"frequency\": 3400,\n \"usage_percent\": 0.85,\n \"temperature\": [65, 66, 65, 67]\n },\n \"memory\": {\n \"total_bytes\": 17179869184,\n \"used_bytes\": 11166914970,\n \"usage_percent\": 0.65\n },\n \"disk\": [\n {\n \"name\": \"/dev/sda1\",\n \"total_bytes\": 250685575168,\n \"used_bytes\": 125342787584,\n \"usage_percent\": 0.5\n }\n ],\n \"host\": {\n \"os\": \"Ubuntu 22.04 LTS\"\n }\n },\n \"errors\": []\n}" + }, + { + "simStepId": "e5b90cf2-9369-4741-9803-c835a81c009b", + "diagramNodeId": "c1bf10c9-8c1a-44f4-8e23-39708f0301e8", + "simStepLabel": "Automated Check: Process and Store Hardware Data", + "simStepDescription": "The backend's StatusService receives the payload from the Capture agent. It parses the data and constructs a 'check' document. This document, containing the detailed hardware metrics, is then saved to the database for historical tracking and visualization.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/statusService.js", + "startLine": "265", + "endLine": "274", + "relevantVariables": [ + "buildCheck", + "type", + "payload", + "check.cpu", + "check.memory", + "check.disk", + "check.host" + ] + }, + "inputDataExample": "{\n \"networkResponse\": {\n \"type\": \"hardware\",\n \"payload\": {\n \"data\": {\n \"cpu\": { \"usage_percent\": 0.85 },\n \"memory\": { \"usage_percent\": 0.65 },\n \"disk\": [{ \"usage_percent\": 0.5 }],\n \"host\": { \"os\": \"Ubuntu 22.04 LTS\" }\n }\n }\n }\n}", + "outputDataExample": "{\n \"check\": {\n \"monitorId\": \"65ada27a51442afd52857053\",\n \"type\": \"hardware\",\n \"status\": true,\n \"cpu\": { \"usage_percent\": 0.85 },\n \"memory\": { \"usage_percent\": 0.65 },\n \"disk\": [{ \"usage_percent\": 0.5 }],\n \"host\": { \"os\": \"Ubuntu 22.04 LTS\" }\n }\n}" + }, + { + "simStepId": "b019a6d0-a23a-4a1d-bd21-644f01996f14", + "diagramNodeId": "7a250d09-50c6-4935-b10c-626b0ebe1fba", + "simStepLabel": "Automated Check: Pass Data for Alert Evaluation", + "simStepDescription": "After storing the check data, the system passes the complete network response, including monitor details and the latest metrics, to the notification service to evaluate if any alerts need to be triggered.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/notificationService.js", + "startLine": "60", + "endLine": "79", + "relevantVariables": [ + "handleNotifications", + "networkResponse", + "monitor.type", + "thresholds", + "metrics" + ] + }, + "inputDataExample": "{\n \"networkResponse\": {\n \"monitor\": {\n \"_id\": \"65ada27a51442afd52857053\",\n \"name\": \"Production Web Server\",\n \"thresholds\": {\n \"usage_cpu\": 0.8\n },\n \"notifications\": [\"65a5b0b3e8a2b0e6e8c7b8e3\"]\n },\n \"payload\": {\n \"data\": {\n \"cpu\": { \"usage_percent\": 0.85 }\n }\n }\n }\n}", + "outputDataExample": "{\n \"networkResponse\": {\n \"monitor\": {\n \"_id\": \"65ada27a51442afd52857053\",\n \"name\": \"Production Web Server\",\n \"thresholds\": {\n \"usage_cpu\": 0.8\n },\n \"notifications\": [\"65a5b0b3e8a2b0e6e8c7b8e3\"]\n },\n \"payload\": {\n \"data\": {\n \"cpu\": { \"usage_percent\": 0.85 }\n }\n }\n }\n}" + }, + { + "simStepId": "07fe8489-1d94-4afb-83f8-7cebb0dff49a", + "diagramNodeId": "d4f48f1a-61de-4cde-87d2-4b5215464899", + "simStepLabel": "Automated Check: Evaluate Data Against Thresholds", + "simStepDescription": "The notification utility function compares the received hardware metrics against the user-defined thresholds stored in the monitor's configuration. In this case, the CPU usage of 85% is greater than the 80% threshold, so an alert condition is met.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/notificationUtils.js", + "startLine": "95", + "endLine": "109", + "relevantVariables": [ + "buildHardwareAlerts", + "thresholds", + "metrics", + "cpuThreshold", + "cpuUsage", + "alerts" + ] + }, + "inputDataExample": "{\n \"networkResponse\": {\n \"monitor\": {\n \"thresholds\": {\n \"usage_cpu\": 0.8\n }\n },\n \"payload\": {\n \"data\": {\n \"cpu\": {\n \"usage_percent\": 0.85\n }\n }\n }\n }\n}", + "outputDataExample": "{\n \"alerts\": {\n \"cpu\": true,\n \"memory\": false,\n \"disk\": false\n }\n}" + }, + { + "simStepId": "c691b3b9-88b3-4c88-a399-ff9fd5866194", + "diagramNodeId": "3d357de8-9fd2-4d0b-9623-f36fce4cf1de", + "simStepLabel": "Automated Check: Generate Alert Notification Content", + "simStepDescription": "Since the CPU threshold was breached, the system constructs a user-friendly alert message. For email notifications, it builds an HTML email detailing the alert, including the monitor name, the metric that triggered the alert, the current value, and the threshold.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/notificationUtils.js", + "startLine": "177", + "endLine": "183", + "relevantVariables": [ + "buildHardwareEmail", + "networkResponse", + "alerts", + "template", + "context", + "subject", + "html" + ] + }, + "inputDataExample": "{\n \"networkResponse\": {\n \"monitor\": {\n \"name\": \"Production Web Server\",\n \"url\": \"http://my-server.example.com\"\n }\n },\n \"alerts\": [\n \"Your current CPU usage (85%) is above your threshold (80%)\"\n ]\n}", + "outputDataExample": "{\n \"subject\": \"Monitor Production Web Server infrastructure alerts\",\n \"html\": \"...HTML content for the email...\"\n}" + }, + { + "simStepId": "c27365db-9db6-4cc9-96f0-4f046fd38562", + "diagramNodeId": "801fe70a-1a19-406c-8f0d-770b0cb74409", + "simStepLabel": "Visualization: User Views Monitor Details", + "simStepDescription": "A user navigates to the details page for their 'Production Web Server' monitor in the Checkmate UI to view its performance. The React component for the details page mounts and prepares to fetch data.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Infrastructure/Details/index.jsx", + "startLine": "23", + "endLine": "31", + "relevantVariables": [ + "InfrastructureDetails", + "useParams", + "useState", + "dateRange", + "monitorId" + ] + }, + "inputDataExample": "{\n \"path\": \"/infrastructure/65ada27a51442afd52857053\"\n}", + "outputDataExample": "{\n \"monitorId\": \"65ada27a51442afd52857053\",\n \"dateRange\": \"recent\"\n}" + }, + { + "simStepId": "a5d20172-1c95-4c9e-af39-f86a498734bb", + "diagramNodeId": "00921831-b1d9-4a5a-ae6f-5ddee7cfc1e4", + "simStepLabel": "API Call: Request for Historical Hardware Data", + "simStepDescription": "The frontend makes a GET request to the backend API to fetch the historical hardware data for the specified monitor and the selected time range (e.g., 'recent').", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "198", + "endLine": "206", + "relevantVariables": [ + "getHardwareDetailsByMonitorId", + "config.monitorId", + "config.dateRange", + "this.axiosInstance.get" + ] + }, + "inputDataExample": "{\n \"monitorId\": \"65ada27a51442afd52857053\",\n \"dateRange\": \"recent\"\n}", + "outputDataExample": "{\n \"request\": \"GET /api/v1/monitors/hardware/details/65ada27a51442afd52857053?dateRange=recent\"\n}" + }, + { + "simStepId": "8502ae1f-f029-446e-9dde-59c2aba87a8a", + "diagramNodeId": "9ef7c423-f4fc-4caf-9bf5-c1aa3002ca03", + "simStepLabel": "Backend: Aggregate Historical Data", + "simStepDescription": "The backend receives the request and executes a database aggregation pipeline. This query fetches all 'hardware' checks for the monitor within the given date range and calculates metrics like average CPU usage, average memory usage, and average temperatures, grouped by time intervals.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/monitorModule.js", + "startLine": "312", + "endLine": "322", + "relevantVariables": [ + "getHardwareDetailsById", + "monitorId", + "dateRange", + "buildHardwareDetailsPipeline", + "hardwareStats" + ] + }, + "inputDataExample": "{\n \"monitorId\": \"65ada27a51442afd52857053\",\n \"dateRange\": \"recent\"\n}", + "outputDataExample": "{\n \"stats\": {\n \"checks\": [\n {\n \"_id\": \"2023-10-27T10:00:00Z\",\n \"avgCpuUsage\": 0.85,\n \"avgMemoryUsage\": 0.65\n },\n {\n \"_id\": \"2023-10-27T10:01:00Z\",\n \"avgCpuUsage\": 0.82,\n \"avgMemoryUsage\": 0.66\n }\n ],\n \"latestCheck\": {\n \"cpu\": { \"usage_percent\": 0.85 },\n \"memory\": { \"usage_percent\": 0.65 }\n }\n }\n}" + }, + { + "simStepId": "627fcb0c-0282-4b4d-8c55-aceb636e4da0", + "diagramNodeId": "60308696-6f8a-43c8-b9a1-da199ef500cd", + "simStepLabel": "API Response: Send Aggregated Data to Frontend", + "simStepDescription": "The backend sends the aggregated hardware statistics back to the frontend in a structured JSON format, ready for visualization.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/monitorModuleQueries.js", + "startLine": "247", + "endLine": "262", + "relevantVariables": [ + "buildHardwareDetailsPipeline", + "$group", + "avgCpuUsage", + "avgMemoryUsage", + "avgTemperatures" + ] + }, + "inputDataExample": "{\n \"stats\": {\n \"checks\": [\n {\n \"_id\": \"2023-10-27T10:00:00Z\",\n \"avgCpuUsage\": 0.85,\n \"avgMemoryUsage\": 0.65,\n \"avgTemperature\": [65.5]\n },\n {\n \"_id\": \"2023-10-27T10:01:00Z\",\n \"avgCpuUsage\": 0.82,\n \"avgMemoryUsage\": 0.66,\n \"avgTemperature\": [66.0]\n }\n ],\n \"latestCheck\": {\n \"cpu\": { \"usage_percent\": 0.85, \"physical_core\": 4, \"frequency\": 3400 },\n \"memory\": { \"usage_percent\": 0.65, \"total_bytes\": 17179869184, \"used_bytes\": 11166914970 }\n }\n }\n}", + "outputDataExample": "{\n \"stats\": {\n \"checks\": [\n {\n \"_id\": \"2023-10-27T10:00:00Z\",\n \"avgCpuUsage\": 0.85,\n \"avgMemoryUsage\": 0.65,\n \"avgTemperature\": [65.5]\n },\n {\n \"_id\": \"2023-10-27T10:01:00Z\",\n \"avgCpuUsage\": 0.82,\n \"avgMemoryUsage\": 0.66,\n \"avgTemperature\": [66.0]\n }\n ],\n \"latestCheck\": {\n \"cpu\": { \"usage_percent\": 0.85, \"physical_core\": 4, \"frequency\": 3400 },\n \"memory\": { \"usage_percent\": 0.65, \"total_bytes\": 17179869184, \"used_bytes\": 11166914970 }\n }\n }\n}" + }, + { + "simStepId": "6eedce82-b9c8-4022-95e5-3a6e00628105", + "diagramNodeId": "9ea2e32d-1ea8-42f2-9a17-613844723ae6", + "simStepLabel": "Visualization: Frontend Renders Charts and Gauges", + "simStepDescription": "The frontend components on the Infrastructure Details page receive the aggregated data. The 'GaugeBoxes' component uses the 'latestCheck' data to render real-time gauges for current CPU and memory usage, while the 'AreaChartBoxes' component uses the historical 'checks' data to render time-series charts.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx", + "startLine": "22", + "endLine": "51", + "relevantVariables": [ + "Gauges", + "latestCheck", + "memoryUsagePercent", + "cpuUsagePercent", + "gauges" + ] + }, + "inputDataExample": "{\n \"monitor\": {\n \"stats\": {\n \"checks\": [\n { \"_id\": \"2023-10-27T10:00:00Z\", \"avgCpuUsage\": 0.85 },\n { \"_id\": \"2023-10-27T10:01:00Z\", \"avgCpuUsage\": 0.82 }\n ],\n \"latestCheck\": {\n \"cpu\": { \"usage_percent\": 0.85, \"physical_core\": 4, \"frequency\": 3400 },\n \"memory\": { \"usage_percent\": 0.65, \"total_bytes\": 17179869184, \"used_bytes\": 11166914970 }\n }\n }\n }\n}", + "outputDataExample": "{\n \"rendered_gauges\": [\n {\n \"type\": \"memory\",\n \"value\": \"65%\",\n \"heading\": \"Memory usage\"\n },\n {\n \"type\": \"cpu\",\n \"value\": \"85%\",\n \"heading\": \"CPU usage\"\n }\n ]\n}" + } + ], + "description": "
  • Checkmate provides detailed monitoring of server hardware resources, requiring an external agent called 'Capture'
  • - Tracks key metrics such as CPU usage, temperature, memory consumption, and disk space
  • - Visualizes historical hardware data through detailed charts and real-time gauges
  • - Users can configure alert thresholds for each metric (CPU, memory, disk, temperature) to get notified about potential issues proactively
  • - Provides insights into network statistics, including data sent/received and packet information
", + "simulationNodesAndEdges": { + "9651d084-5c16-4417-89c1-f52ae6572a95": { + "simStepIds": [ + "80dd889a-1fc9-4d56-b9cf-5b45bf1110b1" + ] + }, + "4c47b5f2-5363-4816-9745-30e61d184c7a": { + "simStepIds": [ + "ed6e7faf-69dd-4d0b-bda8-137739ed288b" + ] + }, + "c1bf10c9-8c1a-44f4-8e23-39708f0301e8": { + "simStepIds": [ + "e5b90cf2-9369-4741-9803-c835a81c009b" + ] + }, + "d4f48f1a-61de-4cde-87d2-4b5215464899": { + "simStepIds": [ + "07fe8489-1d94-4afb-83f8-7cebb0dff49a" + ] + }, + "801fe70a-1a19-406c-8f0d-770b0cb74409": { + "simStepIds": [ + "c27365db-9db6-4cc9-96f0-4f046fd38562" + ] + }, + "9ef7c423-f4fc-4caf-9bf5-c1aa3002ca03": { + "simStepIds": [ + "8502ae1f-f029-446e-9dde-59c2aba87a8a" + ] + }, + "9ea2e32d-1ea8-42f2-9a17-613844723ae6": { + "simStepIds": [ + "6eedce82-b9c8-4022-95e5-3a6e00628105" + ] + }, + "1c5c9857-bf32-40e4-921b-957c252dac2a": { + "simStepIds": [ + "815f96af-c28e-4f80-b4ab-18a6fb23eee0" + ] + }, + "a64547c9-d9e6-4941-8e5e-f49add06516b": { + "simStepIds": [ + "4434dca1-2048-4458-a70b-b70d569e378d" + ] + }, + "7a250d09-50c6-4935-b10c-626b0ebe1fba": { + "simStepIds": [ + "b019a6d0-a23a-4a1d-bd21-644f01996f14" + ] + }, + "3d357de8-9fd2-4d0b-9623-f36fce4cf1de": { + "simStepIds": [ + "c691b3b9-88b3-4c88-a399-ff9fd5866194" + ] + }, + "00921831-b1d9-4a5a-ae6f-5ddee7cfc1e4": { + "simStepIds": [ + "a5d20172-1c95-4c9e-af39-f86a498734bb" + ] + }, + "60308696-6f8a-43c8-b9a1-da199ef500cd": { + "simStepIds": [ + "627fcb0c-0282-4b4d-8c55-aceb636e4da0" + ] + } + }, + "isAIGenerated": true, + "keywords": "hardware, InfrastructureDetails, cpu", + "generationPrompt": "Infrastructure and Hardware Health Monitoring", + "generationKeywords": "hardware, InfrastructureDetails, cpu" + }, + "Multi-Channel Alerting and Notifications": { + "name": "Multi-Channel Alerting and Notifications", + "simSteps": [ + { + "simStepId": "e924ec15-1bea-4e3b-befb-a8bf30cbf740", + "diagramNodeId": "15486629-84b3-463a-962b-06083f69e4bf", + "simStepLabel": "Flow 1: Render Notification Creation Form", + "simStepDescription": "User accesses the `/notifications/create` route, which renders the `CreateNotifications` component. This component displays a form for setting up a new notification channel, including fields for name, type, and provider-specific details.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Notifications/create/index.jsx", + "startLine": "157", + "endLine": "321", + "relevantVariables": [ + "CreateNotifications", + "notification", + "onChange", + "onSubmit" + ] + }, + "inputDataExample": "{\n \"route\": \"/notifications/create\"\n}", + "outputDataExample": "{\n \"renderedComponent\": \"CreateNotifications\",\n \"formFields\": [\"notificationName\", \"type\", \"address\", \"slackWebhookUrl\", etc...]\n}" + }, + { + "simStepId": "71dbcd41-c6d4-4514-b5a4-6c966ca7ba6f", + "diagramNodeId": "54497b9a-4910-4df4-b016-480b9ac250a7", + "simStepLabel": "Flow 1: Submit Form Data for Creation", + "simStepDescription": "After filling the form, the user clicks 'Save'. The `onSubmit` function is triggered, which calls the `createNotification` function from the `useCreateNotification` hook with the form data.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Notifications/create/index.jsx", + "startLine": "96", + "endLine": "101", + "relevantVariables": [ + "onSubmit", + "createNotification" + ] + }, + "inputDataExample": "{\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"isDefault\": false\n}", + "outputDataExample": "{\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"isDefault\": false\n}" + }, + { + "simStepId": "383c6be8-aa51-4398-9ae3-21035413f73a", + "diagramNodeId": "a07b805e-e55f-46cb-b54b-765b7813b0ca", + "simStepLabel": "Flow 1: Frontend API Call", + "simStepDescription": "The `createNotification` hook executes an asynchronous function that calls `networkService.createNotification` to send the new notification data to the backend API.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Hooks/v1/useNotifications.js", + "startLine": "14", + "endLine": "17", + "relevantVariables": [ + "createNotification", + "networkService" + ] + }, + "inputDataExample": "{\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"isDefault\": false\n}", + "outputDataExample": "{\n \"status\": \"pending\",\n \"promise\": {}\n}" + }, + { + "simStepId": "162d8d2f-6cd1-45fa-8711-1ee85c5f86ef", + "diagramNodeId": "dc135d8d-0515-403d-ae58-f3c04647b2ae", + "simStepLabel": "Flow 1: HTTP POST Request", + "simStepDescription": "The `NetworkService` constructs and sends an HTTP POST request to the `/notifications` endpoint with the notification data in the request body.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "1064", + "endLine": "1066", + "relevantVariables": [ + "createNotification", + "this.axiosInstance" + ] + }, + "inputDataExample": "{\n \"url\": \"/notifications\",\n \"method\": \"POST\",\n \"data\": {\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"isDefault\": false\n }\n}", + "outputDataExample": "{\n \"url\": \"/notifications\",\n \"method\": \"POST\",\n \"data\": {\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"isDefault\": false\n }\n}" + }, + { + "simStepId": "2f9266c9-4cbc-4b50-a280-cf8b5800d540", + "diagramNodeId": "d0a4737f-715f-4f50-b6d5-5e4b31f86f93", + "simStepLabel": "Flow 1: Backend Route Handling", + "simStepDescription": "The backend server receives the POST request. The Express router matches the `/notifications` path and forwards the request to the `createNotification` method of the `NotificationController`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/notificationRoute.js", + "startLine": "10", + "endLine": "10", + "relevantVariables": [ + "this.router", + "this.notificationController.createNotification" + ] + }, + "inputDataExample": "{\n \"request\": {\n \"path\": \"/notifications\",\n \"method\": \"POST\",\n \"body\": {\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\"\n }\n }\n}", + "outputDataExample": "{\n \"handler\": \"notificationController.createNotification\"\n}" + }, + { + "simStepId": "73e63d2f-2b43-488d-9e86-170f511673f5", + "diagramNodeId": "7b9ebb0e-a6b2-438c-bfed-9c851a1bccbc", + "simStepLabel": "Flow 1: Controller to Database Module", + "simStepDescription": "The `createNotification` method in the controller validates the request body, appends user/team context, and then calls the `createNotification` method in the `notificationModule` to persist the data.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/notificationController.js", + "startLine": "55", + "endLine": "55", + "relevantVariables": [ + "body", + "this.db.notificationModule.createNotification" + ] + }, + "inputDataExample": "{\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"userId\": \"user-123\",\n \"teamId\": \"team-456\"\n}", + "outputDataExample": "{\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"userId\": \"user-123\",\n \"teamId\": \"team-456\"\n}" + }, + { + "simStepId": "d29ab4ab-be50-4f17-af6e-2c61cec9d0ab", + "diagramNodeId": "4a796ccf-40a9-4446-a109-6a6eed52e898", + "simStepLabel": "Flow 1: Database Insertion", + "simStepDescription": "The `notificationModule` creates a new instance of the `Notification` model with the provided data and saves it to the MongoDB database.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/notificationModule.js", + "startLine": "10", + "endLine": "13", + "relevantVariables": [ + "notificationData", + "this.Notification" + ] + }, + "inputDataExample": "{\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"userId\": \"user-123\",\n \"teamId\": \"team-456\"\n}", + "outputDataExample": "{\n \"_id\": \"60d5ecf31c9d440000a1b2c3\",\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"userId\": \"user-123\",\n \"teamId\": \"team-456\",\n \"createdAt\": \"2023-10-27T10:00:00Z\",\n \"updatedAt\": \"2023-10-27T10:00:00Z\"\n}" + }, + { + "simStepId": "d390ef45-f2fe-4806-a45a-cc9c84b48a5a", + "diagramNodeId": "279155cd-ff31-41a2-8f5f-a538f01bfa66", + "simStepLabel": "Flow 1: User Clicks Test Notification Button", + "simStepDescription": "The user clicks the 'Test notification' button, which invokes the `onTestNotification` handler. This handler calls the `testNotification` function from the `useTestNotification` hook.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Notifications/create/index.jsx", + "startLine": "281", + "endLine": "285", + "relevantVariables": [ + "onTestNotification", + "testNotification" + ] + }, + "inputDataExample": "{\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"isDefault\": false\n}", + "outputDataExample": "{\n \"notificationName\": \"Production Slack Alerts\",\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\",\n \"isDefault\": false\n}" + }, + { + "simStepId": "c1b17b64-90e9-48bc-8927-25d7ebfc3036", + "diagramNodeId": "29c2c56e-9dc0-4304-bfbf-e0c0f8915a39", + "simStepLabel": "Flow 1: Backend Receives Test Request", + "simStepDescription": "The client sends a POST request to `/notifications/test`. The router directs this request to the `testNotification` method in the `NotificationController`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/notificationRoute.js", + "startLine": "13", + "endLine": "13", + "relevantVariables": [ + "this.router", + "this.notificationController.testNotification" + ] + }, + "inputDataExample": "{\n \"request\": {\n \"path\": \"/notifications/test\",\n \"method\": \"POST\",\n \"body\": {\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\"\n }\n }\n}", + "outputDataExample": "{\n \"handler\": \"notificationController.testNotification\"\n}" + }, + { + "simStepId": "6f2bf027-50d9-472e-9569-808a64fce7cf", + "diagramNodeId": "57018b30-e2f4-4eb2-a1da-fe75afbb8c2f", + "simStepLabel": "Flow 1: Controller to Notification Service", + "simStepDescription": "The `testNotification` controller method passes the notification configuration from the request body to the `sendTestNotification` method of the `NotificationService`.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/notificationController.js", + "startLine": "22", + "endLine": "22", + "relevantVariables": [ + "notification", + "this.notificationService.sendTestNotification" + ] + }, + "inputDataExample": "{\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\"\n}", + "outputDataExample": "{\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\"\n}" + }, + { + "simStepId": "f29f9be1-a0cd-43a3-b259-e5105a25ce41", + "diagramNodeId": "1cc01e08-4a6c-4526-b8a7-5ee8a06c1709", + "simStepLabel": "Flow 1: Send Test Notification", + "simStepDescription": "The `NotificationService` prepares a standard test message and calls its internal `sendNotification` method. This method identifies the notification type (e.g., email, Slack) and uses the corresponding service (e.g., EmailService, NetworkService for webhooks) to dispatch the test message.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/notificationService.js", + "startLine": "120", + "endLine": "123", + "relevantVariables": [ + "sendTestNotification", + "sendNotification" + ] + }, + "inputDataExample": "{\n \"type\": \"slack\",\n \"slackWebhookUrl\": \"https://hooks.slack.com/services/T01ABCD/B01EFGH/xXxXxXxXxX\"\n}", + "outputDataExample": "{\n \"success\": true\n}" + }, + { + "simStepId": "157cec87-9b76-4a1c-9d85-0a6d3decbaa3", + "diagramNodeId": "28f1eabb-4f24-4944-8a0a-04f41ceb8589", + "simStepLabel": "Flow 2: Job Execution and Status Update", + "simStepDescription": "A background job is generated for a monitor. When executed, it performs a network check, saves the result, and updates the monitor's status via `statusService.updateMonitorStatus`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v2/infrastructure/JobGenerator.ts", + "startLine": "43", + "endLine": "66", + "relevantVariables": [ + "generateJob", + "networkService.check", + "statusService.updateMonitorStatus" + ] + }, + "inputDataExample": "{\n \"monitor\": {\n \"_id\": \"60d5f1b31c9d440000a1b2c4\",\n \"name\": \"Main API\",\n \"type\": \"http\",\n \"url\": \"https://api.example.com/health\",\n \"status\": \"up\"\n }\n}", + "outputDataExample": "{\n \"updatedMonitor\": {\n \"_id\": \"60d5f1b31c9d440000a1b2c4\",\n \"name\": \"Main API\",\n \"url\": \"https://api.example.com/health\",\n \"status\": \"down\"\n },\n \"statusChanged\": true\n}" + }, + { + "simStepId": "b7ec7415-fcae-4077-8c19-302a015b00b5", + "diagramNodeId": "50797935-1447-47a4-891b-9e886e5601fa", + "simStepLabel": "Flow 2: Status Change Detected", + "simStepDescription": "The `updateMonitorStatus` function returns a boolean indicating if the monitor's status has changed (e.g., from 'up' to 'down'). This boolean is used as a condition to trigger notifications.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v2/infrastructure/JobGenerator.ts", + "startLine": "60", + "endLine": "60", + "relevantVariables": [ + "statusChanged" + ] + }, + "inputDataExample": "{\n \"statusChanged\": true\n}", + "outputDataExample": "{\n \"statusChanged\": true\n}" + }, + { + "simStepId": "55ad645b-118a-478e-a00c-09ef82357008", + "diagramNodeId": "31fe5a28-0640-4b52-841b-dd02e2ae646e", + "simStepLabel": "Flow 2: Initiate Notification Handling", + "simStepDescription": "If the monitor's status has changed, the `handleNotifications` method of the `NotificationService` is invoked with the updated monitor's data.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v2/infrastructure/JobGenerator.ts", + "startLine": "61", + "endLine": "61", + "relevantVariables": [ + "this.notificationService.handleNotifications", + "updatedMonitor" + ] + }, + "inputDataExample": "{\n \"updatedMonitor\": {\n \"_id\": \"60d5f1b31c9d440000a1b2c4\",\n \"name\": \"Main API\",\n \"url\": \"https://api.example.com/health\",\n \"status\": \"down\",\n \"notifications\": [\"60d5ecf31c9d440000a1b2c3\"]\n }\n}", + "outputDataExample": "{\n \"status\": \"pending\",\n \"promise\": {}\n}" + }, + { + "simStepId": "b7259d77-c8df-4060-ac03-58728ae3eb54", + "diagramNodeId": "c778c6b9-c12e-45c0-8682-2c9e892867b0", + "simStepLabel": "Flow 2: Monitor Data Passed to Notification Service", + "simStepDescription": "The monitor object, which includes its current status ('down') and a list of associated notification channels, is passed to the `NotificationService`.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v2/infrastructure/NotificationService.ts", + "startLine": "7", + "endLine": "7", + "relevantVariables": [ + "handleNotifications", + "monitor" + ] + }, + "inputDataExample": "{\n \"monitor\": {\n \"_id\": \"60d5f1b31c9d440000a1b2c4\",\n \"name\": \"Main API\",\n \"status\": \"down\",\n \"notifications\": [\"60d5ecf31c9d440000a1b2c3\"]\n }\n}", + "outputDataExample": "{\n \"monitor\": {\n \"_id\": \"60d5f1b31c9d440000a1b2c4\",\n \"name\": \"Main API\",\n \"status\": \"down\",\n \"notifications\": [\"60d5ecf31c9d440000a1b2c3\"]\n }\n}" + }, + { + "simStepId": "8d1176f3-c755-401d-9ca8-234689b4b837", + "diagramNodeId": "78e5cb9f-6a97-4d31-a487-fe7dbce5d7d1", + "simStepLabel": "Flow 2: Dispatch Alerts", + "simStepDescription": "The `handleNotifications` method retrieves the full details for each associated notification channel. It then iterates through them, using specific provider services (e.g., `SlackService`, `EmailService`) to format and send a tailored alert message for each channel, notifying users that the monitor is down.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v2/infrastructure/NotificationService.ts", + "startLine": "28", + "endLine": "57", + "relevantVariables": [ + "handleNotifications", + "channels", + "emailService.send", + "slackService.send", + "discordService.send" + ] + }, + "inputDataExample": "{\n \"monitor\": {\n \"_id\": \"60d5f1b31c9d440000a1b2c4\",\n \"name\": \"Main API\",\n \"status\": \"down\",\n \"notifications\": [\"60d5ecf31c9d440000a1b2c3\"]\n }\n}", + "outputDataExample": "{\n \"dispatchResults\": [\n {\n \"channelId\": \"60d5ecf31c9d440000a1b2c3\",\n \"status\": \"success\"\n }\n ]\n}" + } + ], + "description": "
  • A critical feature that makes the monitoring data actionable by alerting users when an issue is detected
  • - Users can create and manage notification channels for different services
  • - Supports a wide range of notification providers including Email, Slack, Discord, Webhooks, PagerDuty, and Matrix
  • - Notifications are triggered based on monitor status changes (e
  • g
  • , from 'up' to 'down') or when hardware metrics cross pre-defined thresholds
  • - Users can test notification configurations to ensure they are working correctly before attaching them to monitors
", + "simulationNodesAndEdges": { + "15486629-84b3-463a-962b-06083f69e4bf": { + "simStepIds": [ + "e924ec15-1bea-4e3b-befb-a8bf30cbf740" + ] + }, + "a07b805e-e55f-46cb-b54b-765b7813b0ca": { + "simStepIds": [ + "383c6be8-aa51-4398-9ae3-21035413f73a" + ] + }, + "d0a4737f-715f-4f50-b6d5-5e4b31f86f93": { + "simStepIds": [ + "2f9266c9-4cbc-4b50-a280-cf8b5800d540" + ] + }, + "4a796ccf-40a9-4446-a109-6a6eed52e898": { + "simStepIds": [ + "d29ab4ab-be50-4f17-af6e-2c61cec9d0ab" + ] + }, + "29c2c56e-9dc0-4304-bfbf-e0c0f8915a39": { + "simStepIds": [ + "c1b17b64-90e9-48bc-8927-25d7ebfc3036" + ] + }, + "1cc01e08-4a6c-4526-b8a7-5ee8a06c1709": { + "simStepIds": [ + "f29f9be1-a0cd-43a3-b259-e5105a25ce41" + ] + }, + "28f1eabb-4f24-4944-8a0a-04f41ceb8589": { + "simStepIds": [ + "157cec87-9b76-4a1c-9d85-0a6d3decbaa3" + ] + }, + "31fe5a28-0640-4b52-841b-dd02e2ae646e": { + "simStepIds": [ + "55ad645b-118a-478e-a00c-09ef82357008" + ] + }, + "78e5cb9f-6a97-4d31-a487-fe7dbce5d7d1": { + "simStepIds": [ + "8d1176f3-c755-401d-9ca8-234689b4b837" + ] + }, + "54497b9a-4910-4df4-b016-480b9ac250a7": { + "simStepIds": [ + "71dbcd41-c6d4-4514-b5a4-6c966ca7ba6f" + ] + }, + "dc135d8d-0515-403d-ae58-f3c04647b2ae": { + "simStepIds": [ + "162d8d2f-6cd1-45fa-8711-1ee85c5f86ef" + ] + }, + "7b9ebb0e-a6b2-438c-bfed-9c851a1bccbc": { + "simStepIds": [ + "73e63d2f-2b43-488d-9e86-170f511673f5" + ] + }, + "279155cd-ff31-41a2-8f5f-a538f01bfa66": { + "simStepIds": [ + "d390ef45-f2fe-4806-a45a-cc9c84b48a5a" + ] + }, + "57018b30-e2f4-4eb2-a1da-fe75afbb8c2f": { + "simStepIds": [ + "6f2bf027-50d9-472e-9569-808a64fce7cf" + ] + }, + "50797935-1447-47a4-891b-9e886e5601fa": { + "simStepIds": [ + "b7ec7415-fcae-4077-8c19-302a015b00b5" + ] + }, + "c778c6b9-c12e-45c0-8682-2c9e892867b0": { + "simStepIds": [ + "b7259d77-c8df-4060-ac03-58728ae3eb54" + ] + } + }, + "isAIGenerated": true, + "keywords": "NotificationService, sendNotification, createNotification", + "generationPrompt": "Multi-Channel Alerting and Notifications", + "generationKeywords": "NotificationService, sendNotification, createNotification" + }, + "Customizable Public and Private Status Pages": { + "name": "Customizable Public and Private Status Pages", + "simSteps": [ + { + "simStepId": "de7b5d1f-b370-42a4-a410-4178f1e2d3a6", + "diagramNodeId": "00289d11-0015-4b2a-9c5b-dc9fd0f32b80", + "simStepLabel": "Create Status Page: User Initiates Creation", + "simStepDescription": "The user navigates to the 'Create Status Page' screen. The `CreateStatusPage` component renders the UI, including tabs for configuration, and initializes the form state.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/StatusPage/Create/index.jsx", + "startLine": "26", + "endLine": "313", + "relevantVariables": [ + "CreateStatusPage", + "form", + "setForm", + "errors", + "setErrors", + "handleFormChange", + "handleSubmit" + ] + }, + "inputDataExample": "{}", + "outputDataExample": "{\"form\":{\"companyName\":\"\",\"url\":\"\",\"timezone\":\"UTC\",\"color\":\"#FF5733\",\"isPublished\":false,\"logo\":null,\"monitors\":[],\"showUptimePercentage\":true,\"showCharts\":true,\"showAdminLoginLink\":false},\"errors\":{}}" + }, + { + "simStepId": "d419f7b6-7b9b-4494-9fad-83e3ce208ca4", + "diagramNodeId": "de23eee2-b60a-4236-a890-3c5b78388bb3", + "simStepLabel": "Create Status Page: Form Submission", + "simStepDescription": "The user fills out the form and clicks the 'Save' button, which triggers the `handleSubmit` function. This function validates the form data and calls the `createStatusPage` function from the `useCreateStatusPage` hook.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/StatusPage/Create/index.jsx", + "startLine": "143", + "endLine": "159", + "relevantVariables": [ + "handleSubmit", + "createStatusPage" + ] + }, + "inputDataExample": "{\"form\":{\"companyName\":\"My Awesome Service\",\"url\":\"my-awesome-service\",\"timezone\":\"America/New_York\",\"color\":\"#3B82F6\",\"isPublished\":true,\"logo\":{\"src\":\"data:image/png;base64,...\",\"name\":\"logo.png\",\"type\":\"image/png\",\"size\":12345},\"monitors\":[\"65aee7cc1c363d5032a03972\"],\"showUptimePercentage\":true,\"showCharts\":true,\"showAdminLoginLink\":true}}", + "outputDataExample": "{\"form\":{\"companyName\":\"My Awesome Service\",\"url\":\"my-awesome-service\",\"timezone\":\"America/New_York\",\"color\":\"#3B82F6\",\"isPublished\":true,\"logo\":{\"src\":\"data:image/png;base64,...\",\"name\":\"logo.png\",\"type\":\"image/png\",\"size\":12345},\"monitors\":[\"65aee7cc1c363d5032a03972\"],\"showUptimePercentage\":true,\"showCharts\":true,\"showAdminLoginLink\":true}}" + }, + { + "simStepId": "3c99ae6a-b671-4f47-bed0-ddccd8ff5d35", + "diagramNodeId": "aa5d6d07-05f0-45f3-b19a-199af50e35f1", + "simStepLabel": "Create Status Page: Client Hook Sends API Request", + "simStepDescription": "The `useCreateStatusPage` hook receives the form data and uses the `networkService` to send a request to the backend API to create or update the status page.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx", + "startLine": "8", + "endLine": "11", + "relevantVariables": [ + "createStatusPage", + "networkService.createStatusPage" + ] + }, + "inputDataExample": "{\"form\":{\"companyName\":\"My Awesome Service\",\"url\":\"my-awesome-service\",\"timezone\":\"America/New_York\",\"color\":\"#3B82F6\",\"isPublished\":true,\"logo\":{\"src\":\"data:image/png;base64,...\"},\"monitors\":[\"65aee7cc1c363d5032a03972\"]}}", + "outputDataExample": "{}" + }, + { + "simStepId": "8a2ab372-73db-4b9f-bc0e-6a20f15a4d79", + "diagramNodeId": "d7faa713-4204-4a97-a172-d621da36c287", + "simStepLabel": "Create Status Page: API Call to Backend", + "simStepDescription": "The `NetworkService` class constructs a `FormData` object from the form data and sends a POST request to the `/api/v1/status-page` endpoint. The logo is sent as a file upload.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "912", + "endLine": "944", + "relevantVariables": [ + "createStatusPage", + "axiosInstance.post" + ] + }, + "inputDataExample": "{\"form\":{\"companyName\":\"My Awesome Service\",\"url\":\"my-awesome-service\"...}, \"isCreate\": true}", + "outputDataExample": "{\"form\":{\"companyName\":\"My Awesome Service\",\"url\":\"my-awesome-service\"...}, \"isCreate\": true}" + }, + { + "simStepId": "84f11567-b3c4-42a2-aa7f-d78c1412099a", + "diagramNodeId": "3efddcad-588f-4492-9808-b0c81c823c96", + "simStepLabel": "Create Status Page: Backend Route Handling", + "simStepDescription": "The backend router receives the POST request on `/api/v1/status-page`. It uses `multer` to handle the file upload and forwards the request to the `createStatusPage` method in the `StatusPageController`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/statusPageRoute.js", + "startLine": "14", + "endLine": "14", + "relevantVariables": [ + "router.post", + "upload.single", + "verifyJWT", + "this.statusPageController.createStatusPage" + ] + }, + "inputDataExample": "{\"method\":\"POST\",\"url\":\"/api/v1/status-page\",\"body\":{\"companyName\":\"My Awesome Service\",...},\"file\":{...}}", + "outputDataExample": "{\"req\":{...},\"res\":{...}}" + }, + { + "simStepId": "0923a533-c572-4da3-ad87-cdfff111f3bb", + "diagramNodeId": "a3f28fa6-8268-4c13-95a0-83e3b1abdcb0", + "simStepLabel": "Create Status Page: Data Passed to Controller", + "simStepDescription": "The router passes the request object, containing form data in `req.body`, the uploaded logo in `req.file`, and user details in `req.user`, to the controller method.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/statusPageRoute.js", + "startLine": "14", + "endLine": "14", + "relevantVariables": [ + "statusPageController.createStatusPage" + ] + }, + "inputDataExample": "{\"req\":{\"body\":{\"companyName\":\"My Awesome Service\",...},\"file\":{\"fieldname\":\"logo\",...},\"user\":{\"_id\":\"65aee7cc1c363d5032a03970\",\"teamId\":\"65aee7cc1c363d5032a03971\"}}}", + "outputDataExample": "{\"req\":{\"body\":{\"companyName\":\"My Awesome Service\",...},\"file\":{\"fieldname\":\"logo\",...},\"user\":{\"_id\":\"65aee7cc1c363d5032a03970\",\"teamId\":\"65aee7cc1c363d5032a03971\"}}}" + }, + { + "simStepId": "01694730-8a75-4949-8815-bb4d6e3e2b42", + "diagramNodeId": "f15ef77e-380f-4768-98cb-7f0dd4113841", + "simStepLabel": "Create Status Page: Controller Logic", + "simStepDescription": "The `StatusPageController` validates the incoming request body and file. It then calls the `statusPageModule` to persist the new status page in the database.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/statusPageController.js", + "startLine": "18", + "endLine": "24", + "relevantVariables": [ + "createStatusPageBodyValidation", + "imageValidation", + "this.db.statusPageModule.createStatusPage" + ] + }, + "inputDataExample": "{\"req\":{\"body\":{...},\"file\":{...},\"user\":{...}}}", + "outputDataExample": "{\"statusPageData\":{...},\"image\":{...},\"userId\":\"65aee7cc1c363d5032a03970\",\"teamId\":\"65aee7cc1c363d5032a03971\"}" + }, + { + "simStepId": "67603f03-c088-4ed0-9f18-072435134cdd", + "diagramNodeId": "9735611c-924b-49aa-92e8-10d8a86ac288", + "simStepLabel": "Create Status Page: Data Sent to DB Module", + "simStepDescription": "The controller passes the processed status page data, image buffer, user ID, and team ID to the `createStatusPage` function in the database module.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/statusPageController.js", + "startLine": "21", + "endLine": "21", + "relevantVariables": [ + "this.db.statusPageModule.createStatusPage" + ] + }, + "inputDataExample": "{\"statusPageData\":{\"companyName\":\"My Awesome Service\",...},\"image\":{\"buffer\":,\"mimetype\":\"image/png\"},\"userId\":\"65aee7cc1c363d5032a03970\",\"teamId\":\"65aee7cc1c363d5032a03971\"}", + "outputDataExample": "{\"statusPageData\":{\"companyName\":\"My Awesome Service\",...},\"image\":{\"buffer\":,\"mimetype\":\"image/png\"},\"userId\":\"65aee7cc1c363d5032a03970\",\"teamId\":\"65aee7cc1c363d5032a03971\"}" + }, + { + "simStepId": "b6fa93cd-6fca-4d9e-969a-5400e2437ee8", + "diagramNodeId": "e234917e-a950-48f5-b864-cd957b2216e1", + "simStepLabel": "Create Status Page: Database Record Creation", + "simStepDescription": "The `StatusPageModule` creates a new instance of the `StatusPage` Mongoose model, populates it with the provided data including the image buffer, and saves it to the MongoDB database. It also handles duplicate URL errors.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/statusPageModule.js", + "startLine": "14", + "endLine": "34", + "relevantVariables": [ + "createStatusPage", + "new this.StatusPage", + "statusPage.save" + ] + }, + "inputDataExample": "{\"statusPageData\":{...},\"image\":{...},\"userId\":\"...\",\"teamId\":\"...\"}", + "outputDataExample": "{\"_id\":\"65aee7dd1c363d5032a03975\",\"companyName\":\"My Awesome Service\",\"url\":\"my-awesome-service\",\"userId\":\"65aee7cc1c363d5032a03970\",...}" + }, + { + "simStepId": "ea4b3532-baae-4890-a73e-2ca385d0319e", + "diagramNodeId": "40cb7d7b-2888-44de-bb7d-7735af70ba34", + "simStepLabel": "Create Status Page: Database Result Returned", + "simStepDescription": "The newly created status page document is returned from the database module to the controller.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/statusPageModule.js", + "startLine": "27", + "endLine": "27", + "relevantVariables": [ + "statusPage" + ] + }, + "inputDataExample": "{\"_id\":\"65aee7dd1c363d5032a03975\",\"companyName\":\"My Awesome Service\",\"url\":\"my-awesome-service\",...}", + "outputDataExample": "{\"_id\":\"65aee7dd1c363d5032a03975\",\"companyName\":\"My Awesome Service\",\"url\":\"my-awesome-service\",...}" + }, + { + "simStepId": "2a115421-5175-4c8f-8cdd-62f3fe148af5", + "diagramNodeId": "058ded1f-83ce-40a7-a99d-d178c81fe557", + "simStepLabel": "Create Status Page: Controller Sends Success Response", + "simStepDescription": "The controller receives the newly created status page object from the database module and sends a successful JSON response back to the client, including a success message and the new data.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/statusPageController.js", + "startLine": "25", + "endLine": "28", + "relevantVariables": [ + "res.success" + ] + }, + "inputDataExample": "{\"_id\":\"65aee7dd1c363d5032a03975\",\"companyName\":\"My Awesome Service\",...}", + "outputDataExample": "{\"msg\":\"Status page created successfully\",\"data\":{\"_id\":\"65aee7dd1c363d5032a03975\",...}}" + }, + { + "simStepId": "210acc07-81c3-4394-b566-41f6031e8eeb", + "diagramNodeId": "c3f83811-8674-4dbb-9f84-79499f97fb1f", + "simStepLabel": "Create Status Page: UI Receives Response and Redirects", + "simStepDescription": "The client-side `handleSubmit` function receives the successful response. It displays a success toast message and navigates the user to the newly created status page.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/StatusPage/Create/index.jsx", + "startLine": "151", + "endLine": "156", + "relevantVariables": [ + "createToast", + "navigate" + ] + }, + "inputDataExample": "{\"success\":true}", + "outputDataExample": "{\"success\":true}" + }, + { + "simStepId": "132354d7-df7a-4e03-9d00-16d54f836a1b", + "diagramNodeId": "30aaee38-f509-4e19-a917-e6d2f4bba61c", + "simStepLabel": "View Status Page: User Navigates to URL", + "simStepDescription": "A user accesses a status page URL (e.g., /status/uptime/my-awesome-service). The React router matches the path and renders the `Status` component, which is responsible for fetching and displaying the page details.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Routes/index.jsx", + "startLine": "148", + "endLine": "152", + "relevantVariables": [ + "Route", + "Status" + ] + }, + "inputDataExample": "{\"url\":\"/status/uptime/my-awesome-service\"}", + "outputDataExample": "{}" + }, + { + "simStepId": "2ba49e28-1cac-47f5-9118-ab930788736a", + "diagramNodeId": "0575bf87-5362-4988-b895-7e514148d420", + "simStepLabel": "View Status Page: Component Triggers Data Fetch", + "simStepDescription": "The `Status` component mounts and calls the `useStatusPageFetch` hook, which is responsible for initiating the API call to fetch the status page data from the backend.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/StatusPage/Status/index.jsx", + "startLine": "29", + "endLine": "29", + "relevantVariables": [ + "useStatusPageFetch" + ] + }, + "inputDataExample": "{\"isCreate\":false, \"url\":\"my-awesome-service\"}", + "outputDataExample": "{\"isCreate\":false, \"url\":\"my-awesome-service\"}" + }, + { + "simStepId": "b57bb249-9066-454e-aa8c-89108fdfcc2e", + "diagramNodeId": "ecfb2d04-2681-4f16-af75-632f69e1a868", + "simStepLabel": "View Status Page: Client Hook Sends API Request", + "simStepDescription": "The `useStatusPageFetch` hook uses the `networkService` to send a GET request to the backend API to retrieve the status page data based on its URL.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx", + "startLine": "15", + "endLine": "18", + "relevantVariables": [ + "fetchStatusPage", + "networkService.getStatusPageByUrl" + ] + }, + "inputDataExample": "{\"url\":\"my-awesome-service\", \"type\":\"uptime\"}", + "outputDataExample": "{}" + }, + { + "simStepId": "de0714d2-bf1e-4a1a-a389-60793de8b772", + "diagramNodeId": "ae1c9341-0e39-4d91-a9ac-5cc8a627c247", + "simStepLabel": "View Status Page: API Call to Backend", + "simStepDescription": "The `NetworkService` class makes a GET request to the `/api/v1/status-page/:url` endpoint to fetch the status page details.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "896", + "endLine": "902", + "relevantVariables": [ + "getStatusPageByUrl", + "axiosInstance.get" + ] + }, + "inputDataExample": "{\"url\":\"my-awesome-service\",\"type\":\"uptime\"}", + "outputDataExample": "{\"url\":\"my-awesome-service\",\"type\":\"uptime\"}" + }, + { + "simStepId": "477c6821-b29b-4b38-8930-ced21b8a75e5", + "diagramNodeId": "2e26cd2f-1413-4c16-8e63-ea7808ab6c52", + "simStepLabel": "View Status Page: Backend Route Handling", + "simStepDescription": "The backend router receives the GET request on `/api/v1/status-page/:url` and forwards it to the `getStatusPageByUrl` method in the `StatusPageController`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/statusPageRoute.js", + "startLine": "16", + "endLine": "16", + "relevantVariables": [ + "router.get", + "this.statusPageController.getStatusPageByUrl" + ] + }, + "inputDataExample": "{\"method\":\"GET\",\"url\":\"/api/v1/status-page/my-awesome-service\"}", + "outputDataExample": "{\"req\":{...},\"res\":{...}}" + }, + { + "simStepId": "b79b8922-4de6-477d-8648-37b4bd9a6767", + "diagramNodeId": "5735f30c-b55f-444a-9236-f3d75f68ed27", + "simStepLabel": "View Status Page: Data Passed to Controller", + "simStepDescription": "The router passes the request object, containing the URL in `req.params`, to the controller method.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/statusPageRoute.js", + "startLine": "16", + "endLine": "16", + "relevantVariables": [ + "statusPageController.getStatusPageByUrl" + ] + }, + "inputDataExample": "{\"req\":{\"params\":{\"url\":\"my-awesome-service\"}}}", + "outputDataExample": "{\"req\":{\"params\":{\"url\":\"my-awesome-service\"}}}" + }, + { + "simStepId": "5bdbd179-ce8f-4171-a073-af72a99b4468", + "diagramNodeId": "9f217674-4cd7-4ef8-b088-d5620042a7ba", + "simStepLabel": "View Status Page: Controller Logic", + "simStepDescription": "The `StatusPageController` validates the request parameters and calls the `statusPageModule` to retrieve the status page data from the database.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/statusPageController.js", + "startLine": "75", + "endLine": "78", + "relevantVariables": [ + "getStatusPageByUrl", + "this.db.statusPageModule.getStatusPageByUrl" + ] + }, + "inputDataExample": "{\"req\":{\"params\":{\"url\":\"my-awesome-service\"}}}", + "outputDataExample": "{\"url\":\"my-awesome-service\"}" + }, + { + "simStepId": "788307ef-4cfa-4f5a-b2e9-a0bfc7a378d8", + "diagramNodeId": "dace45d6-fb25-4dae-b940-49ef7d77bbed", + "simStepLabel": "View Status Page: URL Sent to DB Module", + "simStepDescription": "The controller passes the URL string to the `getStatusPageByUrl` function in the database module.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/statusPageController.js", + "startLine": "78", + "endLine": "78", + "relevantVariables": [ + "this.db.statusPageModule.getStatusPageByUrl" + ] + }, + "inputDataExample": "{\"url\":\"my-awesome-service\"}", + "outputDataExample": "{\"url\":\"my-awesome-service\"}" + }, + { + "simStepId": "ceb044e9-6545-4d04-bf79-ec8146eb8ec9", + "diagramNodeId": "26da60f9-dde4-44ec-be89-91858cc14f90", + "simStepLabel": "View Status Page: Database Record Retrieval", + "simStepDescription": "The `StatusPageModule` executes a complex MongoDB aggregation pipeline to find the status page by its URL, and then look up and join data for all associated monitors and their recent checks.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/statusPageModule.js", + "startLine": "89", + "endLine": "299", + "relevantVariables": [ + "getStatusPage", + "this.StatusPage.aggregate" + ] + }, + "inputDataExample": "{\"url\":\"my-awesome-service\"}", + "outputDataExample": "{\"statusPage\":{\"_id\":\"...\",\"companyName\":\"My Awesome Service\",...},\"monitors\":[{\"_id\":\"...\",\"name\":\"Main Website\",...}]}" + }, + { + "simStepId": "4d3fad92-8fa9-4a8f-be3b-f09eda2f8dd4", + "diagramNodeId": "137a5ab7-1e49-42ec-869d-58f7f2380cb3", + "simStepLabel": "View Status Page: Database Result Returned", + "simStepDescription": "The aggregated status page and monitor data is returned from the database module to the controller.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/statusPageModule.js", + "startLine": "291", + "endLine": "291", + "relevantVariables": [ + "statusPage", + "normalizedMonitors" + ] + }, + "inputDataExample": "{\"statusPage\":{...},\"monitors\":[...]}", + "outputDataExample": "{\"statusPage\":{...},\"monitors\":[...]}" + }, + { + "simStepId": "83224406-2f1d-4030-9beb-d65c2baf544d", + "diagramNodeId": "d76e70e0-41f9-46cc-b775-b2027c5a0d5b", + "simStepLabel": "View Status Page: Controller Sends Success Response", + "simStepDescription": "The controller receives the status page data and sends a successful JSON response back to the client.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/statusPageController.js", + "startLine": "79", + "endLine": "82", + "relevantVariables": [ + "res.success" + ] + }, + "inputDataExample": "{\"statusPage\":{...},\"monitors\":[...]}", + "outputDataExample": "{\"msg\":\"Got status page by url successfully\",\"data\":{\"statusPage\":{...},\"monitors\":[...]}}" + }, + { + "simStepId": "d5f3c300-9777-4e09-bb05-f1281c2ff29a", + "diagramNodeId": "04a33e4f-1dfe-4c5a-8b30-01ed0591c380", + "simStepLabel": "View Status Page: UI Receives Data", + "simStepDescription": "The `useStatusPageFetch` hook receives the API response, processes the data, and updates the component's state, triggering a re-render.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx", + "startLine": "20", + "endLine": "26", + "relevantVariables": [ + "setStatusPage", + "setMonitors", + "getMonitorWithPercentage" + ] + }, + "inputDataExample": "{\"data\":{\"data\":{\"statusPage\":{...},\"monitors\":[...]}}}", + "outputDataExample": "{\"statusPage\":{...},\"monitors\":[...]}" + }, + { + "simStepId": "3d51e811-a659-442e-97e0-00798cd15647", + "diagramNodeId": "f59b91de-5196-4a43-9d57-3f28402c0672", + "simStepLabel": "View Status Page: Component Renders UI", + "simStepDescription": "The `Status` component re-renders with the fetched data. It displays the company name, logo, overall status bar, and a list of monitors with their individual status and historical uptime charts.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/StatusPage/Status/index.jsx", + "startLine": "145", + "endLine": "163", + "relevantVariables": [ + "ControlsHeader", + "StatusBar", + "MonitorsList" + ] + }, + "inputDataExample": "{\"statusPage\":{...},\"monitors\":[...]}", + "outputDataExample": "{}" + } + ], + "description": "
  • Allows users to create dedicated pages to communicate the status of their services to internal teams or external customers
  • - Users can create a status page with a custom URL, company name, and logo
  • - Select which monitors to display on the status page, providing granular control over what information is shared
  • - Options to show or hide historical charts and uptime percentages for transparency
  • - Includes settings for custom CSS and an optional link back to the admin login page
", + "simulationNodesAndEdges": { + "00289d11-0015-4b2a-9c5b-dc9fd0f32b80": { + "simStepIds": [ + "de7b5d1f-b370-42a4-a410-4178f1e2d3a6" + ] + }, + "aa5d6d07-05f0-45f3-b19a-199af50e35f1": { + "simStepIds": [ + "3c99ae6a-b671-4f47-bed0-ddccd8ff5d35" + ] + }, + "3efddcad-588f-4492-9808-b0c81c823c96": { + "simStepIds": [ + "84f11567-b3c4-42a2-aa7f-d78c1412099a" + ] + }, + "f15ef77e-380f-4768-98cb-7f0dd4113841": { + "simStepIds": [ + "01694730-8a75-4949-8815-bb4d6e3e2b42" + ] + }, + "e234917e-a950-48f5-b864-cd957b2216e1": { + "simStepIds": [ + "b6fa93cd-6fca-4d9e-969a-5400e2437ee8" + ] + }, + "058ded1f-83ce-40a7-a99d-d178c81fe557": { + "simStepIds": [ + "2a115421-5175-4c8f-8cdd-62f3fe148af5" + ] + }, + "30aaee38-f509-4e19-a917-e6d2f4bba61c": { + "simStepIds": [ + "132354d7-df7a-4e03-9d00-16d54f836a1b" + ] + }, + "ecfb2d04-2681-4f16-af75-632f69e1a868": { + "simStepIds": [ + "b57bb249-9066-454e-aa8c-89108fdfcc2e" + ] + }, + "2e26cd2f-1413-4c16-8e63-ea7808ab6c52": { + "simStepIds": [ + "477c6821-b29b-4b38-8930-ced21b8a75e5" + ] + }, + "9f217674-4cd7-4ef8-b088-d5620042a7ba": { + "simStepIds": [ + "5bdbd179-ce8f-4171-a073-af72a99b4468" + ] + }, + "26da60f9-dde4-44ec-be89-91858cc14f90": { + "simStepIds": [ + "ceb044e9-6545-4d04-bf79-ec8146eb8ec9" + ] + }, + "d76e70e0-41f9-46cc-b775-b2027c5a0d5b": { + "simStepIds": [ + "83224406-2f1d-4030-9beb-d65c2baf544d" + ] + }, + "f59b91de-5196-4a43-9d57-3f28402c0672": { + "simStepIds": [ + "3d51e811-a659-442e-97e0-00798cd15647" + ] + }, + "de23eee2-b60a-4236-a890-3c5b78388bb3": { + "simStepIds": [ + "d419f7b6-7b9b-4494-9fad-83e3ce208ca4" + ] + }, + "d7faa713-4204-4a97-a172-d621da36c287": { + "simStepIds": [ + "8a2ab372-73db-4b9f-bc0e-6a20f15a4d79" + ] + }, + "a3f28fa6-8268-4c13-95a0-83e3b1abdcb0": { + "simStepIds": [ + "0923a533-c572-4da3-ad87-cdfff111f3bb" + ] + }, + "9735611c-924b-49aa-92e8-10d8a86ac288": { + "simStepIds": [ + "67603f03-c088-4ed0-9f18-072435134cdd" + ] + }, + "40cb7d7b-2888-44de-bb7d-7735af70ba34": { + "simStepIds": [ + "ea4b3532-baae-4890-a73e-2ca385d0319e" + ] + }, + "c3f83811-8674-4dbb-9f84-79499f97fb1f": { + "simStepIds": [ + "210acc07-81c3-4394-b566-41f6031e8eeb" + ] + }, + "0575bf87-5362-4988-b895-7e514148d420": { + "simStepIds": [ + "2ba49e28-1cac-47f5-9118-ab930788736a" + ] + }, + "ae1c9341-0e39-4d91-a9ac-5cc8a627c247": { + "simStepIds": [ + "de0714d2-bf1e-4a1a-a389-60793de8b772" + ] + }, + "5735f30c-b55f-444a-9236-f3d75f68ed27": { + "simStepIds": [ + "b79b8922-4de6-477d-8648-37b4bd9a6767" + ] + }, + "dace45d6-fb25-4dae-b940-49ef7d77bbed": { + "simStepIds": [ + "788307ef-4cfa-4f5a-b2e9-a0bfc7a378d8" + ] + }, + "137a5ab7-1e49-42ec-869d-58f7f2380cb3": { + "simStepIds": [ + "4d3fad92-8fa9-4a8f-be3b-f09eda2f8dd4" + ] + }, + "04a33e4f-1dfe-4c5a-8b30-01ed0591c380": { + "simStepIds": [ + "d5f3c300-9777-4e09-bb05-f1281c2ff29a" + ] + } + }, + "isAIGenerated": true, + "keywords": "StatusPage, createStatusPage, getStatusPageByUrl", + "generationPrompt": "Customizable Public and Private Status Pages", + "generationKeywords": "StatusPage, createStatusPage, getStatusPageByUrl" + }, + "Incident History, Analysis, and Management": { + "name": "Incident History, Analysis, and Management", + "simSteps": [ + { + "simStepId": "35195328-9847-4678-b014-9331553f0d19", + "diagramNodeId": "78a8a7c5-41d3-4453-a7f7-ac632bbef288", + "simStepLabel": "Navigate to Monitor Details", + "simStepDescription": "The user navigates to the details page for a specific uptime monitor. The `UptimeDetails` component mounts and initializes its state and hooks.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/Details/index.jsx", + "startLine": "27", + "endLine": "188", + "relevantVariables": [ + "UptimeDetails", + "useParams", + "useFetchChecksByMonitor" + ] + }, + "inputDataExample": "{\"url\": \"/uptime/65a12f60e9a5d109f7596a77\"}", + "outputDataExample": "{\"monitorId\": \"65a12f60e9a5d109f7596a77\"}" + }, + { + "simStepId": "49979250-1a32-4da5-95e0-58652fe218f9", + "diagramNodeId": "e90e4943-6108-46e3-8a2c-a9a1c7361279", + "simStepLabel": "Trigger Check History Fetch", + "simStepDescription": "The `UptimeDetails` component calls the `useFetchChecksByMonitor` hook to retrieve the historical check data for the specified monitor.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/Details/index.jsx", + "startLine": "69", + "endLine": "77", + "relevantVariables": [ + "useFetchChecksByMonitor" + ] + }, + "inputDataExample": "{\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"type\": \"http\", \"sortOrder\": \"desc\", \"page\": 0, \"rowsPerPage\": 10}", + "outputDataExample": "{\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"type\": \"http\", \"sortOrder\": \"desc\", \"page\": 0, \"rowsPerPage\": 10}" + }, + { + "simStepId": "8dc4347c-3e9e-40d7-ae6f-54684f2e6956", + "diagramNodeId": "4a5847d0-91a0-4b4c-8517-2f4ff9a7e8d4", + "simStepLabel": "Fetch Monitor Checks Hook", + "simStepDescription": "The `useFetchChecksByMonitor` custom hook executes. It sets the loading state and prepares to call the network service to fetch the data from the backend API.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Hooks/v1/checkHooks.js", + "startLine": "89", + "endLine": "121", + "relevantVariables": [ + "useFetchChecksByMonitor", + "useEffect", + "networkService.getChecksByMonitor" + ] + }, + "inputDataExample": "{\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"page\": 0, \"rowsPerPage\": 10, \"sortOrder\": \"desc\"}", + "outputDataExample": "{\"config\": {\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"sortOrder\": \"desc\", \"page\": \"0\", \"rowsPerPage\": \"10\", \"status\": undefined}}" + }, + { + "simStepId": "336b534d-8d6a-4639-af8e-889b38f51ab1", + "diagramNodeId": "2b8ae59e-ed8d-4837-9a9b-64eb480f982c", + "simStepLabel": "API Request for Check History", + "simStepDescription": "The `NetworkService` constructs and sends an HTTP GET request to the backend API endpoint to retrieve the paginated list of checks for the specified monitor.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "551", + "endLine": "562", + "relevantVariables": [ + "getChecksByMonitor", + "this.axiosInstance.get" + ] + }, + "inputDataExample": "{\"config\": {\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"sortOrder\": \"desc\", \"page\": \"0\", \"rowsPerPage\": \"10\"}}", + "outputDataExample": "{\"config\": {\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"sortOrder\": \"desc\", \"page\": \"0\", \"rowsPerPage\": \"10\"}}" + }, + { + "simStepId": "c5fd192f-ea28-48a0-b3e7-5bc04f521192", + "diagramNodeId": "cdd090be-0b62-45b4-baa0-fec01fc3ffd1", + "simStepLabel": "Route Request to Controller", + "simStepDescription": "The backend Express router receives the incoming GET request and matches it to the appropriate route, forwarding the request to the `getChecksByMonitor` method in the `CheckController`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/checkRoute.js", + "startLine": "19", + "endLine": "19", + "relevantVariables": [ + "this.router.get" + ] + }, + "inputDataExample": "{\"request\": \"GET /api/v1/checks/65a12f60e9a5d109f7596a77?sortOrder=desc&page=0&rowsPerPage=10\"}", + "outputDataExample": "{\"controllerMethod\": \"getChecksByMonitor\"}" + }, + { + "simStepId": "4f73f0fe-b390-4fe1-a354-eb0857835126", + "diagramNodeId": "839ba9f6-8a34-4919-a04a-13fba1795c61", + "simStepLabel": "Forward Request to Service Layer", + "simStepDescription": "The `CheckController` validates the request parameters and calls the `CheckService` to handle the business logic of fetching the monitor checks.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/checkController.js", + "startLine": "81", + "endLine": "85", + "relevantVariables": [ + "getChecksByMonitor", + "this.checkService.getChecksByMonitor" + ] + }, + "inputDataExample": "{\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"query\": {\"sortOrder\": \"desc\", \"page\": \"0\", \"rowsPerPage\": \"10\"}, \"teamId\": \"65a12f50e9a5d109f7596a66\"}", + "outputDataExample": "{\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"query\": {\"sortOrder\": \"desc\", \"page\": \"0\", \"rowsPerPage\": \"10\"}, \"teamId\": \"65a12f50e9a5d109f7596a66\"}" + }, + { + "simStepId": "83fea05d-8306-4a73-a43f-882cdd14f50f", + "diagramNodeId": "499b46a2-f2a2-4ccd-aecd-012eade2bd23", + "simStepLabel": "Fetch Checks from Database", + "simStepDescription": "The `CheckService` calls the `checkModule` to interact with the database. It passes the necessary parameters to query for the check history.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/checkService.js", + "startLine": "36", + "endLine": "45", + "relevantVariables": [ + "getChecksByMonitor", + "this.db.checkModule.getChecksByMonitor" + ] + }, + "inputDataExample": "{\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"query\": {\"sortOrder\": \"desc\", \"page\": \"0\", \"rowsPerPage\": \"10\"}, \"teamId\": \"65a12f50e9a5d109f7596a66\"}", + "outputDataExample": "{\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"sortOrder\": \"desc\", \"dateRange\": undefined, \"filter\": undefined, \"ack\": undefined, \"page\": \"0\", \"rowsPerPage\": \"10\", \"status\": undefined}" + }, + { + "simStepId": "65da1a9e-041c-4656-9c5b-0a92b761a9be", + "diagramNodeId": "42d3bc70-701a-4be0-96b4-d6c2f0519b49", + "simStepLabel": "Execute Database Query", + "simStepDescription": "The `checkModule` constructs and executes a MongoDB aggregation pipeline on the `Check` collection to find, sort, and paginate the historical checks for the given monitor.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/checkModule.js", + "startLine": "28", + "endLine": "108", + "relevantVariables": [ + "getChecksByMonitor", + "this.Check.aggregate" + ] + }, + "inputDataExample": "{\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"sortOrder\": -1, \"page\": 0, \"rowsPerPage\": 10}", + "outputDataExample": "{\"monitorId\": \"65a12f60e9a5d109f7596a77\", \"sortOrder\": -1, \"page\": 0, \"rowsPerPage\": 10}" + }, + { + "simStepId": "5768fdc5-045c-48ef-b0c9-79769403648f", + "diagramNodeId": "14df9f01-1358-442d-96f5-eab9a4c4abbf", + "simStepLabel": "Return Check Data", + "simStepDescription": "The database returns the query results, which flow back up through the `checkModule` and `checkService`. The `checkController` then formats a successful JSON response.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/checkController.js", + "startLine": "87", + "endLine": "90", + "relevantVariables": [ + "res.success" + ] + }, + "inputDataExample": "{\"checksCount\": 152, \"checks\": [{\"_id\": \"...\", \"status\": false}, {\"...\"}]}", + "outputDataExample": "{\"msg\": \"Got checks successfully\", \"data\": {\"checksCount\": 152, \"checks\": [{\"_id\": \"...\"}]}}" + }, + { + "simStepId": "2c1bdec8-d0e6-4163-a99c-f27899b987c5", + "diagramNodeId": "6c5b028c-356c-43c8-a35a-b47448cd8695", + "simStepLabel": "API Response with Check History", + "simStepDescription": "The backend sends the HTTP response containing the historical check data back to the client.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "560", + "endLine": "560", + "relevantVariables": [ + "this.axiosInstance.get" + ] + }, + "inputDataExample": "{\"success\": true, \"msg\": \"Got checks successfully\", \"data\": {\"checksCount\": 152, \"checks\": [{\"_id\": \"65a13f60e9a5d109f7596b88\", \"status\": false, \"httpStatusCode\": 503, \"responseTime\": 1200}, {\"_id\": \"65a13f50e9a5d109f7596b77\", \"status\": true, \"httpStatusCode\": 200, \"responseTime\": 250}]}}", + "outputDataExample": "{\"success\": true, \"msg\": \"Got checks successfully\", \"data\": {\"checksCount\": 152, \"checks\": [{\"_id\": \"65a13f60e9a5d109f7596b88\", \"status\": false, \"httpStatusCode\": 503, \"responseTime\": 1200}, {\"_id\": \"65a13f50e9a5d109f7596b77\", \"status\": true, \"httpStatusCode\": 200, \"responseTime\": 250}]}}" + }, + { + "simStepId": "faba07f3-7460-47ae-924d-f92f5b2e821d", + "diagramNodeId": "ceb9cda3-968d-44aa-b223-49902adb93b4", + "simStepLabel": "Update Frontend State", + "simStepDescription": "The `useFetchChecksByMonitor` hook receives the data from the API call and updates the component's state with the new list of checks and the total count.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Hooks/v1/checkHooks.js", + "startLine": "109", + "endLine": "110", + "relevantVariables": [ + "setChecks", + "setChecksCount" + ] + }, + "inputDataExample": "{\"data\": {\"checksCount\": 152, \"checks\": [{\"_id\": \"...\"}]}}", + "outputDataExample": "{\"checks\": [{\"_id\": \"...\"}], \"checksCount\": 152}" + }, + { + "simStepId": "9405dce1-0565-4dfa-9539-a048472acda9", + "diagramNodeId": "74b0dd89-9fea-4219-af1f-9571316a9d5a", + "simStepLabel": "Pass Data to UI Components", + "simStepDescription": "The state update triggers a re-render of the `UptimeDetails` page, passing the `checks` and `checksCount` as props to child components like `ResponseTable`.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/Details/index.jsx", + "startLine": "175", + "endLine": "186", + "relevantVariables": [ + "ResponseTable", + "checks", + "checksCount" + ] + }, + "inputDataExample": "{\"checks\": [{\"_id\": \"65a13f60e9a5d109f7596b88\", \"status\": false, \"httpStatusCode\": 503}, {\"_id\": \"65a13f50e9a5d109f7596b77\", \"status\": true, \"httpStatusCode\": 200}], \"checksCount\": 152}", + "outputDataExample": "{\"checks\": [{\"_id\": \"65a13f60e9a5d109f7596b88\", \"status\": false, \"httpStatusCode\": 503}, {\"_id\": \"65a13f50e9a5d109f7596b77\", \"status\": true, \"httpStatusCode\": 200}], \"checksCount\": 152}" + }, + { + "simStepId": "2b1651cd-2e70-4cc6-b2bb-23f94fa73d66", + "diagramNodeId": "c996f7ca-9384-4252-b343-7bfc049d14bb", + "simStepLabel": "Render Historical Data Table", + "simStepDescription": "The `ResponseTable` component receives the list of checks and renders them in a data table, allowing the user to view the incident history, including status, response time, and when the check occurred.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx", + "startLine": "11", + "endLine": "92", + "relevantVariables": [ + "ResponseTable", + "Table", + "TablePagination" + ] + }, + "inputDataExample": "{\"checks\": [{\"_id\": \"65a13f60e9a5d109f7596b88\", \"status\": false, \"httpStatusCode\": 503}, {\"_id\": \"65a13f50e9a5d109f7596b77\", \"status\": true, \"httpStatusCode\": 200}]}", + "outputDataExample": "{\"tableRows\": [{\"id\": \"status\", \"content\": \"Status\", \"render\": \"...\"}, {\"id\": \"httpStatus\", \"content\": \"HTTP Status\", \"render\": \"...\"}]}" + } + ], + "description": "
  • Provides detailed historical data and tools to manage monitoring events, which are treated as incidents
  • - Every check result is stored, creating a comprehensive log of uptime, downtime, and response time history for each monitor
  • - The UI includes detailed views with charts and tables to analyze performance trends and past incidents
  • - Users can acknowledge incidents (failed checks), which helps in tracking the response to an outage
  • - All historical check data can be purged on a per-monitor or per-team basis
", + "simulationNodesAndEdges": { + "78a8a7c5-41d3-4453-a7f7-ac632bbef288": { + "simStepIds": [ + "35195328-9847-4678-b014-9331553f0d19" + ] + }, + "4a5847d0-91a0-4b4c-8517-2f4ff9a7e8d4": { + "simStepIds": [ + "8dc4347c-3e9e-40d7-ae6f-54684f2e6956" + ] + }, + "cdd090be-0b62-45b4-baa0-fec01fc3ffd1": { + "simStepIds": [ + "c5fd192f-ea28-48a0-b3e7-5bc04f521192" + ] + }, + "499b46a2-f2a2-4ccd-aecd-012eade2bd23": { + "simStepIds": [ + "83fea05d-8306-4a73-a43f-882cdd14f50f" + ] + }, + "14df9f01-1358-442d-96f5-eab9a4c4abbf": { + "simStepIds": [ + "5768fdc5-045c-48ef-b0c9-79769403648f" + ] + }, + "ceb9cda3-968d-44aa-b223-49902adb93b4": { + "simStepIds": [ + "faba07f3-7460-47ae-924d-f92f5b2e821d" + ] + }, + "c996f7ca-9384-4252-b343-7bfc049d14bb": { + "simStepIds": [ + "2b1651cd-2e70-4cc6-b2bb-23f94fa73d66" + ] + }, + "e90e4943-6108-46e3-8a2c-a9a1c7361279": { + "simStepIds": [ + "49979250-1a32-4da5-95e0-58652fe218f9" + ] + }, + "2b8ae59e-ed8d-4837-9a9b-64eb480f982c": { + "simStepIds": [ + "336b534d-8d6a-4639-af8e-889b38f51ab1" + ] + }, + "839ba9f6-8a34-4919-a04a-13fba1795c61": { + "simStepIds": [ + "4f73f0fe-b390-4fe1-a354-eb0857835126" + ] + }, + "42d3bc70-701a-4be0-96b4-d6c2f0519b49": { + "simStepIds": [ + "65da1a9e-041c-4656-9c5b-0a92b761a9be" + ] + }, + "6c5b028c-356c-43c8-a35a-b47448cd8695": { + "simStepIds": [ + "2c1bdec8-d0e6-4163-a99c-f27899b987c5" + ] + }, + "74b0dd89-9fea-4219-af1f-9571316a9d5a": { + "simStepIds": [ + "9405dce1-0565-4dfa-9539-a048472acda9" + ] + } + }, + "isAIGenerated": true, + "keywords": "Check, getChecksByMonitor, ResponseTable", + "generationPrompt": "Incident History, Analysis, and Management", + "generationKeywords": "Check, getChecksByMonitor, ResponseTable" + }, + "Website Performance and Page Speed Monitoring": { + "name": "Website Performance and Page Speed Monitoring", + "simSteps": [ + { + "simStepId": "b0cc6e6a-961e-4ea9-aa8a-6bfa44d6ba12", + "diagramNodeId": "0622b212-3a4a-484d-9b7a-125864d785cd", + "simStepLabel": "Create PageSpeed Monitor", + "simStepDescription": "The user navigates to the PageSpeed creation page, fills in the details for a new monitor (URL, name, interval), and submits the form. This triggers the `handleSubmit` function, which calls the `createMonitor` hook to send the data to the backend API.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/PageSpeed/Create/index.jsx", + "startLine": "111", + "endLine": "143", + "relevantVariables": [ + "handleSubmit", + "createMonitor", + "updateMonitor", + "monitor", + "form" + ] + }, + "inputDataExample": "{\"formState\": {\"url\": \"https://example.com\",\"name\": \"Example Homepage\",\"type\": \"pagespeed\",\"interval\": 180000}}", + "outputDataExample": "{\"monitor\": {\"url\": \"https://example.com\",\"name\": \"Example Homepage\",\"type\": \"pagespeed\",\"interval\": 180000}, \"redirect\": \"/pagespeed\"}" + }, + { + "simStepId": "f8df1cad-6051-466e-a13e-47d710ba1c14", + "diagramNodeId": "b8e83b0a-69ff-4c34-a7fe-cb8c9ba85438", + "simStepLabel": "API Call: Create Monitor", + "simStepDescription": "The frontend sends a POST request to the backend API to create a new monitor record in the database. This request includes the monitor's configuration details.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/PageSpeed/Create/index.jsx", + "startLine": "119", + "endLine": "119", + "relevantVariables": [ + "createMonitor" + ] + }, + "inputDataExample": "{\"monitor\": {\"url\": \"https://example.com\",\"name\": \"Example Homepage\",\"type\": \"pagespeed\",\"notifications\": [],\"interval\": 180000}, \"redirect\": \"/pagespeed\"}", + "outputDataExample": "{\"monitor\": {\"url\": \"https://example.com\",\"name\": \"Example Homepage\",\"type\": \"pagespeed\",\"notifications\": [],\"interval\": 180000}, \"redirect\": \"/pagespeed\"}" + }, + { + "simStepId": "53968267-0c6e-42f4-88b3-cd90f44532ae", + "diagramNodeId": "6527fb47-157e-4bba-9aed-adde061c5689", + "simStepLabel": "Backend Requests PageSpeed Analysis", + "simStepDescription": "A background job for the newly created monitor triggers the `NetworkService` to request a performance analysis from the Google PageSpeed Insights API. It constructs the API URL using the monitor's target URL and the configured PageSpeed API key.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v2/infrastructure/NetworkService.ts", + "startLine": "129", + "endLine": "162", + "relevantVariables": [ + "requestPagespeed", + "apiKey", + "pagespeedUrl", + "pagespeedResponse" + ] + }, + "inputDataExample": "{\"monitor\": {\"_id\": \"64b9a6b3e1b6f0b3e8a7d1c0\",\"url\": \"https://example.com\",\"type\": \"pagespeed\"}}", + "outputDataExample": "{\"statusResponse\": {\"monitorId\": \"64b9a6b3e1b6f0b3e8a7d1c0\",\"status\": \"up\",\"responseTime\": 450, \"payload\": {\"lighthouseResult\": {...}}}}" + }, + { + "simStepId": "0b380571-3896-4365-b5dc-187494f02e0d", + "diagramNodeId": "0d3c8091-3d91-4e42-9b46-7e35ed726a7a", + "simStepLabel": "Google PageSpeed API Response", + "simStepDescription": "The Google PageSpeed Insights API responds with a detailed JSON payload containing the Lighthouse analysis results, including scores for performance, accessibility, SEO, and best practices.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v2/infrastructure/NetworkService.ts", + "startLine": "156", + "endLine": "156", + "relevantVariables": [ + "payload" + ] + }, + "inputDataExample": "{\"lighthouseResult\": {\"categories\": {\"performance\": {\"score\": 0.92},\"accessibility\": {\"score\": 0.98},\"best-practices\": {\"score\": 1},\"seo\": {\"score\": 0.95}},\"audits\": {\"first-contentful-paint\": {\"score\": 0.99,\"numericValue\": 850},\"largest-contentful-paint\": {\"score\": 0.91,\"numericValue\": 1450}}}}", + "outputDataExample": "{\"lighthouseResult\": {\"categories\": {\"performance\": {\"score\": 0.92},\"accessibility\": {\"score\": 0.98},\"best-practices\": {\"score\": 1},\"seo\": {\"score\": 0.95}},\"audits\": {\"first-contentful-paint\": {\"score\": 0.99,\"numericValue\": 850},\"largest-contentful-paint\": {\"score\": 0.91,\"numericValue\": 1450}}}}" + }, + { + "simStepId": "01206383-2db8-4b18-84d0-be2a0289c196", + "diagramNodeId": "f89cbd87-c961-4f8b-92b6-714da57718d5", + "simStepLabel": "Backend Processes Lighthouse Data", + "simStepDescription": "The `CheckService` receives the raw data from the Google API. The `buildPagespeedCheck` function extracts and transforms the relevant scores and audit details into a structured `Check` object, preparing it for database storage.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v2/business/CheckService.ts", + "startLine": "82", + "endLine": "102", + "relevantVariables": [ + "buildPagespeedCheck", + "isPagespeedPayload", + "lighthouseResult", + "check.lighthouse" + ] + }, + "inputDataExample": "{\"statusResponse\": {\"payload\": {\"lighthouseResult\": {\"categories\": {\"performance\": {\"score\": 0.92},\"accessibility\": {\"score\": 0.98},\"best-practices\": {\"score\": 1},\"seo\": {\"score\": 0.95}},\"audits\": {\"cumulative-layout-shift\": {},\"speed-index\": {},\"first-contentful-paint\": {},\"largest-contentful-paint\": {},\"total-blocking-time\": {}}}}}}", + "outputDataExample": "{\"check\": {\"lighthouse\": {\"accessibility\": 0.98,\"bestPractices\": 1,\"seo\": 0.95,\"performance\": 0.92,\"audits\": {\"cls\": {},\"si\": {},\"fcp\": {},\"lcp\": {},\"tbt\": {}}}}}" + }, + { + "simStepId": "a629a102-0df1-4798-a7a2-45c8f6cdad29", + "diagramNodeId": "27c7191b-80a2-475b-9371-e106108aff1c", + "simStepLabel": "Store Processed Check Data", + "simStepDescription": "The processed and structured check data, containing the key performance metrics, is saved to the 'checks' collection in the database, associated with the corresponding monitor ID.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/db/v2/models/checks/Check.ts", + "startLine": "237", + "endLine": "268", + "relevantVariables": [ + "lighthouse" + ] + }, + "inputDataExample": "{\"check\": {\"monitorId\": \"64b9a6b3e1b6f0b3e8a7d1c0\", \"status\": \"up\", \"responseTime\": 450, \"lighthouse\": {\"performance\": 0.92, \"accessibility\": 0.98, \"bestPractices\": 1, \"seo\": 0.95, \"audits\": {...}}}}", + "outputDataExample": "{\"check\": {\"monitorId\": \"64b9a6b3e1b6f0b3e8a7d1c0\", \"status\": \"up\", \"responseTime\": 450, \"lighthouse\": {\"performance\": 0.92, \"accessibility\": 0.98, \"bestPractices\": 1, \"seo\": 0.95, \"audits\": {...}}}}" + }, + { + "simStepId": "5616ac6d-c281-4879-82f1-6590c721c8f7", + "diagramNodeId": "ca6ee27b-1fbf-49de-978a-d6de259cc3d5", + "simStepLabel": "Backend Aggregates Performance Data", + "simStepDescription": "When a user views the details of a PageSpeed monitor, the frontend requests historical data. The `MonitorService` on the backend uses a MongoDB aggregation pipeline to group checks by time intervals and calculate average scores for metrics like performance, SEO, etc. This is defined by helper functions like `getPageSpeedGroup`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v2/business/MonitorService.ts", + "startLine": "106", + "endLine": "120", + "relevantVariables": [ + "getPageSpeedGroup", + "accessibility", + "bestPractices", + "seo", + "performance" + ] + }, + "inputDataExample": "{\"monitorId\": \"64b9a6b3e1b6f0b3e8a7d1c0\", \"range\": \"7d\"}", + "outputDataExample": "[{\"_id\": \"2023-07-20T10:00:00Z\", \"avgResponseTime\": 450, \"performance\": 0.92, \"seo\": 0.95, \"accessibility\": 0.98, \"bestPractices\": 1.0}, {\"_id\": \"2023-07-20T11:00:00Z\", \"avgResponseTime\": 465, \"performance\": 0.91, \"seo\": 0.95, \"accessibility\": 0.98, \"bestPractices\": 1.0}]" + }, + { + "simStepId": "4ed43eaf-fa8f-4fce-af87-8a5f74dd5c08", + "diagramNodeId": "7be432cd-2587-4eab-abdc-f7b9c1923597", + "simStepLabel": "Aggregated Data Transmitted to Frontend", + "simStepDescription": "The backend sends the aggregated and time-series formatted performance data to the frontend in a JSON response.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v2/business/MonitorService.ts", + "startLine": "368", + "endLine": "372", + "relevantVariables": [ + "Check.aggregate" + ] + }, + "inputDataExample": "[{\"_id\": \"2023-07-20T10:00:00Z\", \"performance\": 0.92, \"seo\": 0.95, \"accessibility\": 0.98}, {\"_id\": \"2023-07-20T11:00:00Z\", \"performance\": 0.91, \"seo\": 0.95, \"accessibility\": 0.98}]", + "outputDataExample": "[{\"_id\": \"2023-07-20T10:00:00Z\", \"performance\": 0.92, \"seo\": 0.95, \"accessibility\": 0.98}, {\"_id\": \"2023-07-20T11:00:00Z\", \"performance\": 0.91, \"seo\": 0.95, \"accessibility\": 0.98}]" + }, + { + "simStepId": "0ec0eb06-05eb-4204-b077-9e47eb2cc316", + "diagramNodeId": "9ef9e218-4742-487a-9a88-5c70e788ccdc", + "simStepLabel": "Frontend Renders Performance Charts", + "simStepDescription": "The `PageSpeedDetails` component receives the historical data from the backend. It then passes this data to specialized child components like `PageSpeedAreaChart` and `PerformanceReport` to render visualizations of the website's performance over time.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/PageSpeed/Details/index.jsx", + "startLine": "91", + "endLine": "126", + "relevantVariables": [ + "PageSpeedStatusBoxes", + "MonitorTimeFrameHeader", + "PageSpeedAreaChart", + "PerformanceReport", + "monitor", + "audits" + ] + }, + "inputDataExample": "{\"monitor\": {\"checks\": [{\"createdAt\": \"2023-07-20T10:00:00Z\", \"performance\": 0.92, \"seo\": 0.95}, {\"createdAt\": \"2023-07-20T11:00:00Z\", \"performance\": 0.91, \"seo\": 0.95}]}}", + "outputDataExample": "{\"renderedUI\": \"A page with charts and graphs showing performance trends.\"}" + } + ], + "description": "
  • Integrates with Google's PageSpeed Insights to monitor and analyze web page performance
  • - Tracks key web vitals and performance metrics such as SEO, Accessibility, Best Practices, and Performance scores
  • - Provides a grid view of all page speed monitors for a quick overview of website health
  • - Detailed reports show historical performance data, allowing users to track improvements or regressions over time
  • - This feature requires a Google PageSpeed API key to be configured in the settings
", + "simulationNodesAndEdges": { + "0622b212-3a4a-484d-9b7a-125864d785cd": { + "simStepIds": [ + "b0cc6e6a-961e-4ea9-aa8a-6bfa44d6ba12" + ] + }, + "6527fb47-157e-4bba-9aed-adde061c5689": { + "simStepIds": [ + "53968267-0c6e-42f4-88b3-cd90f44532ae" + ] + }, + "f89cbd87-c961-4f8b-92b6-714da57718d5": { + "simStepIds": [ + "01206383-2db8-4b18-84d0-be2a0289c196" + ] + }, + "ca6ee27b-1fbf-49de-978a-d6de259cc3d5": { + "simStepIds": [ + "5616ac6d-c281-4879-82f1-6590c721c8f7" + ] + }, + "9ef9e218-4742-487a-9a88-5c70e788ccdc": { + "simStepIds": [ + "0ec0eb06-05eb-4204-b077-9e47eb2cc316" + ] + }, + "b8e83b0a-69ff-4c34-a7fe-cb8c9ba85438": { + "simStepIds": [ + "f8df1cad-6051-466e-a13e-47d710ba1c14" + ] + }, + "0d3c8091-3d91-4e42-9b46-7e35ed726a7a": { + "simStepIds": [ + "0b380571-3896-4365-b5dc-187494f02e0d" + ] + }, + "27c7191b-80a2-475b-9371-e106108aff1c": { + "simStepIds": [ + "a629a102-0df1-4798-a7a2-45c8f6cdad29" + ] + }, + "7be432cd-2587-4eab-abdc-f7b9c1923597": { + "simStepIds": [ + "4ed43eaf-fa8f-4fce-af87-8a5f74dd5c08" + ] + } + }, + "isAIGenerated": true, + "keywords": "pagespeed, lighthouse, performance", + "generationPrompt": "Website Performance and Page Speed Monitoring", + "generationKeywords": "pagespeed, lighthouse, performance" + }, + "Scheduled Maintenance Windows": { + "name": "Scheduled Maintenance Windows", + "simSteps": [ + { + "simStepId": "2401ef7a-8d8e-4056-868a-547ab213daf1", + "diagramNodeId": "20335f29-a2ac-4e0c-aba6-28707748ecd4", + "simStepLabel": "Flow: Create Maintenance Window - User Interaction", + "simStepDescription": "A user navigates to the 'Create Maintenance Window' page. They fill out a form with details such as a name, start date/time, duration, recurrence, and select the monitors to which this window will apply.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx", + "startLine": "46", + "endLine": "131", + "relevantVariables": [ + "CreateMaintenance", + "form", + "setForm", + "handleFormChange", + "handleSubmit" + ] + }, + "inputDataExample": "{}", + "outputDataExample": "{\"name\":\"Server Patching Window\",\"startDate\":\"2023-10-27T00:00:00.000Z\",\"startTime\":\"2023-10-27T02:00:00.000Z\",\"duration\":\"60\",\"durationUnit\":\"minutes\",\"repeat\":\"none\",\"monitors\":[{\"_id\":\"652ff379963e792a5a3a0e4e\",\"name\":\"Main Production Server\"}]}" + }, + { + "simStepId": "f881435b-3bc2-453d-b6bf-56ad3a12f120", + "diagramNodeId": "d3bdc6ff-875e-472e-88c8-eb10b20c526e", + "simStepLabel": "Flow: Create Maintenance Window - Form Submission", + "simStepDescription": "Upon clicking the 'Create Maintenance' button, the form state is passed to the `handleSubmit` function which then calls the `handleSubmitForm` action from a custom hook.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx", + "startLine": "108", + "endLine": "109", + "relevantVariables": [ + "handleSubmit", + "handleSubmitForm" + ] + }, + "inputDataExample": "{\"name\":\"Server Patching Window\",\"startDate\":\"2023-10-27T00:00:00.000Z\",\"startTime\":\"2023-10-27T02:00:00.000Z\",\"duration\":\"60\",\"durationUnit\":\"minutes\",\"repeat\":\"none\",\"monitors\":[{\"_id\":\"652ff379963e792a5a3a0e4e\",\"name\":\"Main Production Server\"}]}", + "outputDataExample": "{\"name\":\"Server Patching Window\",\"startDate\":\"2023-10-27T00:00:00.000Z\",\"startTime\":\"2023-10-27T02:00:00.000Z\",\"duration\":\"60\",\"durationUnit\":\"minutes\",\"repeat\":\"none\",\"monitors\":[{\"_id\":\"652ff379963e792a5a3a0e4e\",\"name\":\"Main Production Server\"}]}" + }, + { + "simStepId": "5e0d7085-099b-478a-afaa-47ad4dd07043", + "diagramNodeId": "5bdfdfa4-5d93-49fa-b05c-905813e6cf9d", + "simStepLabel": "Flow: Create Maintenance Window - Prepare API Request", + "simStepDescription": "The `handleSubmitForm` hook processes the form data, calculates start and end timestamps, and constructs a request object. It then determines whether to call `createMaintenanceWindow` or `editMaintenanceWindow` on the network service.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx", + "startLine": "25", + "endLine": "58", + "relevantVariables": [ + "handleSubmitForm", + "networkService.createMaintenanceWindow", + "networkService.editMaintenanceWindow" + ] + }, + "inputDataExample": "{\"form\":{\"name\":\"Server Patching Window\",\"startDate\":\"2023-10-27T00:00:00.000Z\",\"startTime\":\"2023-10-27T02:00:00.000Z\",\"duration\":\"60\",\"durationUnit\":\"minutes\",\"repeat\":\"none\",\"monitors\":[{\"_id\":\"652ff379963e792a5a3a0e4e\"}]}}", + "outputDataExample": "{\"maintenanceWindow\":{\"name\":\"Server Patching Window\",\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\",\"repeat\":0,\"oneTime\":true,\"monitors\":[\"652ff379963e792a5a3a0e4e\"],\"expiry\":\"2023-10-27T03:00:00.000Z\"}}" + }, + { + "simStepId": "0660b48b-139f-4ecf-b7d9-f12f544e0ab5", + "diagramNodeId": "fd5e4447-123b-4b2d-8bce-1d1ae7cb00af", + "simStepLabel": "Flow: Create Maintenance Window - API Call", + "simStepDescription": "The `NetworkService` sends the prepared maintenance window data as a POST request to the `/api/v1/maintenance-window` endpoint.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "762", + "endLine": "766", + "relevantVariables": [ + "createMaintenanceWindow", + "this.axiosInstance.post" + ] + }, + "inputDataExample": "{\"maintenanceWindow\":{\"name\":\"Server Patching Window\",\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\",\"repeat\":0,\"oneTime\":true,\"monitors\":[\"652ff379963e792a5a3a0e4e\"],\"expiry\":\"2023-10-27T03:00:00.000Z\"}}", + "outputDataExample": "{\"maintenanceWindow\":{\"name\":\"Server Patching Window\",\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\",\"repeat\":0,\"oneTime\":true,\"monitors\":[\"652ff379963e792a5a3a0e4e\"],\"expiry\":\"2023-10-27T03:00:00.000Z\"}}" + }, + { + "simStepId": "3e93ef65-8e2c-405b-8dc1-56d8e93937b3", + "diagramNodeId": "a391e8ea-9dbe-4fe4-90df-bf475d8b7ef8", + "simStepLabel": "Flow: Create Maintenance Window - API Routing", + "simStepDescription": "The backend Express server receives the request. The routing configuration maps the `/api/v1/maintenance-window` path to the `maintenanceWindowRoutes`, which in turn directs the POST request to the `createMaintenanceWindows` method in the controller.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/config/routes.js", + "startLine": "43", + "endLine": "43", + "relevantVariables": [ + "app", + "verifyJWT", + "maintenanceWindowRoutes" + ] + }, + "inputDataExample": "{\"method\":\"POST\",\"url\":\"/api/v1/maintenance-window\",\"body\":{\"name\":\"Server Patching Window\",\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\",\"repeat\":0,\"oneTime\":true,\"monitors\":[\"652ff379963e792a5a3a0e4e\"]}}", + "outputDataExample": "{\"controllerMethod\":\"createMaintenanceWindows\",\"params\":{}}" + }, + { + "simStepId": "e4cc5ce8-6da9-4281-9204-29c061ab13ce", + "diagramNodeId": "e2588541-3ffc-47c6-8f8a-41d30ea7fc72", + "simStepLabel": "Flow: Create Maintenance Window - Controller Invocation", + "simStepDescription": "The request and response objects are passed from the router to the `createMaintenanceWindows` controller method.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/maintenanceWindowRoute.js", + "startLine": "10", + "endLine": "10", + "relevantVariables": [ + "this.router.post", + "this.mwController.createMaintenanceWindows" + ] + }, + "inputDataExample": "{\"req\": {\"body\": {\"name\":\"Server Patching Window\", \"monitors\":[\"652ff379963e792a5a3a0e4e\"]}, \"user\": {\"teamId\":\"652ff379963e792a5a3a0e4d\"}}}", + "outputDataExample": "{\"req\": {\"body\": {\"name\":\"Server Patching Window\", \"monitors\":[\"652ff379963e792a5a3a0e4e\"]}, \"user\": {\"teamId\":\"652ff379963e792a5a3a0e4d\"}}}" + }, + { + "simStepId": "95fc1919-339d-4659-98cb-3dc50f662075", + "diagramNodeId": "0fae6e75-38ea-444d-a1bf-0f820ba443b9", + "simStepLabel": "Flow: Create Maintenance Window - Controller Logic", + "simStepDescription": "The `maintenanceWindowController` validates the request body using Joi, ensures a `teamId` exists on the user object, and then calls the `maintenanceWindowService` to handle the business logic of creating the window.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/maintenanceWindowController.js", + "startLine": "27", + "endLine": "41", + "relevantVariables": [ + "createMaintenanceWindows", + "createMaintenanceWindowBodyValidation", + "this.maintenanceWindowService.createMaintenanceWindow" + ] + }, + "inputDataExample": "{\"req\": {\"body\": {\"name\":\"Server Patching Window\", \"monitors\":[\"652ff379963e792a5a3a0e4e\"]}, \"user\": {\"teamId\":\"652ff379963e792a5a3a0e4d\"}}}", + "outputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"body\":{\"name\":\"Server Patching Window\",\"monitors\":[\"652ff379963e792a5a3a0e4e\"]}}" + }, + { + "simStepId": "c3d1a95c-e93d-499a-8830-a6d870902414", + "diagramNodeId": "e1d59cf6-0d96-4e0a-ad17-2a7984a9157f", + "simStepLabel": "Flow: Create Maintenance Window - Service Layer Call", + "simStepDescription": "The controller passes the teamId and request body to the `createMaintenanceWindow` method in the `maintenanceWindowService`.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/maintenanceWindowController.js", + "startLine": "35", + "endLine": "35", + "relevantVariables": [ + "this.maintenanceWindowService.createMaintenanceWindow" + ] + }, + "inputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"body\":{\"name\":\"Server Patching Window\",\"monitors\":[\"652ff379963e792a5a3a0e4e\"]}}", + "outputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"body\":{\"name\":\"Server Patching Window\",\"monitors\":[\"652ff379963e792a5a3a0e4e\"]}}" + }, + { + "simStepId": "6e2cf739-1514-4c37-9874-10b68a47010e", + "diagramNodeId": "560d268b-ef0f-4cd2-bcfe-9dc0a0e2ce6f", + "simStepLabel": "Flow: Create Maintenance Window - Business Logic Execution", + "simStepDescription": "The `maintenanceWindowService` verifies that all monitors specified in the request belong to the user's team. It then iterates through each monitor ID and prepares a separate database transaction to create a maintenance window record for each one.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/maintenanceWindowService.js", + "startLine": "18", + "endLine": "39", + "relevantVariables": [ + "createMaintenanceWindow", + "this.db.monitorModule.getMonitorsByIds", + "this.db.maintenanceWindowModule.createMaintenanceWindow", + "Promise.all" + ] + }, + "inputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"body\":{\"name\":\"Server Patching Window\",\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\",\"monitors\":[\"652ff379963e792a5a3a0e4e\"]}}", + "outputDataExample": "[{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"name\":\"Server Patching Window\",\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\"}]" + }, + { + "simStepId": "80aed28e-68b6-47bf-8bc9-ededb4824066", + "diagramNodeId": "12d0ab8a-392f-48fa-9efa-06ffe1f3a252", + "simStepLabel": "Flow: Create Maintenance Window - Database Module Call", + "simStepDescription": "For each monitor, the service layer calls the `createMaintenanceWindow` method in the `maintenanceWindowModule`, passing the structured data for the new record.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/maintenanceWindowService.js", + "startLine": "28", + "endLine": "37", + "relevantVariables": [ + "dbTransactions", + "this.db.maintenanceWindowModule.createMaintenanceWindow" + ] + }, + "inputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"name\":\"Server Patching Window\",\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\",\"oneTime\":true}", + "outputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"name\":\"Server Patching Window\",\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\",\"oneTime\":true}" + }, + { + "simStepId": "a3c47fd4-c701-46c2-9aff-94a667b3e368", + "diagramNodeId": "3890b98d-a133-4279-a961-5d2facc6a17a", + "simStepLabel": "Flow: Create Maintenance Window - Database Record Creation", + "simStepDescription": "The `maintenanceWindowModule` instantiates a new `MaintenanceWindow` Mongoose model. If the window is a one-time event, it sets an `expiry` field to the end time for automatic cleanup by MongoDB's TTL index. Finally, it saves the new document to the database.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/maintenanceWindowModule.js", + "startLine": "7", + "endLine": "22", + "relevantVariables": [ + "createMaintenanceWindow", + "this.MaintenanceWindow", + "maintenanceWindow.save" + ] + }, + "inputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"name\":\"Server Patching Window\",\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\",\"oneTime\":true}", + "outputDataExample": "{\"_id\":\"653ba9a6331a31004887396a\",\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"teamId\":\"652ff379963e792a5a3a0e4d\",\"name\":\"Server Patching Window\",\"active\":true,\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\",\"expiry\":\"2023-10-27T03:00:00.000Z\"}" + }, + { + "simStepId": "69fdc2a9-837f-466c-9562-a12319019580", + "diagramNodeId": "fcf4eef4-394c-4872-9124-f7a0b32cdc3b", + "simStepLabel": "Flow: Create Maintenance Window - API Response", + "simStepDescription": "After the database operations are complete, a success response is sent back through the layers to the frontend.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/maintenanceWindowController.js", + "startLine": "37", + "endLine": "39", + "relevantVariables": [ + "res.success", + "this.stringService.maintenanceWindowCreate" + ] + }, + "inputDataExample": "{\"msg\":\"Maintenance Window created successfully.\"}", + "outputDataExample": "{\"msg\":\"Maintenance Window created successfully.\"}" + }, + { + "simStepId": "ebe8fd52-f2a1-4a98-a71b-b04d6f4cdadc", + "diagramNodeId": "a5a84b08-8c70-4023-8d08-b74e23aab4d7", + "simStepLabel": "Flow: Create Maintenance Window - UI Update", + "simStepDescription": "The frontend receives the success response. A success toast message is displayed, and the user is navigated back to the main maintenance windows table.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx", + "startLine": "111", + "endLine": "119", + "relevantVariables": [ + "request", + "createToast", + "navigate" + ] + }, + "inputDataExample": "{\"data\":{\"msg\":\"Maintenance Window created successfully.\"}}", + "outputDataExample": "{}" + }, + { + "simStepId": "82b5ea14-bb00-4511-aba4-76196029fd81", + "diagramNodeId": "d145bb4c-e1e0-473a-b6bc-decc932b738b", + "simStepLabel": "Flow: View Maintenance Windows - Page Load", + "simStepDescription": "User navigates to the '/maintenance' page. The `Maintenance` component mounts and triggers a `useEffect` hook to fetch the list of maintenance windows.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Maintenance/index.jsx", + "startLine": "25", + "endLine": "47", + "relevantVariables": [ + "useEffect", + "networkService.getMaintenanceWindowsByTeamId" + ] + }, + "inputDataExample": "{}", + "outputDataExample": "{\"page\":0,\"rowsPerPage\":5}" + }, + { + "simStepId": "7e20cd6c-d24e-4540-86d6-d6a9fa087314", + "diagramNodeId": "7f921125-500f-4902-abc3-936ecac033d8", + "simStepLabel": "Flow: View Maintenance Windows - API Call", + "simStepDescription": "The `networkService` makes a GET request to the backend API to fetch the maintenance windows for the user's team, including pagination parameters.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "824", + "endLine": "824", + "relevantVariables": [ + "getMaintenanceWindowsByTeamId", + "this.axiosInstance.get" + ] + }, + "inputDataExample": "{\"page\":0,\"rowsPerPage\":5}", + "outputDataExample": "{\"page\":0,\"rowsPerPage\":5}" + }, + { + "simStepId": "1251da83-5949-4e24-a1d2-fc74ff785aa9", + "diagramNodeId": "c35a065c-2808-402c-877e-9055d956ce0d", + "simStepLabel": "Flow: View Maintenance Windows - Controller and Service", + "simStepDescription": "The backend routes the request to `maintenanceWindowController.getMaintenanceWindowsByTeamId`. This controller calls the corresponding service, which in turn calls the database module to query for the data.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/maintenanceWindowController.js", + "startLine": "75", + "endLine": "75", + "relevantVariables": [ + "this.maintenanceWindowService.getMaintenanceWindowsByTeamId" + ] + }, + "inputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"query\":{\"page\":\"0\",\"rowsPerPage\":\"5\"}}", + "outputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"query\":{\"page\":\"0\",\"rowsPerPage\":\"5\"}}" + }, + { + "simStepId": "feb8bd4f-339f-4132-a709-89c63c4efa9c", + "diagramNodeId": "c3127c74-43f1-42d7-8da6-b0a494c3658f", + "simStepLabel": "Flow: View Maintenance Windows - Database Query", + "simStepDescription": "The service layer invokes the database module to fetch the maintenance windows from the database.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/maintenanceWindowService.js", + "startLine": "48", + "endLine": "48", + "relevantVariables": [ + "this.db.maintenanceWindowModule.getMaintenanceWindowsByTeamId" + ] + }, + "inputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"query\":{\"page\":\"0\",\"rowsPerPage\":\"5\"}}", + "outputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"query\":{\"page\":\"0\",\"rowsPerPage\":\"5\"}}" + }, + { + "simStepId": "9bfbae5a-f487-40ef-86f5-04a1cc3c7662", + "diagramNodeId": "27d9160a-4f8d-4dff-a09b-1678bc28cf2c", + "simStepLabel": "Flow: View Maintenance Windows - Data Retrieval", + "simStepDescription": "The `maintenanceWindowModule` constructs a MongoDB query to find all maintenance windows for the given `teamId`, applying pagination (skip, limit) and sorting. It executes the query and returns the results along with the total count.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/maintenanceWindowModule.js", + "startLine": "37", + "endLine": "66", + "relevantVariables": [ + "getMaintenanceWindowsByTeamId", + "this.MaintenanceWindow.countDocuments", + "this.MaintenanceWindow.find" + ] + }, + "inputDataExample": "{\"teamId\":\"652ff379963e792a5a3a0e4d\",\"query\":{\"page\":\"0\",\"rowsPerPage\":\"5\"}}", + "outputDataExample": "{\"maintenanceWindows\":[{\"_id\":\"653ba9a6331a31004887396a\",\"name\":\"Server Patching Window\"}],\"maintenanceWindowCount\":1}" + }, + { + "simStepId": "eda86dca-dea6-4671-a079-7d212f4f18c4", + "diagramNodeId": "6c9979cd-1cc1-4227-b535-2ec3a10ca27e", + "simStepLabel": "Flow: View Maintenance Windows - Data Return to Frontend", + "simStepDescription": "The fetched data is sent back in the API response to the client.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/maintenanceWindowController.js", + "startLine": "77", + "endLine": "80", + "relevantVariables": [ + "res.success" + ] + }, + "inputDataExample": "{\"data\":{\"maintenanceWindows\":[{\"_id\":\"653ba9a6331a31004887396a\",\"name\":\"Server Patching Window\"}],\"maintenanceWindowCount\":1}}", + "outputDataExample": "{\"data\":{\"maintenanceWindows\":[{\"_id\":\"653ba9a6331a31004887396a\",\"name\":\"Server Patching Window\"}],\"maintenanceWindowCount\":1}}" + }, + { + "simStepId": "acf97639-e291-4ca6-81a6-d0487397f42a", + "diagramNodeId": "970e2002-7c0e-40ef-80d8-e5e6d0a60735", + "simStepLabel": "Flow: View Maintenance Windows - UI Rendering", + "simStepDescription": "The client receives the data and updates its state. The `MaintenanceTable` component then renders the list of maintenance windows in a table, displaying their names, schedules, and providing actions like edit or delete.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx", + "startLine": "195", + "endLine": "201", + "relevantVariables": [ + "Table", + "headers", + "maintenanceWindows" + ] + }, + "inputDataExample": "{\"maintenanceWindows\":[{\"_id\":\"653ba9a6331a31004887396a\",\"name\":\"Server Patching Window\"}],\"maintenanceWindowCount\":1}", + "outputDataExample": "{}" + }, + { + "simStepId": "a47affaf-b952-46d6-9709-6120a77690e1", + "diagramNodeId": "23630a1f-7452-4373-a864-7ec7821137f2", + "simStepLabel": "Flow: Monitor Suppression - Job Execution Start", + "simStepDescription": "The `SuperSimpleQueue` processing system picks up a job to check a specific monitor. The job handler is retrieved from `SuperSimpleQueueHelper`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "startLine": "30", + "endLine": "45", + "relevantVariables": [ + "getMonitorJob", + "this.isInMaintenanceWindow" + ] + }, + "inputDataExample": "{\"monitor\":{\"_id\":\"652ff379963e792a5a3a0e4e\",\"teamId\":\"652ff379963e792a5a3a0e4d\",\"name\":\"Main Production Server\"}}", + "outputDataExample": "{\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"teamId\":\"652ff379963e792a5a3a0e4d\"}" + }, + { + "simStepId": "87583096-f1ec-4160-ae52-5238f32ed0ab", + "diagramNodeId": "c99291bb-7316-440e-9a7f-323ee39e26b8", + "simStepLabel": "Flow: Monitor Suppression - Maintenance Check Call", + "simStepDescription": "Before performing the monitor check, the `getMonitorJob` function calls `isInMaintenanceWindow`, passing the monitor's ID and team ID to see if it should be suppressed.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "startLine": "36", + "endLine": "36", + "relevantVariables": [ + "this.isInMaintenanceWindow" + ] + }, + "inputDataExample": "{\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"teamId\":\"652ff379963e792a5a3a0e4d\"}", + "outputDataExample": "{\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"teamId\":\"652ff379963e792a5a3a0e4d\"}" + }, + { + "simStepId": "2127e749-80b4-499a-ae14-8dc2dcc9db9b", + "diagramNodeId": "fc78b5b6-7fad-4f8f-a5a3-c7c38931daba", + "simStepLabel": "Flow: Monitor Suppression - Fetching Maintenance Windows", + "simStepDescription": "The `isInMaintenanceWindow` function calls the database module to fetch all maintenance windows associated with the given monitor ID and team ID.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "startLine": "80", + "endLine": "83", + "relevantVariables": [ + "this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId" + ] + }, + "inputDataExample": "{\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"teamId\":\"652ff379963e792a5a3a0e4d\"}", + "outputDataExample": "{\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"teamId\":\"652ff379963e792a5a3a0e4d\"}" + }, + { + "simStepId": "e6542e14-556d-482a-8c35-f7b03997be49", + "diagramNodeId": "8a13e585-2310-420b-9e92-211b6d65f201", + "simStepLabel": "Flow: Monitor Suppression - Database Result Return", + "simStepDescription": "The database module returns a list of maintenance window documents that match the monitor ID.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/maintenanceWindowModule.js", + "startLine": "69", + "endLine": "76", + "relevantVariables": [ + "getMaintenanceWindowsByMonitorId", + "this.MaintenanceWindow.find" + ] + }, + "inputDataExample": "[{\"_id\":\"653ba9a6331a31004887396a\",\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"active\":true,\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\"}]", + "outputDataExample": "[{\"_id\":\"653ba9a6331a31004887396a\",\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"active\":true,\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\"}]" + }, + { + "simStepId": "6ba6b9d1-5fc3-454e-8744-2022b5192ed9", + "diagramNodeId": "0fe32416-db43-49b9-9438-3796ba1a0add", + "simStepLabel": "Flow: Monitor Suppression - Time Evaluation", + "simStepDescription": "The `isInMaintenanceWindow` function iterates through the returned windows. For each active window, it checks if the current time (`new Date()`) is between the `start` and `end` times.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "startLine": "85", + "endLine": "113", + "relevantVariables": [ + "maintenanceWindows.reduce", + "start", + "end", + "now" + ] + }, + "inputDataExample": "[{\"_id\":\"653ba9a6331a31004887396a\",\"monitorId\":\"652ff379963e792a5a3a0e4e\",\"active\":true,\"start\":\"2023-10-27T02:00:00.000Z\",\"end\":\"2023-10-27T03:00:00.000Z\"}]", + "outputDataExample": "true" + }, + { + "simStepId": "a715d98f-60bf-4f32-aae6-21ee4e09f4da", + "diagramNodeId": "40f4d9df-4c0f-4384-901c-8d7117e3b538", + "simStepLabel": "Flow: Monitor Suppression - Return Suppression Flag", + "simStepDescription": "The boolean result (true if in maintenance, false otherwise) is returned to the `getMonitorJob` function.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "startLine": "114", + "endLine": "114", + "relevantVariables": [ + "maintenanceWindowIsActive" + ] + }, + "inputDataExample": "true", + "outputDataExample": "true" + }, + { + "simStepId": "e59fe64e-f06b-4855-b631-923bb5a68540", + "diagramNodeId": "01e042c3-40b4-416a-8d9e-6d6fd323d60b", + "simStepLabel": "Flow: Monitor Suppression - Conditional Job Termination", + "simStepDescription": "If the `isInMaintenanceWindow` function returns true, the `getMonitorJob` logs that the monitor is in a maintenance window and terminates its execution for this cycle, effectively suppressing the check.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "startLine": "37", + "endLine": "44", + "relevantVariables": [ + "maintenanceWindowActive", + "this.logger.info", + "return" + ] + }, + "inputDataExample": "{\"maintenanceWindowActive\": true}", + "outputDataExample": "{\"message\":\"Monitor 652ff379963e792a5a3a0e4e is in maintenance window\"}" + } + ], + "description": "
  • Allows users to schedule maintenance periods to prevent false alarms and avoid skewing uptime statistics during planned downtime
  • - Users can define a start and end time for a maintenance window
  • - Specific monitors or all monitors can be associated with a maintenance window
  • - During a scheduled maintenance, notifications for associated monitors are suppressed
  • - Provides a central table to view all active, upcoming, and past maintenance windows
", + "simulationNodesAndEdges": { + "20335f29-a2ac-4e0c-aba6-28707748ecd4": { + "simStepIds": [ + "2401ef7a-8d8e-4056-868a-547ab213daf1" + ] + }, + "5bdfdfa4-5d93-49fa-b05c-905813e6cf9d": { + "simStepIds": [ + "5e0d7085-099b-478a-afaa-47ad4dd07043" + ] + }, + "a391e8ea-9dbe-4fe4-90df-bf475d8b7ef8": { + "simStepIds": [ + "3e93ef65-8e2c-405b-8dc1-56d8e93937b3" + ] + }, + "0fae6e75-38ea-444d-a1bf-0f820ba443b9": { + "simStepIds": [ + "95fc1919-339d-4659-98cb-3dc50f662075" + ] + }, + "560d268b-ef0f-4cd2-bcfe-9dc0a0e2ce6f": { + "simStepIds": [ + "6e2cf739-1514-4c37-9874-10b68a47010e" + ] + }, + "3890b98d-a133-4279-a961-5d2facc6a17a": { + "simStepIds": [ + "a3c47fd4-c701-46c2-9aff-94a667b3e368" + ] + }, + "a5a84b08-8c70-4023-8d08-b74e23aab4d7": { + "simStepIds": [ + "ebe8fd52-f2a1-4a98-a71b-b04d6f4cdadc" + ] + }, + "d145bb4c-e1e0-473a-b6bc-decc932b738b": { + "simStepIds": [ + "82b5ea14-bb00-4511-aba4-76196029fd81" + ] + }, + "c35a065c-2808-402c-877e-9055d956ce0d": { + "simStepIds": [ + "1251da83-5949-4e24-a1d2-fc74ff785aa9" + ] + }, + "27d9160a-4f8d-4dff-a09b-1678bc28cf2c": { + "simStepIds": [ + "9bfbae5a-f487-40ef-86f5-04a1cc3c7662" + ] + }, + "970e2002-7c0e-40ef-80d8-e5e6d0a60735": { + "simStepIds": [ + "acf97639-e291-4ca6-81a6-d0487397f42a" + ] + }, + "23630a1f-7452-4373-a864-7ec7821137f2": { + "simStepIds": [ + "a47affaf-b952-46d6-9709-6120a77690e1" + ] + }, + "fc78b5b6-7fad-4f8f-a5a3-c7c38931daba": { + "simStepIds": [ + "2127e749-80b4-499a-ae14-8dc2dcc9db9b" + ] + }, + "0fe32416-db43-49b9-9438-3796ba1a0add": { + "simStepIds": [ + "6ba6b9d1-5fc3-454e-8744-2022b5192ed9" + ] + }, + "01e042c3-40b4-416a-8d9e-6d6fd323d60b": { + "simStepIds": [ + "e59fe64e-f06b-4855-b631-923bb5a68540" + ] + }, + "d3bdc6ff-875e-472e-88c8-eb10b20c526e": { + "simStepIds": [ + "f881435b-3bc2-453d-b6bf-56ad3a12f120" + ] + }, + "fd5e4447-123b-4b2d-8bce-1d1ae7cb00af": { + "simStepIds": [ + "0660b48b-139f-4ecf-b7d9-f12f544e0ab5" + ] + }, + "e2588541-3ffc-47c6-8f8a-41d30ea7fc72": { + "simStepIds": [ + "e4cc5ce8-6da9-4281-9204-29c061ab13ce" + ] + }, + "e1d59cf6-0d96-4e0a-ad17-2a7984a9157f": { + "simStepIds": [ + "c3d1a95c-e93d-499a-8830-a6d870902414" + ] + }, + "12d0ab8a-392f-48fa-9efa-06ffe1f3a252": { + "simStepIds": [ + "80aed28e-68b6-47bf-8bc9-ededb4824066" + ] + }, + "fcf4eef4-394c-4872-9124-f7a0b32cdc3b": { + "simStepIds": [ + "69fdc2a9-837f-466c-9562-a12319019580" + ] + }, + "7f921125-500f-4902-abc3-936ecac033d8": { + "simStepIds": [ + "7e20cd6c-d24e-4540-86d6-d6a9fa087314" + ] + }, + "c3127c74-43f1-42d7-8da6-b0a494c3658f": { + "simStepIds": [ + "feb8bd4f-339f-4132-a709-89c63c4efa9c" + ] + }, + "6c9979cd-1cc1-4227-b535-2ec3a10ca27e": { + "simStepIds": [ + "eda86dca-dea6-4671-a079-7d212f4f18c4" + ] + }, + "c99291bb-7316-440e-9a7f-323ee39e26b8": { + "simStepIds": [ + "87583096-f1ec-4160-ae52-5238f32ed0ab" + ] + }, + "8a13e585-2310-420b-9e92-211b6d65f201": { + "simStepIds": [ + "e6542e14-556d-482a-8c35-f7b03997be49" + ] + }, + "40f4d9df-4c0f-4384-901c-8d7117e3b538": { + "simStepIds": [ + "a715d98f-60bf-4f32-aae6-21ee4e09f4da" + ] + } + }, + "isAIGenerated": true, + "keywords": "MaintenanceWindow, createMaintenanceWindow, maintenanceWindowController", + "generationPrompt": "Scheduled Maintenance Windows", + "generationKeywords": "MaintenanceWindow, createMaintenanceWindow, maintenanceWindowController" + }, + "User and Team Management with Role-Based Access": { + "name": "User and Team Management with Role-Based Access", + "simSteps": [ + { + "simStepId": "2d5a952a-ce62-4948-9309-96b28ad74e59", + "diagramNodeId": "3de2d413-5e9b-49f4-9409-74be32c52e0f", + "simStepLabel": "Client: Conditional UI Rendering Based on Role", + "simStepDescription": "A user with 'superadmin' or 'admin' role navigates to the Account page. The application checks the user's role stored in the Redux state to conditionally render the 'Team' management tab. Non-admin users will not see this option.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Account/index.jsx", + "startLine": "37", + "endLine": "41", + "relevantVariables": [ + "user.role", + "requiredRoles", + "hideTeams" + ] + }, + "inputDataExample": "{\n \"user\": {\n \"_id\": \"60f8f7b3a7b3c2a3e4f5a6b7\",\n \"firstName\": \"Jane\",\n \"lastName\": \"Doe\",\n \"email\": \"jane.doe@example.com\",\n \"role\": [\"superadmin\"],\n \"teamId\": \"60f8f7b3a7b3c2a3e4f5a6b8\"\n }\n}", + "outputDataExample": "{\n \"hideTeams\": false,\n \"tabList\": [\n {\n \"name\": \"Profile\",\n \"value\": \"profile\"\n },\n {\n \"name\": \"Password\",\n \"value\": \"password\"\n },\n {\n \"name\": \"Team\",\n \"value\": \"team\"\n }\n ]\n}" + }, + { + "simStepId": "10f80ada-2d0a-4e98-bd5d-057e7d4fe6de", + "diagramNodeId": "c441312f-f3c6-48c6-b1b9-7c12749d91bb", + "simStepLabel": "Client: User Navigates to Team Panel", + "simStepDescription": "The user clicks on the 'Team' tab, which navigates them to the team management view. The user's role data is implicitly passed through the application state to render the appropriate components.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Account/index.jsx", + "startLine": "98", + "endLine": "98", + "relevantVariables": [ + "TeamPanel" + ] + }, + "inputDataExample": "{\n \"navigationPath\": \"/account/team\",\n \"userState\": {\n \"role\": [\"superadmin\"]\n }\n}", + "outputDataExample": "{\n \"navigationPath\": \"/account/team\",\n \"userState\": {\n \"role\": [\"superadmin\"]\n }\n}" + }, + { + "simStepId": "eebaca25-d588-49c4-a35c-9abc82fde580", + "diagramNodeId": "b27f1bba-c655-40c6-a1dc-71e78c590c92", + "simStepLabel": "Client: Initiate User Invitation", + "simStepDescription": "In the `TeamPanel`, the admin clicks the 'Add Team Member' button, then selects 'Invite a team member'. A dialog opens, prompting for the new user's email and role. After filling the form and clicking 'E-mail token', the `handleInviteMember` function is triggered to start the invitation process.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Account/components/TeamPanel.jsx", + "startLine": "348", + "endLine": "354", + "relevantVariables": [ + "handleInviteMember", + "Button" + ] + }, + "inputDataExample": "{\n \"email\": \"new.developer@example.com\",\n \"role\": [\"user\"]\n}", + "outputDataExample": "{\n \"apiCall\": \"networkService.sendInviteEmail\",\n \"payload\": {\n \"email\": \"new.developer@example.com\",\n \"role\": [\"user\"]\n }\n}" + }, + { + "simStepId": "16d87608-4458-430c-b459-cd9b1d413fa6", + "diagramNodeId": "3530244f-2689-444e-bcbf-611db2e3c000", + "simStepLabel": "API Call: Send Invitation Request", + "simStepDescription": "The client sends a POST request to the `/api/v1/invite/send` endpoint. The request includes the new user's email and selected role in the body, and the admin's authentication token in the headers.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/inviteRoute.js", + "startLine": "14", + "endLine": "14", + "relevantVariables": [ + "this.router.post" + ] + }, + "inputDataExample": "{\n \"endpoint\": \"/api/v1/invite/send\",\n \"method\": \"POST\",\n \"headers\": {\n \"Authorization\": \"Bearer \"\n },\n \"body\": {\n \"email\": \"new.developer@example.com\",\n \"role\": [\"user\"]\n }\n}", + "outputDataExample": "{\n \"endpoint\": \"/api/v1/invite/send\",\n \"method\": \"POST\",\n \"headers\": {\n \"Authorization\": \"Bearer \"\n },\n \"body\": {\n \"email\": \"new.developer@example.com\",\n \"role\": [\"user\"]\n }\n}" + }, + { + "simStepId": "50dd7316-7d01-43f7-9036-d97d51a9c178", + "diagramNodeId": "8b3cb6c2-ed57-4510-be15-decb8e3c89e1", + "simStepLabel": "Server: Role-Based Access Control Middleware", + "simStepDescription": "The server receives the request. Before reaching the main controller, the `isAllowed` middleware intercepts the request. It verifies the JWT from the request header and checks if the user's role (e.g., 'superadmin') is present in the list of allowed roles for this route (`['admin', 'superadmin']`).", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/middleware/v1/isAllowed.js", + "startLine": "39", + "endLine": "44", + "relevantVariables": [ + "decoded.role", + "allowedRoles", + "hasRole" + ] + }, + "inputDataExample": "{\n \"req\": {\n \"user\": {\n \"_id\": \"60f8f7b3a7b3c2a3e4f5a6b7\",\n \"role\": [\"superadmin\"],\n \"teamId\": \"60f8f7b3a7b3c2a3e4f5a6b8\"\n },\n \"headers\": {\n \"authorization\": \"Bearer \"\n }\n },\n \"allowedRoles\": [\"admin\", \"superadmin\"]\n}", + "outputDataExample": "{\n \"result\": \"next() called\",\n \"req\": {\n \"user\": {\n \"_id\": \"60f8f7b3a7b3c2a3e4f5a6b7\",\n \"role\": [\"superadmin\"],\n \"teamId\": \"60f8f7b3a7b3c2a3e4f5a6b8\"\n },\n \"headers\": {\n \"authorization\": \"Bearer \"\n }\n }\n}" + }, + { + "simStepId": "6c024d4c-178b-45fc-8bca-4fc6c00549a4", + "diagramNodeId": "2ee1c4e8-90fb-4470-ad9f-6ffed5420853", + "simStepLabel": "Server: Forward Request to Controller", + "simStepDescription": "Since the user's role is authorized, the middleware calls `next()` to pass the request object, now populated with user data, to the next handler in the chain: `inviteController.sendInviteEmail`.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/inviteController.js", + "startLine": "61", + "endLine": "68", + "relevantVariables": [ + "sendInviteEmail", + "this.inviteService.sendInviteEmail" + ] + }, + "inputDataExample": "{\n \"req\": {\n \"user\": {\n \"_id\": \"60f8f7b3a7b3c2a3e4f5a6b7\",\n \"firstName\": \"Jane\",\n \"role\": [\"superadmin\"],\n \"teamId\": \"60f8f7b3a7b3c2a3e4f5a6b8\"\n },\n \"body\": {\n \"email\": \"new.developer@example.com\",\n \"role\": [\"user\"]\n }\n }\n}", + "outputDataExample": "{\n \"req\": {\n \"user\": {\n \"_id\": \"60f8f7b3a7b3c2a3e4f5a6b7\",\n \"firstName\": \"Jane\",\n \"role\": [\"superadmin\"],\n \"teamId\": \"60f8f7b3a7b3c2a3e4f5a6b8\"\n },\n \"body\": {\n \"email\": \"new.developer@example.com\",\n \"role\": [\"user\"]\n }\n }\n}" + }, + { + "simStepId": "55fe9963-30d2-47f8-859f-b485a77064c4", + "diagramNodeId": "ae858158-8db6-4917-8fda-0966a953f6ed", + "simStepLabel": "Server: Process Invitation in Service Layer", + "simStepDescription": "The `inviteController` calls the `inviteService`. The service layer handles the business logic: it first requests an invitation token from the database module and then instructs the email service to send the invitation.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/inviteService.js", + "startLine": "24", + "endLine": "28", + "relevantVariables": [ + "sendInviteEmail", + "this.db.inviteModule.requestInviteToken", + "this.emailService.sendInviteEmail" + ] + }, + "inputDataExample": "{\n \"inviteRequest\": {\n \"email\": \"new.developer@example.com\",\n \"role\": [\"user\"],\n \"teamId\": \"60f8f7b3a7b3c2a3e4f5a6b8\"\n },\n \"firstName\": \"Jane\"\n}", + "outputDataExample": "{\n \"inviteToken\": {\n \"token\": \"a1b2c3d4e5f6...\",\n \"email\": \"new.developer@example.com\"\n }\n}" + }, + { + "simStepId": "c796f8ad-d548-45f3-8d62-67f59c6c1aa4", + "diagramNodeId": "bdd52e6f-ddf7-4966-b3af-957f9553082f", + "simStepLabel": "Database Query: Create and Store Invite Token", + "simStepDescription": "The `inviteService` calls the `inviteModule` to generate and persist a unique invitation token. This involves creating a new document in the `InviteToken` collection with the invitee's details and a securely generated random token.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/inviteModule.js", + "startLine": "23", + "endLine": "26", + "relevantVariables": [ + "requestInviteToken", + "InviteToken", + "invite.save" + ] + }, + "inputDataExample": "{\n \"email\": \"new.developer@example.com\",\n \"role\": [\"user\"],\n \"teamId\": \"60f8f7b3a7b3c2a3e4f5a6b8\"\n}", + "outputDataExample": "{\n \"_id\": \"61f8f7b3a7b3c2a3e4f5a6b9\",\n \"email\": \"new.developer@example.com\",\n \"role\": [\"user\"],\n \"teamId\": \"60f8f7b3a7b3c2a3e4f5a6b8\",\n \"token\": \"a1b2c3d4e5f6...\",\n \"createdAt\": \"2023-10-27T10:00:00.000Z\"\n}" + }, + { + "simStepId": "7d1f86f2-c8d5-48fb-a8c1-b337dc56271f", + "diagramNodeId": "a7be2d0b-0227-4228-a0a6-074f7324fd89", + "simStepLabel": "Server: Dispatch Invitation Email", + "simStepDescription": "After successfully storing the token, the `inviteService` uses the `emailService` to compose and send an email to the new user. The email contains a unique registration link including the generated token.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/inviteService.js", + "startLine": "26", + "endLine": "28", + "relevantVariables": [ + "this.emailService.sendInviteEmail", + "inviteToken" + ] + }, + "inputDataExample": "{\n \"to\": \"new.developer@example.com\",\n \"token\": \"a1b2c3d4e5f6...\",\n \"inviterName\": \"Jane\"\n}", + "outputDataExample": "{\n \"status\": \"success\",\n \"messageId\": \"\"\n}" + }, + { + "simStepId": "6ba5094d-d1e6-4ec3-8917-28cdb30f2ef9", + "diagramNodeId": "52732a37-82b8-40a7-b12d-dc1b82380b2d", + "simStepLabel": "API Response: Send Confirmation to Client", + "simStepDescription": "The server's controller, having successfully processed the invitation, sends a JSON response back to the client indicating that the invite was sent successfully.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/inviteController.js", + "startLine": "69", + "endLine": "72", + "relevantVariables": [ + "res.success" + ] + }, + "inputDataExample": "{\n \"success\": true,\n \"msg\": \"Invite sent successfully\",\n \"data\": {\n \"token\": \"a1b2c3d4e5f6...\",\n \"email\": \"new.developer@example.com\"\n }\n}", + "outputDataExample": "{\n \"success\": true,\n \"msg\": \"Invite sent successfully\",\n \"data\": {\n \"token\": \"a1b2c3d4e5f6...\",\n \"email\": \"new.developer@example.com\"\n }\n}" + }, + { + "simStepId": "700f35e0-b39d-417c-b0a8-0612feade977", + "diagramNodeId": "02d87257-883c-4822-a421-823c983532ec", + "simStepLabel": "Client: Display Success Feedback", + "simStepDescription": "The client application receives the successful API response. It then closes the invitation dialog and displays a toast notification to the admin, confirming that the invitation has been sent.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Account/components/TeamPanel.jsx", + "startLine": "145", + "endLine": "149", + "relevantVariables": [ + "createToast", + "closeInviteModal" + ] + }, + "inputDataExample": "{\n \"response\": {\n \"status\": 200,\n \"data\": {\n \"success\": true,\n \"msg\": \"Invite sent successfully\"\n }\n }\n}", + "outputDataExample": "{\n \"uiState\": {\n \"toast\": {\n \"type\": \"success\",\n \"message\": \"Invite sent successfully\"\n }\n }\n}" + } + ], + "description": "
  • Enables collaborative use of the application within an organization with proper access controls
  • - The system supports multiple users organized into teams
  • - Implements role-based access control (RBAC) with roles like 'superadmin' and 'admin', restricting access to sensitive areas like settings and user management
  • - Superadmins can invite new users to the team via email, assigning them a specific role
  • - Users can manage their own profiles and change their passwords
", + "simulationNodesAndEdges": { + "3de2d413-5e9b-49f4-9409-74be32c52e0f": { + "simStepIds": [ + "2d5a952a-ce62-4948-9309-96b28ad74e59" + ] + }, + "b27f1bba-c655-40c6-a1dc-71e78c590c92": { + "simStepIds": [ + "eebaca25-d588-49c4-a35c-9abc82fde580" + ] + }, + "8b3cb6c2-ed57-4510-be15-decb8e3c89e1": { + "simStepIds": [ + "50dd7316-7d01-43f7-9036-d97d51a9c178" + ] + }, + "ae858158-8db6-4917-8fda-0966a953f6ed": { + "simStepIds": [ + "55fe9963-30d2-47f8-859f-b485a77064c4" + ] + }, + "a7be2d0b-0227-4228-a0a6-074f7324fd89": { + "simStepIds": [ + "7d1f86f2-c8d5-48fb-a8c1-b337dc56271f" + ] + }, + "02d87257-883c-4822-a421-823c983532ec": { + "simStepIds": [ + "700f35e0-b39d-417c-b0a8-0612feade977" + ] + }, + "c441312f-f3c6-48c6-b1b9-7c12749d91bb": { + "simStepIds": [ + "10f80ada-2d0a-4e98-bd5d-057e7d4fe6de" + ] + }, + "3530244f-2689-444e-bcbf-611db2e3c000": { + "simStepIds": [ + "16d87608-4458-430c-b459-cd9b1d413fa6" + ] + }, + "2ee1c4e8-90fb-4470-ad9f-6ffed5420853": { + "simStepIds": [ + "6c024d4c-178b-45fc-8bca-4fc6c00549a4" + ] + }, + "bdd52e6f-ddf7-4966-b3af-957f9553082f": { + "simStepIds": [ + "c796f8ad-d548-45f3-8d62-67f59c6c1aa4" + ] + }, + "52732a37-82b8-40a7-b12d-dc1b82380b2d": { + "simStepIds": [ + "6ba5094d-d1e6-4ec3-8917-28cdb30f2ef9" + ] + } + }, + "isAIGenerated": true, + "keywords": "isAllowed, RoleProtectedRoute, Team", + "generationPrompt": "User and Team Management with Role-Based Access", + "generationKeywords": "isAllowed, RoleProtectedRoute, Team" + }, + "Bulk Monitor Management": { + "name": "Bulk Monitor Management", + "simSteps": [ + { + "simStepId": "a29da01e-8c5a-4e6d-8d3c-e49af528e4fa", + "diagramNodeId": "c4caeba8-ed9a-4cbc-9888-38cc8ca55362", + "simStepLabel": "Flow 1: CSV Import - Navigate to Bulk Import Page", + "simStepDescription": "The user navigates to the \"/uptime/bulk-import\" URL, which is routed to the BulkImport component for rendering the UI.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Routes/index.jsx", + "startLine": "88", + "endLine": "91", + "relevantVariables": [ + "Route", + "BulkImport" + ] + }, + "inputDataExample": "{\"url\": \"/uptime/bulk-import\"}", + "outputDataExample": "{\"component\": \"BulkImport\"}" + }, + { + "simStepId": "ef0bbcff-9535-4c0e-a5ca-96c457f89139", + "diagramNodeId": "c9a012bc-d53f-494f-82f2-1ee3f40ce646", + "simStepLabel": "Flow 1: CSV Import - Render UI", + "simStepDescription": "The BulkImport component is rendered, displaying the file upload interface to the user.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/BulkImport/index.jsx", + "startLine": "16", + "endLine": "50", + "relevantVariables": [ + "BulkImport" + ] + }, + "inputDataExample": "{\"props\": {}}", + "outputDataExample": "{\"props\": {}}" + }, + { + "simStepId": "bbd04d55-fc6a-4ba8-bcde-5a4dc263d6eb", + "diagramNodeId": "6f1948fa-e84a-4f8e-8f3b-8f24c511b3d8", + "simStepLabel": "Flow 1: CSV Import - User Selects File", + "simStepDescription": "The user clicks the 'Select File' button, triggering the file input. When a file is chosen, the `handleFileChange` function validates that it's a CSV file and updates the component's state.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx", + "startLine": "23", + "endLine": "31", + "relevantVariables": [ + "handleFileChange", + "selectedFile", + "setError", + "setFile" + ] + }, + "inputDataExample": "{\"event\": {\"target\": {\"files\": [{\"name\": \"monitors_to_import.csv\", \"type\": \"text/csv\", \"size\": 512}]}}}", + "outputDataExample": "{\"stateUpdate\": {\"selectedFile\": {\"name\": \"monitors_to_import.csv\", \"type\": \"text/csv\", \"size\": 512}}}" + }, + { + "simStepId": "610120d2-0521-4444-b245-feb97239a389", + "diagramNodeId": "e03a8ce2-c2cd-4147-8bab-63d6c2cf0a48", + "simStepLabel": "Flow 1: CSV Import - File State Update", + "simStepDescription": "The selected file object is passed from the UploadFile component to the parent BulkImport component via the `onFileSelect` prop, updating its `selectedFile` state.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/BulkImport/index.jsx", + "startLine": "22", + "endLine": "22", + "relevantVariables": [ + "setSelectedFile" + ] + }, + "inputDataExample": "{\"selectedFile\": {\"name\": \"monitors_to_import.csv\", \"type\": \"text/csv\", \"size\": 512}}", + "outputDataExample": "{\"selectedFile\": {\"name\": \"monitors_to_import.csv\", \"type\": \"text/csv\", \"size\": 512}}" + }, + { + "simStepId": "0139fcaa-eb46-4176-b7bd-1cb15e7f1f4c", + "diagramNodeId": "dd2f9356-fad3-47d4-81d5-a5e038298e2b", + "simStepLabel": "Flow 1: CSV Import - Submit Upload", + "simStepDescription": "The user clicks the submit button, which executes the `handleSubmit` function. This function calls the `createBulkMonitors` function provided by the `useCreateBulkMonitors` custom hook.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/BulkImport/index.jsx", + "startLine": "32", + "endLine": "45", + "relevantVariables": [ + "handleSubmit", + "createBulkMonitors", + "selectedFile", + "user" + ] + }, + "inputDataExample": "{\"selectedFile\": {\"name\": \"monitors_to_import.csv\", \"type\": \"text/csv\", \"size\": 512}}", + "outputDataExample": "null" + }, + { + "simStepId": "a365d018-e80c-419c-80b3-5ffc8f14dc5c", + "diagramNodeId": "b8c0292d-afbb-4648-b484-1c27be96d52f", + "simStepLabel": "Flow 1: CSV Import - Prepare Form Data", + "simStepDescription": "The `useCreateBulkMonitors` hook creates a `FormData` object and appends the selected CSV file to it, preparing it for the network request.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Hooks/v1/monitorHooks.js", + "startLine": "490", + "endLine": "493", + "relevantVariables": [ + "createBulkMonitors", + "formData" + ] + }, + "inputDataExample": "{\"file\": {\"name\": \"monitors_to_import.csv\", \"type\": \"text/csv\", \"size\": 512}}", + "outputDataExample": "{\"formData\": {\"csvFile\": \"\"}}" + }, + { + "simStepId": "2fc087b3-033d-4cf5-81f2-e35b99a14bce", + "diagramNodeId": "5967e576-f542-4c0b-a3f4-9446ae992721", + "simStepLabel": "Flow 1: CSV Import - Send API Request", + "simStepDescription": "The NetworkService sends the `FormData` containing the CSV file to the backend via a POST request to the `/monitors/bulk` endpoint. The `Content-Type` is set to `multipart/form-data`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "978", + "endLine": "983", + "relevantVariables": [ + "createBulkMonitors", + "this.axiosInstance.post" + ] + }, + "inputDataExample": "{\"url\": \"/monitors/bulk\", \"formData\": {\"csvFile\": \"\"}}", + "outputDataExample": "{\"promise\": \"\"}" + }, + { + "simStepId": "e6dec1dc-33cb-4669-a3b0-7a3c0b233185", + "diagramNodeId": "23715cce-c83a-44c0-9853-4c95b85e5ecd", + "simStepLabel": "Flow 1: CSV Import - Transmit Data to Server", + "simStepDescription": "The HTTP POST request containing the multipart form data is transmitted from the client to the server's API endpoint.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/monitorRoute.js", + "startLine": "46", + "endLine": "46", + "relevantVariables": [ + "this.router.post", + "upload.single", + "this.monitorController.createBulkMonitors" + ] + }, + "inputDataExample": "{\"request\": {\"method\": \"POST\", \"endpoint\": \"/api/v1/monitors/bulk\", \"headers\": {\"Content-Type\": \"multipart/form-data\"}, \"body\": \"---boundary\\nContent-Disposition: form-data; name=\\\"csvFile\\\"; filename=\\\"monitors.csv\\\"\\n...\"}}", + "outputDataExample": "{\"request\": {\"method\": \"POST\", \"endpoint\": \"/api/v1/monitors/bulk\", \"headers\": {\"Content-Type\": \"multipart/form-data\"}, \"body\": \"---boundary\\nContent-Disposition: form-data; name=\\\"csvFile\\\"; filename=\\\"monitors.csv\\\"\\n...\"}}" + }, + { + "simStepId": "07bf3d83-047d-4e51-9f96-8c9c64848500", + "diagramNodeId": "c1ac3c26-996a-43ef-9c34-4c2776188784", + "simStepLabel": "Flow 1: CSV Import - Server Receives Request", + "simStepDescription": "The server's monitor route receives the request. The `multer` middleware processes the file upload, making the file available in `req.file`. The request is then passed to the `createBulkMonitors` controller method.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/monitorRoute.js", + "startLine": "46", + "endLine": "46", + "relevantVariables": [ + "this.router.post", + "upload.single", + "this.monitorController.createBulkMonitors" + ] + }, + "inputDataExample": "{\"request\": {\"endpoint\": \"/monitors/bulk\"}}", + "outputDataExample": "{\"req.file\": {\"fieldname\": \"csvFile\", \"originalname\": \"monitors.csv\", \"buffer\": \"\"}}" + }, + { + "simStepId": "c53d3f62-73fe-4fad-9835-ef2c8cf52894", + "diagramNodeId": "20d0e690-a569-4c6f-8238-916cac920553", + "simStepLabel": "Flow 1: CSV Import - Route to Controller", + "simStepDescription": "The request object, now populated with the uploaded file buffer by the middleware, is passed from the routing layer to the monitor controller for business logic processing.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/monitorController.js", + "startLine": "206", + "endLine": "212", + "relevantVariables": [ + "createBulkMonitors", + "req.file" + ] + }, + "inputDataExample": "{\"req\": {\"file\": {\"buffer\": \"\"}, \"user\": {\"_id\": \"user_123\", \"teamId\": \"team_abc\"}}}", + "outputDataExample": "{\"req\": {\"file\": {\"buffer\": \"\"}, \"user\": {\"_id\": \"user_123\", \"teamId\": \"team_abc\"}}}" + }, + { + "simStepId": "a3799570-64bf-481e-9ffe-31dbad8a36e4", + "diagramNodeId": "0e520a6d-34bf-467b-a894-e73979364478", + "simStepLabel": "Flow 1: CSV Import - Controller Processes Data", + "simStepDescription": "The controller validates the presence of the file and extracts the file data (buffer), `userId`, and `teamId`. It then calls the `monitorService.createBulkMonitors` method to handle the core logic.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/monitorController.js", + "startLine": "228", + "endLine": "232", + "relevantVariables": [ + "fileData", + "monitors", + "this.monitorService.createBulkMonitors" + ] + }, + "inputDataExample": "{\"file\": {\"buffer\": \"\"}, \"user\": {\"_id\": \"user_123\", \"teamId\": \"team_abc\"}}", + "outputDataExample": "null" + }, + { + "simStepId": "6038422c-ab83-4fc6-a58b-c75734276118", + "diagramNodeId": "7292bf6e-431d-4a9d-94ce-2bcfabcaa79d", + "simStepLabel": "Flow 1: CSV Import - Controller to Service", + "simStepDescription": "The file buffer and user/team identifiers are passed from the controller to the monitor service layer.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/monitorService.js", + "startLine": "84", + "endLine": "84", + "relevantVariables": [ + "createBulkMonitors" + ] + }, + "inputDataExample": "{\"fileData\": \"\", \"userId\": \"user_123\", \"teamId\": \"team_abc\"}", + "outputDataExample": "{\"fileData\": \"\", \"userId\": \"user_123\", \"teamId\": \"team_abc\"}" + }, + { + "simStepId": "2c7c3082-e4b6-4525-860f-2ffcd9e86db1", + "diagramNodeId": "6139e487-1dd5-405a-ba7d-ededec4b282e", + "simStepLabel": "Flow 1: CSV Import - Service Parses CSV", + "simStepDescription": "The monitor service uses the `papaparse` library to parse the CSV file buffer into a JSON object array. In the `complete` callback, it enriches each record with `userId` and `teamId`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/monitorService.js", + "startLine": "85", + "endLine": "134", + "relevantVariables": [ + "papaparse.parse", + "enrichedData", + "this.db.monitorModule.createBulkMonitors" + ] + }, + "inputDataExample": "{\"fileData\": \"name,type,url\\nGoogle,http,https://google.com\\n\"}", + "outputDataExample": "{\"enrichedData\": [{\"name\": \"Google\", \"type\": \"http\", \"url\": \"https://google.com\", \"userId\": \"user_123\", \"teamId\": \"team_abc\"}]}" + }, + { + "simStepId": "0115fb69-4959-4b32-bd90-1dcd3c2231f6", + "diagramNodeId": "b55a0dc6-f596-4e5f-9265-63588b46b519", + "simStepLabel": "Flow 1: CSV Import - Service to Database Module", + "simStepDescription": "The array of parsed and enriched monitor objects is passed to the database module for persistence.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/monitorService.js", + "startLine": "126", + "endLine": "126", + "relevantVariables": [ + "this.db.monitorModule.createBulkMonitors" + ] + }, + "inputDataExample": "{\"enrichedData\": [{\"name\": \"Google\", \"type\": \"http\", \"url\": \"https://google.com\", \"userId\": \"user_123\", \"teamId\": \"team_abc\"}]}", + "outputDataExample": "{\"enrichedData\": [{\"name\": \"Google\", \"type\": \"http\", \"url\": \"https://google.com\", \"userId\": \"user_123\", \"teamId\": \"team_abc\"}]}" + }, + { + "simStepId": "70f18c21-2918-402e-9c35-1bdce8bc82d4", + "diagramNodeId": "d9d519af-5c4d-4494-929e-a869581c1dc4", + "simStepLabel": "Flow 1: CSV Import - Database Bulk Save", + "simStepDescription": "The `monitorModule` uses `Monitor.bulkSave` to efficiently insert all the new monitor documents into the MongoDB database in a single operation.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/monitorModule.js", + "startLine": "465", + "endLine": "468", + "relevantVariables": [ + "createBulkMonitors", + "this.Monitor.bulkSave" + ] + }, + "inputDataExample": "[{\"name\": \"Google\", \"type\": \"http\", \"url\": \"https://google.com\"}]", + "outputDataExample": "[{\"_id\": \"monitor_xyz\", \"name\": \"Google\"}]" + }, + { + "simStepId": "2ee7bd94-d131-4dd8-bdf8-68433d00472d", + "diagramNodeId": "f27c60ef-fb40-43e2-9750-c2617bdeaf43", + "simStepLabel": "Flow 1: CSV Import - Database to Service", + "simStepDescription": "The newly created monitor documents, now with database-generated `_id`s, are returned from the database module back to the service layer.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/db/v1/modules/monitorModule.js", + "startLine": "467", + "endLine": "467", + "relevantVariables": [ + "monitors" + ] + }, + "inputDataExample": "[{\"_id\": \"monitor_xyz\", \"name\": \"Google\"}]", + "outputDataExample": "[{\"_id\": \"monitor_xyz\", \"name\": \"Google\"}]" + }, + { + "simStepId": "66fbf913-e662-4bd9-ada6-077be9e37f07", + "diagramNodeId": "1e1d975f-7f0f-4132-bd71-2563b35e99b3", + "simStepLabel": "Flow 1: CSV Import - Enqueue Monitor Jobs", + "simStepDescription": "After the monitors are saved, the service iterates through them and adds a new job to the `jobQueue` for each one, scheduling them for their first check. It then resolves the promise, returning the data up the call stack.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/monitorService.js", + "startLine": "128", + "endLine": "132", + "relevantVariables": [ + "Promise.all", + "this.jobQueue.addJob" + ] + }, + "inputDataExample": "[{\"_id\": \"monitor_xyz\", \"name\": \"Google\"}]", + "outputDataExample": "[{\"_id\": \"monitor_xyz\", \"name\": \"Google\"}]" + }, + { + "simStepId": "94d5772d-3f46-4d43-b764-3493db5c4971", + "diagramNodeId": "c449f81a-d6af-4f7a-b2d8-050a9a9ee0f9", + "simStepLabel": "Flow 1: CSV Import - Service to Controller", + "simStepDescription": "The final list of created monitors is passed back to the controller.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/monitorController.js", + "startLine": "230", + "endLine": "230", + "relevantVariables": [ + "monitors" + ] + }, + "inputDataExample": "[{\"_id\": \"monitor_xyz\", \"name\": \"Google\"}]", + "outputDataExample": "[{\"_id\": \"monitor_xyz\", \"name\": \"Google\"}]" + }, + { + "simStepId": "f5377571-156f-4c5c-8bab-b6dacdf0fea6", + "diagramNodeId": "72b55fa1-d944-4e46-ac98-f6a6cb201afa", + "simStepLabel": "Flow 1: CSV Import - Send Success Response", + "simStepDescription": "The controller constructs a successful JSON response containing a message and the data for the newly created monitors, and sends it back to the client.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/monitorController.js", + "startLine": "232", + "endLine": "235", + "relevantVariables": [ + "res.success" + ] + }, + "inputDataExample": "[{\"_id\": \"monitor_xyz\", \"name\": \"Google\"}]", + "outputDataExample": "{\"msg\": \"Monitors created successfully.\", \"data\": [{\"_id\": \"monitor_xyz\"}]}" + }, + { + "simStepId": "c60a3adb-dd81-41b0-95e4-a0a6d23f9dd0", + "diagramNodeId": "926a330c-a9c5-4983-a4b5-70359813e49a", + "simStepLabel": "Flow 1: CSV Import - Transmit Success to Client", + "simStepDescription": "The server sends the successful HTTP response back to the client's browser.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Hooks/v1/monitorHooks.js", + "startLine": "494", + "endLine": "494", + "relevantVariables": [ + "response" + ] + }, + "inputDataExample": "{\"status\": 200, \"body\": {\"msg\": \"Monitors created successfully!\", \"data\": [{\"_id\": \"monitor_xyz\"}]}}", + "outputDataExample": "{\"status\": 200, \"body\": {\"msg\": \"Monitors created successfully!\", \"data\": [{\"_id\": \"monitor_xyz\"}]}}" + }, + { + "simStepId": "72b3f16d-290d-4a61-8748-2ad90d63ec68", + "diagramNodeId": "ae3b7e23-ed64-40aa-9760-389f0a869f8d", + "simStepLabel": "Flow 1: CSV Import - Client Handles Success", + "simStepDescription": "The client-side promise resolves. The `handleSubmit` function in the `BulkImport` component receives the success status, displays a success toast message, and navigates the user to the main uptime monitoring page.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Uptime/BulkImport/index.jsx", + "startLine": "37", + "endLine": "41", + "relevantVariables": [ + "createToast", + "navigate" + ] + }, + "inputDataExample": "{\"success\": true, \"data\": [{\"_id\": \"monitor_xyz\"}]}", + "outputDataExample": "{\"navigation\": \"/uptime\"}" + }, + { + "simStepId": "63d8d81f-f436-4594-a6eb-8c0fd2110bc1", + "diagramNodeId": "26d76516-fd7e-4ae8-956a-d077de4257fa", + "simStepLabel": "Flow 2: CSV Export - Initiate Export", + "simStepDescription": "An admin user clicks an 'Export to CSV' button in the UI. This action triggers a call to the `/monitors/export` API endpoint to begin the export process.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/monitorRoute.js", + "startLine": "45", + "endLine": "45", + "relevantVariables": [ + "this.router.get", + "this.monitorController.exportMonitorsToCSV" + ] + }, + "inputDataExample": "{}", + "outputDataExample": "{\"request\": {\"method\": \"GET\", \"endpoint\": \"/api/v1/monitors/export\"}}" + }, + { + "simStepId": "53cebb8e-cf45-49eb-a7d3-98fd87af21b4", + "diagramNodeId": "febea87e-5033-479c-ba7a-eb35d98db899", + "simStepLabel": "Flow 2: CSV Export - Transmit Request to Server", + "simStepDescription": "An HTTP GET request is sent from the client to the server to fetch the monitor data as a CSV file.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/routes/v1/monitorRoute.js", + "startLine": "45", + "endLine": "45", + "relevantVariables": [ + "this.router.get", + "this.monitorController.exportMonitorsToCSV" + ] + }, + "inputDataExample": "{\"request\": {\"method\": \"GET\", \"endpoint\": \"/api/v1/monitors/export\"}}", + "outputDataExample": "{\"request\": {\"method\": \"GET\", \"endpoint\": \"/api/v1/monitors/export\"}}" + }, + { + "simStepId": "d9db3f40-8add-4551-9323-274348b9800f", + "diagramNodeId": "a3106388-6a3d-4b68-a3fc-3761a0d54f98", + "simStepLabel": "Flow 2: CSV Export - Controller Handles Request", + "simStepDescription": "The `exportMonitorsToCSV` method in the monitor controller is invoked. It retrieves the `teamId` from the authenticated user's session and calls the monitor service to perform the export.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/monitorController.js", + "startLine": "427", + "endLine": "434", + "relevantVariables": [ + "exportMonitorsToCSV", + "teamId", + "this.monitorService.exportMonitorsToCSV" + ] + }, + "inputDataExample": "{\"req\": {\"user\": {\"teamId\": \"team_abc\"}}}", + "outputDataExample": "{\"teamId\": \"team_abc\"}" + }, + { + "simStepId": "74e95fb5-863c-4137-a41b-d7a32efa04a5", + "diagramNodeId": "3e0f0966-5c2f-41af-9bcc-e8f615e03a00", + "simStepLabel": "Flow 2: CSV Export - Controller to Service", + "simStepDescription": "The `teamId` is passed from the controller to the monitor service to ensure only monitors belonging to the correct team are exported.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/monitorService.js", + "startLine": "245", + "endLine": "245", + "relevantVariables": [ + "exportMonitorsToCSV", + "teamId" + ] + }, + "inputDataExample": "{\"teamId\": \"team_abc\"}", + "outputDataExample": "{\"teamId\": \"team_abc\"}" + }, + { + "simStepId": "85cd1d5d-7ffb-49c5-aacb-907048003fc5", + "diagramNodeId": "ab9cb862-0ac7-45a3-9888-f64e87bdda54", + "simStepLabel": "Flow 2: CSV Export - Generate CSV", + "simStepDescription": "The monitor service fetches all monitors for the given `teamId` from the database. It then maps the data into the required format and uses `papaparse` to convert the JSON array into a CSV-formatted string.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/monitorService.js", + "startLine": "245", + "endLine": "266", + "relevantVariables": [ + "getMonitorsByTeamId", + "csvData", + "this.papaparse.unparse" + ] + }, + "inputDataExample": "{\"teamId\": \"team_abc\"}", + "outputDataExample": "{\"csv\": \"name,description,type,url,interval,port,ignoreTlsErrors,isActive\\nMy Website,,http,https://example.com,60,,false,true\"}" + }, + { + "simStepId": "4deb5e47-2ac4-4638-9ee6-48295d027bcd", + "diagramNodeId": "ad300b1c-6748-40f9-a7c9-5895e0915991", + "simStepLabel": "Flow 2: CSV Export - Service to Controller", + "simStepDescription": "The generated CSV string is returned from the service layer to the controller.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/monitorController.js", + "startLine": "432", + "endLine": "432", + "relevantVariables": [ + "csv" + ] + }, + "inputDataExample": "{\"csv\": \"name,description,type,url,...\\nMy Website,,http,https://example.com,...\"}", + "outputDataExample": "{\"csv\": \"name,description,type,url,...\\nMy Website,,http,https://example.com,...\"}" + }, + { + "simStepId": "256095b8-5e3a-4c20-abd9-be9e6a9d0bad", + "diagramNodeId": "f151f89d-6399-48f0-aecd-5eb1572b48cc", + "simStepLabel": "Flow 2: CSV Export - Send File Response", + "simStepDescription": "The controller receives the CSV string and uses the `res.file` method to send it back to the client as a file. It sets the `Content-Type` to `text/csv` and `Content-Disposition` to trigger a download with the filename `monitors.csv`.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/monitorController.js", + "startLine": "434", + "endLine": "440", + "relevantVariables": [ + "res.file", + "csv" + ] + }, + "inputDataExample": "{\"csv\": \"name,description,type,url,...\\nMy Website,,http,https://example.com,...\"}", + "outputDataExample": "{\"responseHeaders\": {\"Content-Type\": \"text/csv\", \"Content-Disposition\": \"attachment; filename=\\\"monitors.csv\\\"\"}}" + }, + { + "simStepId": "605f4cbd-1f7f-4e89-b028-f18737ac7fb1", + "diagramNodeId": "3dfb432a-2ce4-4f92-98ce-5d114b5f7f03", + "simStepLabel": "Flow 2: CSV Export - Transmit File to Client", + "simStepDescription": "The server sends the HTTP response, which contains the CSV data and headers, to the client's browser.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/monitorController.js", + "startLine": "434", + "endLine": "440", + "relevantVariables": [ + "res.file" + ] + }, + "inputDataExample": "{\"httpResponse\": {\"headers\": {\"Content-Disposition\": \"attachment; filename=\\\"monitors.csv\\\"\"}, \"body\": \"name,description,...\\n...\"}}", + "outputDataExample": "{\"httpResponse\": {\"headers\": {\"Content-Disposition\": \"attachment; filename=\\\"monitors.csv\\\"\"}, \"body\": \"name,description,...\\n...\"}}" + }, + { + "simStepId": "60abdb27-4078-4678-a010-0f6938d4be48", + "diagramNodeId": "d8877474-7203-484f-bd47-fc69e3c7c482", + "simStepLabel": "Flow 2: CSV Export - Browser Prompts Download", + "simStepDescription": "The browser receives the server's response. The `Content-Disposition` header instructs the browser to prompt the user to save the received data as a file named `monitors.csv`, completing the export process.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/monitorController.js", + "startLine": "434", + "endLine": "440", + "relevantVariables": [ + "res.file" + ] + }, + "inputDataExample": "{\"httpResponse\": {\"headers\": {\"Content-Disposition\": \"attachment; filename=\\\"monitors.csv\\\"\"}}}", + "outputDataExample": "{\"userAction\": \"File Save Dialog\"}" + } + ], + "description": "
  • Provides efficiency tools for managing a large number of monitors
  • - Users can import multiple monitors at once by uploading a CSV file, saving significant time during initial setup
  • - A template CSV is provided to guide the user on the required format and available fields
  • - All existing monitors can be exported to a CSV file, useful for backups or migrations
  • - Superadmins have the ability to delete all monitors for a team in a single action
", + "simulationNodesAndEdges": { + "c4caeba8-ed9a-4cbc-9888-38cc8ca55362": { + "simStepIds": [ + "a29da01e-8c5a-4e6d-8d3c-e49af528e4fa" + ] + }, + "6f1948fa-e84a-4f8e-8f3b-8f24c511b3d8": { + "simStepIds": [ + "bbd04d55-fc6a-4ba8-bcde-5a4dc263d6eb" + ] + }, + "dd2f9356-fad3-47d4-81d5-a5e038298e2b": { + "simStepIds": [ + "0139fcaa-eb46-4176-b7bd-1cb15e7f1f4c" + ] + }, + "5967e576-f542-4c0b-a3f4-9446ae992721": { + "simStepIds": [ + "2fc087b3-033d-4cf5-81f2-e35b99a14bce" + ] + }, + "c1ac3c26-996a-43ef-9c34-4c2776188784": { + "simStepIds": [ + "07bf3d83-047d-4e51-9f96-8c9c64848500" + ] + }, + "0e520a6d-34bf-467b-a894-e73979364478": { + "simStepIds": [ + "a3799570-64bf-481e-9ffe-31dbad8a36e4" + ] + }, + "6139e487-1dd5-405a-ba7d-ededec4b282e": { + "simStepIds": [ + "2c7c3082-e4b6-4525-860f-2ffcd9e86db1" + ] + }, + "d9d519af-5c4d-4494-929e-a869581c1dc4": { + "simStepIds": [ + "70f18c21-2918-402e-9c35-1bdce8bc82d4" + ] + }, + "1e1d975f-7f0f-4132-bd71-2563b35e99b3": { + "simStepIds": [ + "66fbf913-e662-4bd9-ada6-077be9e37f07" + ] + }, + "72b55fa1-d944-4e46-ac98-f6a6cb201afa": { + "simStepIds": [ + "f5377571-156f-4c5c-8bab-b6dacdf0fea6" + ] + }, + "ae3b7e23-ed64-40aa-9760-389f0a869f8d": { + "simStepIds": [ + "72b3f16d-290d-4a61-8748-2ad90d63ec68" + ] + }, + "26d76516-fd7e-4ae8-956a-d077de4257fa": { + "simStepIds": [ + "63d8d81f-f436-4594-a6eb-8c0fd2110bc1" + ] + }, + "a3106388-6a3d-4b68-a3fc-3761a0d54f98": { + "simStepIds": [ + "d9db3f40-8add-4551-9323-274348b9800f" + ] + }, + "ab9cb862-0ac7-45a3-9888-f64e87bdda54": { + "simStepIds": [ + "85cd1d5d-7ffb-49c5-aacb-907048003fc5" + ] + }, + "f151f89d-6399-48f0-aecd-5eb1572b48cc": { + "simStepIds": [ + "256095b8-5e3a-4c20-abd9-be9e6a9d0bad" + ] + }, + "d8877474-7203-484f-bd47-fc69e3c7c482": { + "simStepIds": [ + "60abdb27-4078-4678-a010-0f6938d4be48" + ] + }, + "c9a012bc-d53f-494f-82f2-1ee3f40ce646": { + "simStepIds": [ + "ef0bbcff-9535-4c0e-a5ca-96c457f89139" + ] + }, + "e03a8ce2-c2cd-4147-8bab-63d6c2cf0a48": { + "simStepIds": [ + "610120d2-0521-4444-b245-feb97239a389" + ] + }, + "b8c0292d-afbb-4648-b484-1c27be96d52f": { + "simStepIds": [ + "a365d018-e80c-419c-80b3-5ffc8f14dc5c" + ] + }, + "23715cce-c83a-44c0-9853-4c95b85e5ecd": { + "simStepIds": [ + "e6dec1dc-33cb-4669-a3b0-7a3c0b233185" + ] + }, + "20d0e690-a569-4c6f-8238-916cac920553": { + "simStepIds": [ + "c53d3f62-73fe-4fad-9835-ef2c8cf52894" + ] + }, + "7292bf6e-431d-4a9d-94ce-2bcfabcaa79d": { + "simStepIds": [ + "6038422c-ab83-4fc6-a58b-c75734276118" + ] + }, + "b55a0dc6-f596-4e5f-9265-63588b46b519": { + "simStepIds": [ + "0115fb69-4959-4b32-bd90-1dcd3c2231f6" + ] + }, + "f27c60ef-fb40-43e2-9750-c2617bdeaf43": { + "simStepIds": [ + "2ee7bd94-d131-4dd8-bdf8-68433d00472d" + ] + }, + "c449f81a-d6af-4f7a-b2d8-050a9a9ee0f9": { + "simStepIds": [ + "94d5772d-3f46-4d43-b764-3493db5c4971" + ] + }, + "926a330c-a9c5-4983-a4b5-70359813e49a": { + "simStepIds": [ + "c60a3adb-dd81-41b0-95e4-a0a6d23f9dd0" + ] + }, + "febea87e-5033-479c-ba7a-eb35d98db899": { + "simStepIds": [ + "53cebb8e-cf45-49eb-a7d3-98fd87af21b4" + ] + }, + "3e0f0966-5c2f-41af-9bcc-e8f615e03a00": { + "simStepIds": [ + "74e95fb5-863c-4137-a41b-d7a32efa04a5" + ] + }, + "ad300b1c-6748-40f9-a7c9-5895e0915991": { + "simStepIds": [ + "4deb5e47-2ac4-4638-9ee6-48295d027bcd" + ] + }, + "3dfb432a-2ce4-4f92-98ce-5d114b5f7f03": { + "simStepIds": [ + "605f4cbd-1f7f-4e89-b028-f18737ac7fb1" + ] + } + }, + "isAIGenerated": true, + "keywords": "bulkImport, createBulkMonitors, exportMonitorsToCSV", + "generationPrompt": "Bulk Monitor Management", + "generationKeywords": "bulkImport, createBulkMonitors, exportMonitorsToCSV" + }, + "System Administration and Configuration": { + "name": "System Administration and Configuration", + "simSteps": [ + { + "simStepId": "64e9f1af-13b7-4361-a2a9-3f45760e316d", + "diagramNodeId": "97908daf-d071-4df5-8366-1f5f4d8d534a", + "simStepLabel": "Flow 1: Render Settings Page", + "simStepDescription": "The user navigates to the settings page. The main `Settings` component fetches existing settings using the `useFetchSettings` hook and renders various sub-components for different configuration areas like TimeZone, UI, PageSpeed, Email, etc.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Settings/index.jsx", + "startLine": "25", + "endLine": "52", + "relevantVariables": [ + "useFetchSettings", + "settingsData", + "setSettingsData", + "isApiKeySet", + "isEmailPasswordSet" + ] + }, + "inputDataExample": "{}", + "outputDataExample": "{\"settingsData\": {}, \"errors\": {}, \"isApiKeySet\": false, \"isEmailPasswordSet\": false}" + }, + { + "simStepId": "dca4a33c-b666-47b8-896b-370ed9d6f9c5", + "diagramNodeId": "f6d80f09-ae83-4c95-aba0-4efffeda38e0", + "simStepLabel": "API Call: Fetch Application Settings", + "simStepDescription": "The `useFetchSettings` hook triggers a GET request to the `/api/v1/settings` endpoint to retrieve the current application configuration.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "692", + "endLine": "698", + "relevantVariables": [ + "getAppSettings" + ] + }, + "inputDataExample": "{\"method\": \"GET\", \"url\": \"/api/v1/settings\"}", + "outputDataExample": "{\"method\": \"GET\", \"url\": \"/api/v1/settings\"}" + }, + { + "simStepId": "441b0c45-5f27-478d-8ca8-58fbd5f460ea", + "diagramNodeId": "83596bd1-dfca-4196-b52e-955f6824fafb", + "simStepLabel": "Backend: Retrieve Settings from Database", + "simStepDescription": "The `settingsController` handles the GET request. It calls the `settingsService` to fetch settings from the database. Sensitive data like passwords is sanitized, and flags like `emailPasswordSet` are added before sending the data to the client.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/settingsController.js", + "startLine": "40", + "endLine": "49", + "relevantVariables": [ + "getAppSettings", + "settingsService.getDBSettings", + "buildAppSettings" + ] + }, + "inputDataExample": "{\"request\": \"GET /api/v1/settings\"}", + "outputDataExample": "{\"msg\": \"Got app settings successfully\", \"data\": {\"emailPasswordSet\": false, \"pagespeedKeySet\": false, \"settings\": {\"checkTTL\": 7, \"timezone\": \"America/Toronto\"}}}" + }, + { + "simStepId": "aacc9302-6ca8-45c6-acb8-956d8d0242c2", + "diagramNodeId": "195ad532-9858-493e-876a-6ec457811d1d", + "simStepLabel": "API Response: Send Settings to Client", + "simStepDescription": "The backend responds to the GET request with a JSON object containing the application settings, which will be used to populate the UI.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/settingsController.js", + "startLine": "45", + "endLine": "48", + "relevantVariables": [ + "res.success" + ] + }, + "inputDataExample": "{\"msg\": \"Got app settings successfully\", \"data\": {\"emailPasswordSet\": false, \"pagespeedKeySet\": false, \"settings\": {\"checkTTL\": 7, \"timezone\": \"America/Toronto\", \"systemEmailHost\": \"\", \"systemEmailPort\": \"\", \"systemEmailSecure\": false, \"systemEmailUser\": \"\", \"systemEmailAddress\": \"\"}}}", + "outputDataExample": "{\"msg\": \"Got app settings successfully\", \"data\": {\"emailPasswordSet\": false, \"pagespeedKeySet\": false, \"settings\": {\"checkTTL\": 7, \"timezone\": \"America/Toronto\", \"systemEmailHost\": \"\", \"systemEmailPort\": \"\", \"systemEmailSecure\": false, \"systemEmailUser\": \"\", \"systemEmailAddress\": \"\"}}}" + }, + { + "simStepId": "ae5c5146-4dbb-4612-9d23-086e57f72474", + "diagramNodeId": "09cb194a-0ea4-46e2-b32b-36b62fa87f40", + "simStepLabel": "User Interaction: Modify Email Settings", + "simStepDescription": "The user interacts with the `SettingsEmail` component, filling in SMTP server details. The component's state is updated via the `handleChange` function passed down from the parent `Settings` component, which prepares the new configuration data.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Settings/SettingsEmail.jsx", + "startLine": "119", + "endLine": "167", + "relevantVariables": [ + "handleChange", + "handlePasswordChange", + "TextInput", + "systemEmailHost", + "systemEmailPort", + "systemEmailUser", + "systemEmailPassword" + ] + }, + "inputDataExample": "{\"settingsData\": {\"settings\": {\"systemEmailHost\": \"\"}}}", + "outputDataExample": "{\"settingsData\": {\"settings\": {\"systemEmailHost\": \"smtp.gmail.com\", \"systemEmailPort\": \"465\", \"systemEmailUser\": \"user@gmail.com\", \"systemEmailAddress\": \"user@gmail.com\", \"systemEmailPassword\": \"app-password-123\"}}}" + }, + { + "simStepId": "e7e82a97-1409-4bc5-ae27-b4ff5ba746df", + "diagramNodeId": "8c107d79-f796-49eb-ab88-f553dd6f9d95", + "simStepLabel": "User Action: Save Settings", + "simStepDescription": "The user clicks the 'Save' button. This action triggers the `handleSave` function, which validates the new settings and prepares to send them to the backend.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Settings/index.jsx", + "startLine": "258", + "endLine": "261", + "relevantVariables": [ + "handleSave" + ] + }, + "inputDataExample": "{\"settingsData\": {\"settings\": {\"systemEmailHost\": \"smtp.gmail.com\", \"systemEmailPort\": \"465\", \"systemEmailUser\": \"user@gmail.com\", \"systemEmailAddress\": \"user@gmail.com\", \"systemEmailPassword\": \"app-password-123\"}}}", + "outputDataExample": "{\"settingsData\": {\"settings\": {\"systemEmailHost\": \"smtp.gmail.com\", \"systemEmailPort\": \"465\", \"systemEmailUser\": \"user@gmail.com\", \"systemEmailAddress\": \"user@gmail.com\", \"systemEmailPassword\": \"app-password-123\"}}}" + }, + { + "simStepId": "d7098072-9a83-4369-8176-9646ba34e186", + "diagramNodeId": "1b2cf182-9ee3-4218-a7ec-b74e427ccddf", + "simStepLabel": "API Call: Update Application Settings", + "simStepDescription": "The `handleSave` function calls `saveSettings` from the `useSaveSettings` hook. This triggers a PUT request to `/api/v1/settings` with the new configuration in the request body.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Hooks/v1/settingsHooks.js", + "startLine": "41", + "endLine": "67", + "relevantVariables": [ + "saveSettings", + "networkService.updateAppSettings" + ] + }, + "inputDataExample": "{\"settings\": {\"systemEmailHost\": \"smtp.gmail.com\", \"systemEmailPort\": \"465\", \"systemEmailUser\": \"user@gmail.com\", \"systemEmailAddress\": \"user@gmail.com\", \"systemEmailPassword\": \"app-password-123\"}}", + "outputDataExample": "{\"settings\": {\"systemEmailHost\": \"smtp.gmail.com\", \"systemEmailPort\": \"465\", \"systemEmailUser\": \"user@gmail.com\", \"systemEmailAddress\": \"user@gmail.com\", \"systemEmailPassword\": \"app-password-123\"}}" + }, + { + "simStepId": "383387e5-4623-44ab-b2c4-d9ea046f4547", + "diagramNodeId": "ce3ed3ba-a35b-4881-8dfd-f8425b9bf812", + "simStepLabel": "Data Transmission: Send Updated Settings to Server", + "simStepDescription": "The PUT request containing the new settings is sent to the backend API endpoint for processing and persistence.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "711", + "endLine": "717", + "relevantVariables": [ + "updateAppSettings", + "this.axiosInstance.put" + ] + }, + "inputDataExample": "{\"settings\": {\"systemEmailHost\": \"smtp.gmail.com\", \"systemEmailPort\": \"465\", \"systemEmailUser\": \"user@gmail.com\", \"systemEmailAddress\": \"user@gmail.com\", \"systemEmailPassword\": \"app-password-123\"}}", + "outputDataExample": "{\"settings\": {\"systemEmailHost\": \"smtp.gmail.com\", \"systemEmailPort\": \"465\", \"systemEmailUser\": \"user@gmail.com\", \"systemEmailAddress\": \"user@gmail.com\", \"systemEmailPassword\": \"app-password-123\"}}" + }, + { + "simStepId": "7632e8d2-4086-4169-ae3f-bfbf8d657987", + "diagramNodeId": "cce3b9c8-f3b7-434b-b3c6-d8d164714f86", + "simStepLabel": "Backend: Persist Updated Settings", + "simStepDescription": "The `settingsController` receives the PUT request, validates the body, and calls the `db.settingsModule.updateAppSettings` method to update the settings document in the database.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/settingsController.js", + "startLine": "54", + "endLine": "64", + "relevantVariables": [ + "updateAppSettings", + "updateAppSettingsBodyValidation", + "db.settingsModule.updateAppSettings" + ] + }, + "inputDataExample": "{\"settings\": {\"systemEmailHost\": \"smtp.gmail.com\", \"systemEmailPort\": \"465\", \"systemEmailUser\": \"user@gmail.com\", \"systemEmailAddress\": \"user@gmail.com\", \"systemEmailPassword\": \"app-password-123\"}}", + "outputDataExample": "{\"msg\": \"Updated app settings successfully\", \"data\": {\"emailPasswordSet\": true, \"settings\": {\"systemEmailHost\": \"smtp.gmail.com\", ...}}}" + }, + { + "simStepId": "0afcde35-7d51-4939-aae8-bac5ac83c49d", + "diagramNodeId": "fbe9b160-307e-4f51-8f72-713fda5373c8", + "simStepLabel": "API Response: Confirm Settings Update", + "simStepDescription": "The backend sends a success response to the client, including the newly saved (and sanitized) settings, confirming the update.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/settingsController.js", + "startLine": "60", + "endLine": "63", + "relevantVariables": [ + "res.success" + ] + }, + "inputDataExample": "{\"msg\": \"Updated app settings successfully\", \"data\": {\"emailPasswordSet\": true, \"pagespeedKeySet\": false, \"settings\": {\"checkTTL\": 7, \"timezone\": \"America/Toronto\", \"systemEmailHost\": \"smtp.gmail.com\", \"systemEmailPort\": 465, \"systemEmailUser\": \"user@gmail.com\", \"systemEmailAddress\": \"user@gmail.com\"}}}", + "outputDataExample": "{\"msg\": \"Updated app settings successfully\", \"data\": {\"emailPasswordSet\": true, \"pagespeedKeySet\": false, \"settings\": {\"checkTTL\": 7, \"timezone\": \"America/Toronto\", \"systemEmailHost\": \"smtp.gmail.com\", \"systemEmailPort\": 465, \"systemEmailUser\": \"user@gmail.com\", \"systemEmailAddress\": \"user@gmail.com\"}}}" + }, + { + "simStepId": "a157bd07-b969-44f8-914c-3d8393c1605a", + "diagramNodeId": "fff5dde4-8eb8-4c21-9c6d-0845a9749ff3", + "simStepLabel": "UI Update: Display Success Confirmation", + "simStepDescription": "The frontend network service receives the successful API response. The `saveSettings` hook then calls `createToast` with a success message, which is displayed to the user.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Hooks/v1/settingsHooks.js", + "startLine": "58", + "endLine": "58", + "relevantVariables": [ + "createToast", + "t(\"settingsSuccessSaved\")" + ] + }, + "inputDataExample": "{\"body\": \"Settings saved successfully\"}", + "outputDataExample": "{}" + }, + { + "simStepId": "a9b24b5f-534b-4baf-a185-bac7c32a1c42", + "diagramNodeId": "704e2ec8-9f2f-47e5-852f-c6ada67a02e0", + "simStepLabel": "Flow 2: Navigate to Diagnostics Page", + "simStepDescription": "The user navigates to the 'Logs' page and selects the 'Diagnostics' tab. The `Diagnostics` component renders and immediately calls the `useFetchDiagnostics` hook to retrieve system health data.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Logs/Diagnostics/index.jsx", + "startLine": "14", + "endLine": "15", + "relevantVariables": [ + "useFetchDiagnostics", + "diagnostics" + ] + }, + "inputDataExample": "{}", + "outputDataExample": "{}" + }, + { + "simStepId": "23b8ad7d-8967-4e48-814c-1a7bf5b4dfac", + "diagramNodeId": "499dfe1f-77d4-4b26-a12c-45240691109e", + "simStepLabel": "API Call: Fetch System Diagnostics", + "simStepDescription": "The `useFetchDiagnostics` hook triggers a GET request to the `/api/v1/diagnostic/system` endpoint to fetch real-time system metrics.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "client/src/Utils/NetworkService.js", + "startLine": "1113", + "endLine": "1119", + "relevantVariables": [ + "getDiagnostics", + "this.axiosInstance.get" + ] + }, + "inputDataExample": "{\"method\": \"GET\", \"url\": \"/api/v1/diagnostic/system\"}", + "outputDataExample": "{\"method\": \"GET\", \"url\": \"/api/v1/diagnostic/system\"}" + }, + { + "simStepId": "8fb704ca-67d9-437c-90d8-01302634d568", + "diagramNodeId": "09f07e80-185e-4547-aab4-edaf8ae7501e", + "simStepLabel": "Backend: Gather System Statistics", + "simStepDescription": "The `diagnosticController` receives the request and calls `diagnosticService.getSystemStats()`. This service uses native Node.js modules like `os` and `v8` to collect metrics on memory, CPU, heap statistics, and process uptime.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "server/src/service/v1/business/diagnosticService.js", + "startLine": "39", + "endLine": "95", + "relevantVariables": [ + "getSystemStats", + "os", + "process.memoryUsage", + "process.cpuUsage", + "v8.getHeapStatistics", + "process.uptime" + ] + }, + "inputDataExample": "{\"request\": \"GET /api/v1/diagnostic/system\"}", + "outputDataExample": "{\"osStats\": {\"totalMemoryBytes\": 16777216000, \"freeMemoryBytes\": 1572864000}, \"memoryUsage\": {\"rss\": 123456789}, \"cpuUsage\": {\"usagePercentage\": 12.5}, \"v8HeapStats\": {\"usedHeapSizeBytes\": 54321098}, \"uptimeMs\": 86400000}" + }, + { + "simStepId": "44a4372f-a903-4c43-a9a5-c7549c875e11", + "diagramNodeId": "d0f92604-a6fa-4d85-9716-ea8f32db78e0", + "simStepLabel": "API Response: Send Diagnostics Data to Client", + "simStepDescription": "The backend responds with a JSON object containing the comprehensive system diagnostics data.", + "isEdge": 1, + "sourceCodeMapping": { + "filePath": "server/src/controllers/v1/diagnosticController.js", + "startLine": "52", + "endLine": "55", + "relevantVariables": [ + "res.success" + ] + }, + "inputDataExample": "{\"msg\": \"OK\", \"data\": {\"osStats\": {\"totalMemoryBytes\": 16777216000, \"freeMemoryBytes\": 1572864000}, \"memoryUsage\": {\"rss\": 123456789}, \"cpuUsage\": {\"usagePercentage\": 12.5}, \"v8HeapStats\": {\"usedHeapSizeBytes\": 54321098}, \"uptimeMs\": 86400000}}", + "outputDataExample": "{\"msg\": \"OK\", \"data\": {\"osStats\": {\"totalMemoryBytes\": 16777216000, \"freeMemoryBytes\": 1572864000}, \"memoryUsage\": {\"rss\": 123456789}, \"cpuUsage\": {\"usagePercentage\": 12.5}, \"v8HeapStats\": {\"usedHeapSizeBytes\": 54321098}, \"uptimeMs\": 86400000}}" + }, + { + "simStepId": "e6c94f52-a599-4568-a06f-2db18f963817", + "diagramNodeId": "4b418736-6b72-47e0-917a-d86d4bed9ecd", + "simStepLabel": "UI Update: Display System Health", + "simStepDescription": "The frontend receives the diagnostic data. The `Diagnostics` component passes this data to its children, `Gauges` and `StatusBoxes`, which then render the information as interactive gauges and statistical boxes.", + "isEdge": 0, + "sourceCodeMapping": { + "filePath": "client/src/Pages/v1/Logs/Diagnostics/index.jsx", + "startLine": "59", + "endLine": "63", + "relevantVariables": [ + "Gauges", + "StatusBoxes", + "diagnostics" + ] + }, + "inputDataExample": "{\"diagnostics\": {\"osStats\": {\"totalMemoryBytes\": 16777216000, \"freeMemoryBytes\": 1572864000}, \"cpuUsage\": {\"usagePercentage\": 12.5}, \"v8HeapStats\": {\"usedHeapSizeBytes\": 54321098}, \"uptimeMs\": 86400000}}", + "outputDataExample": "{}" + } + ], + "description": "
  • Provides centralized settings for administrators to configure and maintain the Checkmate instance
  • - Admins can configure global application settings, including email server (SMTP) details for sending alerts
  • - UI settings like theme (light/dark) and default timezone can be customized
  • - Data retention policies, such as the Time-To-Live (TTL) for check history, can be adjusted
  • - A diagnostics page provides system information and logs for troubleshooting and monitoring the health of the Checkmate application itself
", + "simulationNodesAndEdges": { + "97908daf-d071-4df5-8366-1f5f4d8d534a": { + "simStepIds": [ + "64e9f1af-13b7-4361-a2a9-3f45760e316d" + ] + }, + "83596bd1-dfca-4196-b52e-955f6824fafb": { + "simStepIds": [ + "441b0c45-5f27-478d-8ca8-58fbd5f460ea" + ] + }, + "09cb194a-0ea4-46e2-b32b-36b62fa87f40": { + "simStepIds": [ + "ae5c5146-4dbb-4612-9d23-086e57f72474" + ] + }, + "1b2cf182-9ee3-4218-a7ec-b74e427ccddf": { + "simStepIds": [ + "d7098072-9a83-4369-8176-9646ba34e186" + ] + }, + "cce3b9c8-f3b7-434b-b3c6-d8d164714f86": { + "simStepIds": [ + "7632e8d2-4086-4169-ae3f-bfbf8d657987" + ] + }, + "fff5dde4-8eb8-4c21-9c6d-0845a9749ff3": { + "simStepIds": [ + "a157bd07-b969-44f8-914c-3d8393c1605a" + ] + }, + "704e2ec8-9f2f-47e5-852f-c6ada67a02e0": { + "simStepIds": [ + "a9b24b5f-534b-4baf-a185-bac7c32a1c42" + ] + }, + "09f07e80-185e-4547-aab4-edaf8ae7501e": { + "simStepIds": [ + "8fb704ca-67d9-437c-90d8-01302634d568" + ] + }, + "4b418736-6b72-47e0-917a-d86d4bed9ecd": { + "simStepIds": [ + "e6c94f52-a599-4568-a06f-2db18f963817" + ] + }, + "f6d80f09-ae83-4c95-aba0-4efffeda38e0": { + "simStepIds": [ + "dca4a33c-b666-47b8-896b-370ed9d6f9c5" + ] + }, + "195ad532-9858-493e-876a-6ec457811d1d": { + "simStepIds": [ + "aacc9302-6ca8-45c6-acb8-956d8d0242c2" + ] + }, + "8c107d79-f796-49eb-ab88-f553dd6f9d95": { + "simStepIds": [ + "e7e82a97-1409-4bc5-ae27-b4ff5ba746df" + ] + }, + "ce3ed3ba-a35b-4881-8dfd-f8425b9bf812": { + "simStepIds": [ + "383387e5-4623-44ab-b2c4-d9ea046f4547" + ] + }, + "fbe9b160-307e-4f51-8f72-713fda5373c8": { + "simStepIds": [ + "0afcde35-7d51-4939-aae8-bac5ac83c49d" + ] + }, + "499dfe1f-77d4-4b26-a12c-45240691109e": { + "simStepIds": [ + "23b8ad7d-8967-4e48-814c-1a7bf5b4dfac" + ] + }, + "d0f92604-a6fa-4d85-9716-ea8f32db78e0": { + "simStepIds": [ + "44a4372f-a903-4c43-a9a5-c7549c875e11" + ] + } + }, + "isAIGenerated": true, + "keywords": "Settings, updateAppSettings, diagnostic", + "generationPrompt": "System Administration and Configuration", + "generationKeywords": "Settings, updateAppSettings, diagnostic" + } + }, + "cellToPath": { + "0b23ea66-3e37-4643-b3b9-acb87ea02bae": "client", + "98af25c7-efc2-43d2-a273-c8da4e1038ad": "server", + "d3264577-10f0-42fe-9411-7e9df0754d1e": "client/src", + "35864a2c-7bd0-4428-84fc-c0aa90c7f1be": "server/src", + "f0feab85-316a-41b5-a2f1-629220018846": "client/src/Pages", + "4295a047-076f-4c07-95dd-b3123b6614f7": "client/src/Hooks", + "02b07589-ec21-4366-b7d2-9071e8f3bad8": "client/src/Utils", + "ffaaf471-ac3c-4ea3-a5a9-d5255169e54b": "server/src/routes", + "478e649c-1d1b-41d2-9b7b-6927e1dec7e1": "server/src/controllers", + "99e9b746-7809-41f4-a433-1de7cc90574b": "server/src/service", + "15ee5d81-a0cf-4614-80b8-7a1123ab20b8": "server/src/db", + "1a083b4a-787c-4eb4-8029-6700de987679": "client/src/Pages/v1", + "3bf0c0f9-cbb0-4f0c-8cb3-6e2a4abc0961": "client/src/Hooks/v1", + "16731e9d-3f73-4055-9d79-5ca9be679f68": "client/src/Utils/NetworkService.js", + "d3375a20-2776-4771-b806-649bbeb541bd": "server/src/routes/v1", + "fc591f7a-d4ac-4565-949d-a87a58a79bbc": "server/src/controllers/v1", + "bbbe53d6-f595-498a-ae45-e08611f39c3d": "server/src/service/v1", + "58c5a3fd-1e69-4ca4-a64e-314f5644bdaf": "server/src/db/v1", + "7090fcdb-09be-4c85-86fd-e73553bbb2ff": "client/src/Pages/v1/Uptime", + "595cf0e8-2f86-4543-b51c-b2aeac307f4b": "client/src/Hooks/v1/monitorHooks.js", + "10611636-ec73-4a26-87a5-654ae64a4843": "server/src/routes/v1/monitorRoute.js", + "341f2784-67ee-4477-9445-a8c240ee6261": "server/src/controllers/v1/monitorController.js", + "87d3b101-9443-4ed4-9c4f-f759d41677eb": "server/src/service/v1/business", + "1670815f-860d-4ea4-92e1-b238a799d3dc": "server/src/service/v1/infrastructure", + "82cb3dbe-c922-438a-a912-255376f4b380": "server/src/db/v1/modules", + "3dba5acc-cf25-40b7-ad21-9cd715454cd5": "client/src/Pages/v1/Uptime/Create", + "b4e479df-9b21-4430-9383-84831f503c2e": "server/src/service/v1/business/monitorService.js", + "dda70a36-2d1d-4e46-8bcc-52d0d474bf7b": "server/src/service/v1/infrastructure/SuperSimpleQueue", + "00406786-9663-440a-96b6-919d544732c0": "server/src/service/v1/infrastructure/networkService.js", + "fbd7d244-2f37-453c-8c83-a23a260b26cc": "server/src/service/v1/infrastructure/statusService.js", + "2ff1f030-2080-48da-bbbe-90302fdfc2f7": "server/src/service/v1/infrastructure/notificationService.js", + "630b3ece-2648-48d5-99b5-ef9611de2ad5": "server/src/db/v1/modules/monitorModule.js", + "391ad99e-3114-4f54-a6ca-fd89b33c7e06": "client/src/Pages/v1/Uptime/Create/index.jsx", + "d9c0127e-31aa-4509-b5c9-f7017d10d69a": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js", + "04c12e9a-e025-457c-a634-5f7472a42985": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js", + "7a2da928-a65c-49d8-b0a3-c159a6df2bc5": "client/src/Pages/v1/Uptime/Create/index.jsx-simstep-ab5de0b9-c70d-492c-98d9-8ddee66284ad", + "a81f4ae8-41fe-4d1b-95ce-86abcc67b53c": "client/src/Pages/v1/Uptime/Create/index.jsx-simstep-e02ddf57-270e-4f71-b4c4-d5183bb7879a", + "a30ac765-c546-4993-988e-8c8cfdb7532f": "client/src/Utils/NetworkService.js-simstep-14e9986b-c6ce-475e-97ed-da1fa2b6968d", + "f0409948-75dc-457f-a02d-6b9774258e48": "server/src/controllers/v1/monitorController.js-simstep-38fd069d-9c44-4078-954b-3a0899e9acf4", + "2d407bfd-e4b0-46b8-99e7-121c507d525f": "server/src/db/v1/modules/monitorModule.js-simstep-ca3ad309-323a-418d-a171-c16f56fae23c", + "51e8db5f-2fdd-4ddc-9605-43981db9d940": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js-simstep-2702bfca-7611-4562-8d3e-a05af8257871", + "18c13391-0cf7-4303-8180-0a40cfd64b58": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-e22a708a-a348-4775-8ee3-f32a35446e24", + "5c900373-e6ba-44d5-b341-676a32d659c8": "server/src/service/v1/infrastructure/networkService.js-simstep-84f82fea-e306-4e0b-8222-e9705803d51a", + "45e38f3a-a7c3-4de7-b3dc-589361d793fd": "server/src/service/v1/infrastructure/statusService.js-simstep-c66a04c6-113e-4391-ab71-93fc612279ef", + "9f3ea7fc-16d3-4d36-97eb-66f8808425cc": "server/src/service/v1/infrastructure/notificationService.js-simstep-1a260a75-cf50-4f7a-90b0-c280b512a5cb", + "0e09aa33-d7c7-4c89-9da8-f0937658f4d3": "generated-edge-simstep-888df086-41bb-469d-9c0a-daf74bdecb9f-0e09aa33-d7c7-4c89-9da8-f0937658f4d3", + "1f6f04a9-0123-4729-b06a-8f3da6d1b58c": "generated-edge-simstep-3fe0afaa-c673-4b52-a0c4-02a3ffd3a075-1f6f04a9-0123-4729-b06a-8f3da6d1b58c", + "b6d963d5-c613-407a-9ce9-60a04048dea7": "generated-edge-simstep-13dd0564-44a5-43ea-a163-ce1148cc48e7-b6d963d5-c613-407a-9ce9-60a04048dea7", + "a6bccd1d-4272-4df7-a7c8-b3979e37b8d7": "generated-edge-simstep-a6f4c967-b1a8-4804-bc2e-67f47a7b68f9-a6bccd1d-4272-4df7-a7c8-b3979e37b8d7", + "de320ac1-0949-4297-be7c-b9d7a14e0fec": "generated-edge-simstep-71ec03b6-1166-4c5d-9ada-0875a0866e30-de320ac1-0949-4297-be7c-b9d7a14e0fec", + "38ba9b22-5b26-432d-a856-0eceb792c737": "generated-edge-simstep-1d7580f2-f107-4008-a207-b8ec07998721-38ba9b22-5b26-432d-a856-0eceb792c737", + "5e80734c-7b72-41be-9068-30cda3ffb425": "generated-edge-simstep-9b0ada30-d3e6-4b2c-b3ff-f50615139588-5e80734c-7b72-41be-9068-30cda3ffb425", + "0a7e4a72-b644-4bd8-afa1-ae8a1d6c111c": "generated-edge-simstep-9896c387-4fca-47ad-82eb-7301286decb7-0a7e4a72-b644-4bd8-afa1-ae8a1d6c111c", + "3f26b33e-477f-41df-a78a-3e3e4f4209ae": "client/src/Pages/v1/Infrastructure", + "74019e8a-3148-4a32-aae2-5afd16f3eb9b": "client/src/Pages/v1/Infrastructure/Create", + "591128f9-0994-4e01-a1ab-f23357f97aff": "client/src/Pages/v1/Infrastructure/Details", + "4940977f-933d-4bbc-b19e-749647b44d25": "server/src/service/v1/infrastructure/notificationUtils.js", + "a0b22a5a-59e1-47e3-8218-a4ad083b1b71": "server/src/db/v1/modules/monitorModuleQueries.js", + "ceded1fc-87a9-43c8-a756-746be3d4ad8d": "client/src/Pages/v1/Infrastructure/Create/Components", + "e292c552-b3df-4bac-bd9a-8f44c260a5aa": "client/src/Pages/v1/Infrastructure/Details/index.jsx", + "e97d4e61-483e-4ac5-85a1-0ff72fcdd449": "client/src/Pages/v1/Infrastructure/Details/Components", + "83800be3-2a0e-4419-b379-3f995f4050e7": "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx", + "6a7c7720-a399-4d4d-8225-36e0b2f59731": "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes", + "8fe3d890-3605-407d-b06c-1d890a354e46": "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx", + "9651d084-5c16-4417-89c1-f52ae6572a95": "client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx-simstep-80dd889a-1fc9-4d56-b9cf-5b45bf1110b1", + "4c47b5f2-5363-4816-9745-30e61d184c7a": "server/src/service/v1/infrastructure/networkService.js-simstep-ed6e7faf-69dd-4d0b-bda8-137739ed288b", + "c1bf10c9-8c1a-44f4-8e23-39708f0301e8": "server/src/service/v1/infrastructure/statusService.js-simstep-e5b90cf2-9369-4741-9803-c835a81c009b", + "d4f48f1a-61de-4cde-87d2-4b5215464899": "server/src/service/v1/infrastructure/notificationUtils.js-simstep-07fe8489-1d94-4afb-83f8-7cebb0dff49a", + "801fe70a-1a19-406c-8f0d-770b0cb74409": "client/src/Pages/v1/Infrastructure/Details/index.jsx-simstep-c27365db-9db6-4cc9-96f0-4f046fd38562", + "9ef7c423-f4fc-4caf-9bf5-c1aa3002ca03": "server/src/db/v1/modules/monitorModule.js-simstep-8502ae1f-f029-446e-9dde-59c2aba87a8a", + "9ea2e32d-1ea8-42f2-9a17-613844723ae6": "client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx-simstep-6eedce82-b9c8-4022-95e5-3a6e00628105", + "1c5c9857-bf32-40e4-921b-957c252dac2a": "generated-edge-simstep-815f96af-c28e-4f80-b4ab-18a6fb23eee0-1c5c9857-bf32-40e4-921b-957c252dac2a", + "a64547c9-d9e6-4941-8e5e-f49add06516b": "generated-edge-simstep-4434dca1-2048-4458-a70b-b70d569e378d-a64547c9-d9e6-4941-8e5e-f49add06516b", + "7a250d09-50c6-4935-b10c-626b0ebe1fba": "generated-edge-simstep-b019a6d0-a23a-4a1d-bd21-644f01996f14-7a250d09-50c6-4935-b10c-626b0ebe1fba", + "3d357de8-9fd2-4d0b-9623-f36fce4cf1de": "generated-edge-simstep-c691b3b9-88b3-4c88-a399-ff9fd5866194-3d357de8-9fd2-4d0b-9623-f36fce4cf1de", + "00921831-b1d9-4a5a-ae6f-5ddee7cfc1e4": "generated-edge-simstep-a5d20172-1c95-4c9e-af39-f86a498734bb-00921831-b1d9-4a5a-ae6f-5ddee7cfc1e4", + "60308696-6f8a-43c8-b9a1-da199ef500cd": "generated-edge-simstep-627fcb0c-0282-4b4d-8c55-aceb636e4da0-60308696-6f8a-43c8-b9a1-da199ef500cd", + "feefae96-61b8-4ac5-a37c-592ed4b2330e": "server/src/service/v2", + "b3575a92-df07-4cf7-810f-8e5e0672d666": "client/src/Pages/v1/Notifications", + "cd2428fd-6545-48dd-b685-5261311b7532": "client/src/Hooks/v1/useNotifications.js", + "96250197-23af-4059-9904-fd6751f70ad5": "server/src/routes/v1/notificationRoute.js", + "8e5941e1-e577-455f-bbf2-83a418ad5908": "server/src/controllers/v1/notificationController.js", + "3f8ff792-40c8-4ae6-9af5-ff8b90da2f9f": "server/src/service/v2/infrastructure", + "a6fb23f0-73bc-40ee-bb3c-ed93605a2bcb": "client/src/Pages/v1/Notifications/create", + "62daeac4-9402-448a-972c-cb8cb760671c": "server/src/db/v1/modules/notificationModule.js", + "159e569d-31d8-4883-b443-4ec08b21a044": "server/src/service/v2/infrastructure/JobGenerator.ts", + "c6d9907f-0a05-4153-a27f-47c3a1c4c0aa": "server/src/service/v2/infrastructure/NotificationService.ts", + "b71b02fb-d08e-4fcf-b959-de1b9547d9c2": "client/src/Pages/v1/Notifications/create/index.jsx", + "15486629-84b3-463a-962b-06083f69e4bf": "client/src/Pages/v1/Notifications/create/index.jsx-simstep-e924ec15-1bea-4e3b-befb-a8bf30cbf740", + "a07b805e-e55f-46cb-b54b-765b7813b0ca": "client/src/Hooks/v1/useNotifications.js-simstep-383c6be8-aa51-4398-9ae3-21035413f73a", + "d0a4737f-715f-4f50-b6d5-5e4b31f86f93": "server/src/routes/v1/notificationRoute.js-simstep-2f9266c9-4cbc-4b50-a280-cf8b5800d540", + "4a796ccf-40a9-4446-a109-6a6eed52e898": "server/src/db/v1/modules/notificationModule.js-simstep-d29ab4ab-be50-4f17-af6e-2c61cec9d0ab", + "29c2c56e-9dc0-4304-bfbf-e0c0f8915a39": "server/src/routes/v1/notificationRoute.js-simstep-c1b17b64-90e9-48bc-8927-25d7ebfc3036", + "1cc01e08-4a6c-4526-b8a7-5ee8a06c1709": "server/src/service/v1/infrastructure/notificationService.js-simstep-f29f9be1-a0cd-43a3-b259-e5105a25ce41", + "28f1eabb-4f24-4944-8a0a-04f41ceb8589": "server/src/service/v2/infrastructure/JobGenerator.ts-simstep-157cec87-9b76-4a1c-9d85-0a6d3decbaa3", + "31fe5a28-0640-4b52-841b-dd02e2ae646e": "server/src/service/v2/infrastructure/JobGenerator.ts-simstep-55ad645b-118a-478e-a00c-09ef82357008", + "78e5cb9f-6a97-4d31-a487-fe7dbce5d7d1": "server/src/service/v2/infrastructure/NotificationService.ts-simstep-8d1176f3-c755-401d-9ca8-234689b4b837", + "54497b9a-4910-4df4-b016-480b9ac250a7": "generated-edge-simstep-71dbcd41-c6d4-4514-b5a4-6c966ca7ba6f-54497b9a-4910-4df4-b016-480b9ac250a7", + "dc135d8d-0515-403d-ae58-f3c04647b2ae": "generated-edge-simstep-162d8d2f-6cd1-45fa-8711-1ee85c5f86ef-dc135d8d-0515-403d-ae58-f3c04647b2ae", + "7b9ebb0e-a6b2-438c-bfed-9c851a1bccbc": "generated-edge-simstep-73e63d2f-2b43-488d-9e86-170f511673f5-7b9ebb0e-a6b2-438c-bfed-9c851a1bccbc", + "279155cd-ff31-41a2-8f5f-a538f01bfa66": "generated-edge-simstep-d390ef45-f2fe-4806-a45a-cc9c84b48a5a-279155cd-ff31-41a2-8f5f-a538f01bfa66", + "57018b30-e2f4-4eb2-a1da-fe75afbb8c2f": "generated-edge-simstep-6f2bf027-50d9-472e-9569-808a64fce7cf-57018b30-e2f4-4eb2-a1da-fe75afbb8c2f", + "50797935-1447-47a4-891b-9e886e5601fa": "generated-edge-simstep-b7ec7415-fcae-4077-8c19-302a015b00b5-50797935-1447-47a4-891b-9e886e5601fa", + "c778c6b9-c12e-45c0-8682-2c9e892867b0": "generated-edge-simstep-b7259d77-c8df-4060-ac03-58728ae3eb54-c778c6b9-c12e-45c0-8682-2c9e892867b0", + "987f145b-5df6-4c30-bf1c-2a2d2b0e5cdc": "client/src/Routes", + "c4d7e7dd-75b1-44f1-a34a-6ed290a8e562": "client/src/Routes/index.jsx", + "9d662180-8b65-4426-ad37-cfed4bc01fb4": "client/src/Pages/v1/StatusPage", + "773e1065-b0a5-46a9-b0cc-6b963f69f5cc": "server/src/routes/v1/statusPageRoute.js", + "17a4f435-5a8e-4433-84a6-9ac17362709a": "server/src/controllers/v1/statusPageController.js", + "08602a28-f073-484e-9403-7eb798126dd4": "client/src/Pages/v1/StatusPage/Create", + "8ef70566-37d9-48fb-b5e8-37e5b68768a2": "client/src/Pages/v1/StatusPage/Status", + "f8541fe3-4bcf-4833-a226-66b5301adf58": "server/src/db/v1/modules/statusPageModule.js", + "e2d2bbf6-dffd-4edf-8c5d-34088d9d4745": "client/src/Pages/v1/StatusPage/Create/index.jsx", + "56f6775e-9843-4ca2-bc8c-1c263b6c35e9": "client/src/Pages/v1/StatusPage/Create/Hooks", + "e1e6f5e0-fd8c-4260-aa4f-cdb096c6b1c3": "client/src/Pages/v1/StatusPage/Status/index.jsx", + "79da0721-d810-4f7f-9cf9-3ea462bf0bff": "client/src/Pages/v1/StatusPage/Status/Hooks", + "3945e05f-2d99-4d6d-9a3e-b6c75ea3c45a": "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx", + "5b2cdab8-66f1-40a1-9621-0efaf54e04ae": "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx", + "00289d11-0015-4b2a-9c5b-dc9fd0f32b80": "client/src/Pages/v1/StatusPage/Create/index.jsx-simstep-de7b5d1f-b370-42a4-a410-4178f1e2d3a6", + "aa5d6d07-05f0-45f3-b19a-199af50e35f1": "client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx-simstep-3c99ae6a-b671-4f47-bed0-ddccd8ff5d35", + "3efddcad-588f-4492-9808-b0c81c823c96": "server/src/routes/v1/statusPageRoute.js-simstep-84f11567-b3c4-42a2-aa7f-d78c1412099a", + "f15ef77e-380f-4768-98cb-7f0dd4113841": "server/src/controllers/v1/statusPageController.js-simstep-01694730-8a75-4949-8815-bb4d6e3e2b42", + "e234917e-a950-48f5-b864-cd957b2216e1": "server/src/db/v1/modules/statusPageModule.js-simstep-b6fa93cd-6fca-4d9e-969a-5400e2437ee8", + "058ded1f-83ce-40a7-a99d-d178c81fe557": "server/src/controllers/v1/statusPageController.js-simstep-2a115421-5175-4c8f-8cdd-62f3fe148af5", + "30aaee38-f509-4e19-a917-e6d2f4bba61c": "client/src/Routes/index.jsx-simstep-132354d7-df7a-4e03-9d00-16d54f836a1b", + "ecfb2d04-2681-4f16-af75-632f69e1a868": "client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx-simstep-b57bb249-9066-454e-aa8c-89108fdfcc2e", + "2e26cd2f-1413-4c16-8e63-ea7808ab6c52": "server/src/routes/v1/statusPageRoute.js-simstep-477c6821-b29b-4b38-8930-ced21b8a75e5", + "9f217674-4cd7-4ef8-b088-d5620042a7ba": "server/src/controllers/v1/statusPageController.js-simstep-5bdbd179-ce8f-4171-a073-af72a99b4468", + "26da60f9-dde4-44ec-be89-91858cc14f90": "server/src/db/v1/modules/statusPageModule.js-simstep-ceb044e9-6545-4d04-bf79-ec8146eb8ec9", + "d76e70e0-41f9-46cc-b775-b2027c5a0d5b": "server/src/controllers/v1/statusPageController.js-simstep-83224406-2f1d-4030-9beb-d65c2baf544d", + "f59b91de-5196-4a43-9d57-3f28402c0672": "client/src/Pages/v1/StatusPage/Status/index.jsx-simstep-3d51e811-a659-442e-97e0-00798cd15647", + "de23eee2-b60a-4236-a890-3c5b78388bb3": "generated-edge-simstep-d419f7b6-7b9b-4494-9fad-83e3ce208ca4-de23eee2-b60a-4236-a890-3c5b78388bb3", + "d7faa713-4204-4a97-a172-d621da36c287": "generated-edge-simstep-8a2ab372-73db-4b9f-bc0e-6a20f15a4d79-d7faa713-4204-4a97-a172-d621da36c287", + "a3f28fa6-8268-4c13-95a0-83e3b1abdcb0": "generated-edge-simstep-0923a533-c572-4da3-ad87-cdfff111f3bb-a3f28fa6-8268-4c13-95a0-83e3b1abdcb0", + "9735611c-924b-49aa-92e8-10d8a86ac288": "generated-edge-simstep-67603f03-c088-4ed0-9f18-072435134cdd-9735611c-924b-49aa-92e8-10d8a86ac288", + "40cb7d7b-2888-44de-bb7d-7735af70ba34": "generated-edge-simstep-ea4b3532-baae-4890-a73e-2ca385d0319e-40cb7d7b-2888-44de-bb7d-7735af70ba34", + "c3f83811-8674-4dbb-9f84-79499f97fb1f": "generated-edge-simstep-210acc07-81c3-4394-b566-41f6031e8eeb-c3f83811-8674-4dbb-9f84-79499f97fb1f", + "0575bf87-5362-4988-b895-7e514148d420": "generated-edge-simstep-2ba49e28-1cac-47f5-9118-ab930788736a-0575bf87-5362-4988-b895-7e514148d420", + "ae1c9341-0e39-4d91-a9ac-5cc8a627c247": "generated-edge-simstep-de0714d2-bf1e-4a1a-a389-60793de8b772-ae1c9341-0e39-4d91-a9ac-5cc8a627c247", + "5735f30c-b55f-444a-9236-f3d75f68ed27": "generated-edge-simstep-b79b8922-4de6-477d-8648-37b4bd9a6767-5735f30c-b55f-444a-9236-f3d75f68ed27", + "dace45d6-fb25-4dae-b940-49ef7d77bbed": "generated-edge-simstep-788307ef-4cfa-4f5a-b2e9-a0bfc7a378d8-dace45d6-fb25-4dae-b940-49ef7d77bbed", + "137a5ab7-1e49-42ec-869d-58f7f2380cb3": "generated-edge-simstep-4d3fad92-8fa9-4a8f-be3b-f09eda2f8dd4-137a5ab7-1e49-42ec-869d-58f7f2380cb3", + "04a33e4f-1dfe-4c5a-8b30-01ed0591c380": "generated-edge-simstep-d5f3c300-9777-4e09-bb05-f1281c2ff29a-04a33e4f-1dfe-4c5a-8b30-01ed0591c380", + "933a6cdc-349c-4725-b7ae-14cf780bfde8": "client/src/Hooks/v1/checkHooks.js", + "d18dbe1d-2f2c-4336-bd21-47c1670f7bab": "server/src/routes/v1/checkRoute.js", + "d983e54f-2fbe-496a-932d-a827e1124804": "server/src/controllers/v1/checkController.js", + "273f3009-69d0-46fe-af57-3cd94a24b55b": "client/src/Pages/v1/Uptime/Details", + "f90db867-e1e7-4554-b446-fa78fc6a8578": "server/src/service/v1/business/checkService.js", + "860b5eec-a1ce-4e1f-a316-e1586f03af96": "server/src/db/v1/modules/checkModule.js", + "1807d5b9-54be-430b-8997-cada4dd9b521": "client/src/Pages/v1/Uptime/Details/index.jsx", + "6aed78bb-62f1-4ac1-9b9a-9d5977e87404": "client/src/Pages/v1/Uptime/Details/Components", + "5ac0878f-d238-4763-8464-a85b4885a40f": "client/src/Pages/v1/Uptime/Details/Components/ResponseTable", + "d3f74ac7-3b9a-4cae-8d77-a0d5d605bb27": "client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx", + "78a8a7c5-41d3-4453-a7f7-ac632bbef288": "client/src/Pages/v1/Uptime/Details/index.jsx-simstep-35195328-9847-4678-b014-9331553f0d19", + "4a5847d0-91a0-4b4c-8517-2f4ff9a7e8d4": "client/src/Hooks/v1/checkHooks.js-simstep-8dc4347c-3e9e-40d7-ae6f-54684f2e6956", + "cdd090be-0b62-45b4-baa0-fec01fc3ffd1": "server/src/routes/v1/checkRoute.js-simstep-c5fd192f-ea28-48a0-b3e7-5bc04f521192", + "499b46a2-f2a2-4ccd-aecd-012eade2bd23": "server/src/service/v1/business/checkService.js-simstep-83fea05d-8306-4a73-a43f-882cdd14f50f", + "14df9f01-1358-442d-96f5-eab9a4c4abbf": "server/src/controllers/v1/checkController.js-simstep-5768fdc5-045c-48ef-b0c9-79769403648f", + "ceb9cda3-968d-44aa-b223-49902adb93b4": "client/src/Hooks/v1/checkHooks.js-simstep-faba07f3-7460-47ae-924d-f92f5b2e821d", + "c996f7ca-9384-4252-b343-7bfc049d14bb": "client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx-simstep-2b1651cd-2e70-4cc6-b2bb-23f94fa73d66", + "e90e4943-6108-46e3-8a2c-a9a1c7361279": "generated-edge-simstep-49979250-1a32-4da5-95e0-58652fe218f9-e90e4943-6108-46e3-8a2c-a9a1c7361279", + "2b8ae59e-ed8d-4837-9a9b-64eb480f982c": "generated-edge-simstep-336b534d-8d6a-4639-af8e-889b38f51ab1-2b8ae59e-ed8d-4837-9a9b-64eb480f982c", + "839ba9f6-8a34-4919-a04a-13fba1795c61": "generated-edge-simstep-4f73f0fe-b390-4fe1-a354-eb0857835126-839ba9f6-8a34-4919-a04a-13fba1795c61", + "42d3bc70-701a-4be0-96b4-d6c2f0519b49": "generated-edge-simstep-65da1a9e-041c-4656-9c5b-0a92b761a9be-42d3bc70-701a-4be0-96b4-d6c2f0519b49", + "6c5b028c-356c-43c8-a35a-b47448cd8695": "generated-edge-simstep-2c1bdec8-d0e6-4163-a99c-f27899b987c5-6c5b028c-356c-43c8-a35a-b47448cd8695", + "74b0dd89-9fea-4219-af1f-9571316a9d5a": "generated-edge-simstep-9405dce1-0565-4dfa-9539-a048472acda9-74b0dd89-9fea-4219-af1f-9571316a9d5a", + "e2f99143-54b7-44dc-b566-1c2fb901bd16": "server/src/db/v2", + "043d3dac-d4bb-449b-a3a7-270d53b66dac": "client/src/Pages/v1/PageSpeed", + "7ef79284-534a-466f-a1f1-4e3a2f56be9d": "server/src/service/v2/business", + "b53384d7-b989-4ac7-bd68-1c3621174ef4": "server/src/db/v2/models", + "414c500b-7b68-42ba-8110-e855efbee750": "client/src/Pages/v1/PageSpeed/Create", + "581b4919-8b0c-4f0d-ae67-ad6de11d8c8b": "client/src/Pages/v1/PageSpeed/Details", + "50aeed0b-bc13-43f7-bb15-cb1b12b2caa7": "server/src/service/v2/infrastructure/NetworkService.ts", + "f0ebd5da-21bb-4d96-8ff9-ca10409acc80": "server/src/service/v2/business/CheckService.ts", + "7a3824cd-24c8-4b32-8b1d-e0bf357262c4": "server/src/service/v2/business/MonitorService.ts", + "64e3bb0a-67f5-4269-9e7d-94fe91ac78fc": "server/src/db/v2/models/checks", + "5ffe0baa-054a-4e5b-8b29-1254f07ea975": "client/src/Pages/v1/PageSpeed/Create/index.jsx", + "c4a7bb58-1d9c-4690-b6b8-460c1cc33702": "client/src/Pages/v1/PageSpeed/Details/index.jsx", + "1425ec43-f0f4-4c8b-a140-176c079219d6": "server/src/db/v2/models/checks/Check.ts", + "0622b212-3a4a-484d-9b7a-125864d785cd": "client/src/Pages/v1/PageSpeed/Create/index.jsx-simstep-b0cc6e6a-961e-4ea9-aa8a-6bfa44d6ba12", + "6527fb47-157e-4bba-9aed-adde061c5689": "server/src/service/v2/infrastructure/NetworkService.ts-simstep-53968267-0c6e-42f4-88b3-cd90f44532ae", + "f89cbd87-c961-4f8b-92b6-714da57718d5": "server/src/service/v2/business/CheckService.ts-simstep-01206383-2db8-4b18-84d0-be2a0289c196", + "ca6ee27b-1fbf-49de-978a-d6de259cc3d5": "server/src/service/v2/business/MonitorService.ts-simstep-5616ac6d-c281-4879-82f1-6590c721c8f7", + "9ef9e218-4742-487a-9a88-5c70e788ccdc": "client/src/Pages/v1/PageSpeed/Details/index.jsx-simstep-0ec0eb06-05eb-4204-b077-9e47eb2cc316", + "b8e83b0a-69ff-4c34-a7fe-cb8c9ba85438": "generated-edge-simstep-f8df1cad-6051-466e-a13e-47d710ba1c14-b8e83b0a-69ff-4c34-a7fe-cb8c9ba85438", + "0d3c8091-3d91-4e42-9b46-7e35ed726a7a": "generated-edge-simstep-0b380571-3896-4365-b5dc-187494f02e0d-0d3c8091-3d91-4e42-9b46-7e35ed726a7a", + "27c7191b-80a2-475b-9371-e106108aff1c": "generated-edge-simstep-a629a102-0df1-4798-a7a2-45c8f6cdad29-27c7191b-80a2-475b-9371-e106108aff1c", + "7be432cd-2587-4eab-abdc-f7b9c1923597": "generated-edge-simstep-4ed43eaf-fa8f-4fce-af87-8a5f74dd5c08-7be432cd-2587-4eab-abdc-f7b9c1923597", + "fe47576c-c186-4950-82a5-b2fd24eb2561": "server/src/config", + "cb53d612-9b89-47da-911c-165d0382a7d8": "server/src/config/routes.js", + "8021b55d-2d3a-4416-b598-e134e76c9434": "client/src/Pages/v1/Maintenance", + "5f21f109-1967-416f-a649-9b48e78b90f6": "server/src/routes/v1/maintenanceWindowRoute.js", + "a0cae18c-3c0e-40d3-ab03-eef7eace65d6": "server/src/controllers/v1/maintenanceWindowController.js", + "6f7119ac-d65a-4fdf-a6f2-31052e0f5549": "client/src/Pages/v1/Maintenance/CreateMaintenance", + "1eae6263-aaa2-4206-b92e-1e67f529c1c9": "client/src/Pages/v1/Maintenance/index.jsx", + "5ad4d33f-0932-479c-bb57-22ce6405758f": "client/src/Pages/v1/Maintenance/MaintenanceTable", + "26b8daff-aa48-4baa-9d6c-05f771bf0990": "server/src/service/v1/business/maintenanceWindowService.js", + "95ed6c41-ebeb-4c08-98c3-bab8b426789e": "server/src/db/v1/modules/maintenanceWindowModule.js", + "92c244d2-e6a7-4804-a643-1e8d36baf9c6": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx", + "015a17fc-3fcf-45e0-8936-19f5e2ca86a3": "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks", + "e7ec5ca9-6d25-4bf2-9695-639556f7d141": "client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx", + "6ad0396e-9b38-4f5d-9ad9-6c9fcb4434f4": "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx", + "20335f29-a2ac-4e0c-aba6-28707748ecd4": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx-simstep-2401ef7a-8d8e-4056-868a-547ab213daf1", + "5bdfdfa4-5d93-49fa-b05c-905813e6cf9d": "client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx-simstep-5e0d7085-099b-478a-afaa-47ad4dd07043", + "a391e8ea-9dbe-4fe4-90df-bf475d8b7ef8": "server/src/config/routes.js-simstep-3e93ef65-8e2c-405b-8dc1-56d8e93937b3", + "0fae6e75-38ea-444d-a1bf-0f820ba443b9": "server/src/controllers/v1/maintenanceWindowController.js-simstep-95fc1919-339d-4659-98cb-3dc50f662075", + "560d268b-ef0f-4cd2-bcfe-9dc0a0e2ce6f": "server/src/service/v1/business/maintenanceWindowService.js-simstep-6e2cf739-1514-4c37-9874-10b68a47010e", + "3890b98d-a133-4279-a961-5d2facc6a17a": "server/src/db/v1/modules/maintenanceWindowModule.js-simstep-a3c47fd4-c701-46c2-9aff-94a667b3e368", + "a5a84b08-8c70-4023-8d08-b74e23aab4d7": "client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx-simstep-ebe8fd52-f2a1-4a98-a71b-b04d6f4cdadc", + "d145bb4c-e1e0-473a-b6bc-decc932b738b": "client/src/Pages/v1/Maintenance/index.jsx-simstep-82b5ea14-bb00-4511-aba4-76196029fd81", + "c35a065c-2808-402c-877e-9055d956ce0d": "server/src/controllers/v1/maintenanceWindowController.js-simstep-1251da83-5949-4e24-a1d2-fc74ff785aa9", + "27d9160a-4f8d-4dff-a09b-1678bc28cf2c": "server/src/db/v1/modules/maintenanceWindowModule.js-simstep-9bfbae5a-f487-40ef-86f5-04a1cc3c7662", + "970e2002-7c0e-40ef-80d8-e5e6d0a60735": "client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx-simstep-acf97639-e291-4ca6-81a6-d0487397f42a", + "23630a1f-7452-4373-a864-7ec7821137f2": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-a47affaf-b952-46d6-9709-6120a77690e1", + "fc78b5b6-7fad-4f8f-a5a3-c7c38931daba": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-2127e749-80b4-499a-ae14-8dc2dcc9db9b", + "0fe32416-db43-49b9-9438-3796ba1a0add": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-6ba6b9d1-5fc3-454e-8744-2022b5192ed9", + "01e042c3-40b4-416a-8d9e-6d6fd323d60b": "server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js-simstep-e59fe64e-f06b-4855-b631-923bb5a68540", + "d3bdc6ff-875e-472e-88c8-eb10b20c526e": "generated-edge-simstep-f881435b-3bc2-453d-b6bf-56ad3a12f120-d3bdc6ff-875e-472e-88c8-eb10b20c526e", + "fd5e4447-123b-4b2d-8bce-1d1ae7cb00af": "generated-edge-simstep-0660b48b-139f-4ecf-b7d9-f12f544e0ab5-fd5e4447-123b-4b2d-8bce-1d1ae7cb00af", + "e2588541-3ffc-47c6-8f8a-41d30ea7fc72": "generated-edge-simstep-e4cc5ce8-6da9-4281-9204-29c061ab13ce-e2588541-3ffc-47c6-8f8a-41d30ea7fc72", + "e1d59cf6-0d96-4e0a-ad17-2a7984a9157f": "generated-edge-simstep-c3d1a95c-e93d-499a-8830-a6d870902414-e1d59cf6-0d96-4e0a-ad17-2a7984a9157f", + "12d0ab8a-392f-48fa-9efa-06ffe1f3a252": "generated-edge-simstep-80aed28e-68b6-47bf-8bc9-ededb4824066-12d0ab8a-392f-48fa-9efa-06ffe1f3a252", + "fcf4eef4-394c-4872-9124-f7a0b32cdc3b": "generated-edge-simstep-69fdc2a9-837f-466c-9562-a12319019580-fcf4eef4-394c-4872-9124-f7a0b32cdc3b", + "7f921125-500f-4902-abc3-936ecac033d8": "generated-edge-simstep-7e20cd6c-d24e-4540-86d6-d6a9fa087314-7f921125-500f-4902-abc3-936ecac033d8", + "c3127c74-43f1-42d7-8da6-b0a494c3658f": "generated-edge-simstep-feb8bd4f-339f-4132-a709-89c63c4efa9c-c3127c74-43f1-42d7-8da6-b0a494c3658f", + "6c9979cd-1cc1-4227-b535-2ec3a10ca27e": "generated-edge-simstep-eda86dca-dea6-4671-a079-7d212f4f18c4-6c9979cd-1cc1-4227-b535-2ec3a10ca27e", + "c99291bb-7316-440e-9a7f-323ee39e26b8": "generated-edge-simstep-87583096-f1ec-4160-ae52-5238f32ed0ab-c99291bb-7316-440e-9a7f-323ee39e26b8", + "8a13e585-2310-420b-9e92-211b6d65f201": "generated-edge-simstep-e6542e14-556d-482a-8c35-f7b03997be49-8a13e585-2310-420b-9e92-211b6d65f201", + "40f4d9df-4c0f-4384-901c-8d7117e3b538": "generated-edge-simstep-a715d98f-60bf-4f32-aae6-21ee4e09f4da-40f4d9df-4c0f-4384-901c-8d7117e3b538", + "a968db5d-8f6d-4fd7-9df0-8b332b521703": "server/src/middleware", + "09082f73-e215-465f-9206-0a963fb4c7cb": "server/src/middleware/v1", + "3cad7666-0d1a-4e31-b6ca-70d9c3a46b19": "client/src/Pages/v1/Account", + "9edafb8a-3661-4a7e-8510-a47f3cbc7564": "server/src/routes/v1/inviteRoute.js", + "001fa2e4-eadd-4474-b2ec-79d7ea43465f": "server/src/middleware/v1/isAllowed.js", + "6a186805-9d50-4bc3-9d56-e627a7ec2eb8": "server/src/controllers/v1/inviteController.js", + "33960806-1b9e-4d61-a163-e4bebf82d87a": "client/src/Pages/v1/Account/index.jsx", + "425a4c15-043f-4d05-8832-da00e18f97b9": "client/src/Pages/v1/Account/components", + "570278b7-9b72-4299-9fd9-46b253b48e9b": "server/src/service/v1/business/inviteService.js", + "08853f8a-f596-4df0-9190-dad04f54caef": "server/src/db/v1/modules/inviteModule.js", + "1145f351-2257-411d-86f7-30bf8c26ad68": "client/src/Pages/v1/Account/components/TeamPanel.jsx", + "3de2d413-5e9b-49f4-9409-74be32c52e0f": "client/src/Pages/v1/Account/index.jsx-simstep-2d5a952a-ce62-4948-9309-96b28ad74e59", + "b27f1bba-c655-40c6-a1dc-71e78c590c92": "client/src/Pages/v1/Account/components/TeamPanel.jsx-simstep-eebaca25-d588-49c4-a35c-9abc82fde580", + "8b3cb6c2-ed57-4510-be15-decb8e3c89e1": "server/src/middleware/v1/isAllowed.js-simstep-50dd7316-7d01-43f7-9036-d97d51a9c178", + "ae858158-8db6-4917-8fda-0966a953f6ed": "server/src/service/v1/business/inviteService.js-simstep-55fe9963-30d2-47f8-859f-b485a77064c4", + "a7be2d0b-0227-4228-a0a6-074f7324fd89": "server/src/service/v1/business/inviteService.js-simstep-7d1f86f2-c8d5-48fb-a8c1-b337dc56271f", + "02d87257-883c-4822-a421-823c983532ec": "client/src/Pages/v1/Account/components/TeamPanel.jsx-simstep-700f35e0-b39d-417c-b0a8-0612feade977", + "c441312f-f3c6-48c6-b1b9-7c12749d91bb": "generated-edge-simstep-10f80ada-2d0a-4e98-bd5d-057e7d4fe6de-c441312f-f3c6-48c6-b1b9-7c12749d91bb", + "3530244f-2689-444e-bcbf-611db2e3c000": "generated-edge-simstep-16d87608-4458-430c-b459-cd9b1d413fa6-3530244f-2689-444e-bcbf-611db2e3c000", + "2ee1c4e8-90fb-4470-ad9f-6ffed5420853": "generated-edge-simstep-6c024d4c-178b-45fc-8bca-4fc6c00549a4-2ee1c4e8-90fb-4470-ad9f-6ffed5420853", + "bdd52e6f-ddf7-4966-b3af-957f9553082f": "generated-edge-simstep-c796f8ad-d548-45f3-8d62-67f59c6c1aa4-bdd52e6f-ddf7-4966-b3af-957f9553082f", + "52732a37-82b8-40a7-b12d-dc1b82380b2d": "generated-edge-simstep-6ba5094d-d1e6-4ec3-8917-28cdb30f2ef9-52732a37-82b8-40a7-b12d-dc1b82380b2d", + "56dae13e-5735-4089-a9a4-e18be315aff7": "client/src/Pages/v1/Uptime/BulkImport", + "29b6ad3c-9538-47bc-8796-aab942dc4b71": "client/src/Pages/v1/Uptime/BulkImport/index.jsx", + "cd84476a-1c8b-4781-811f-58fb9944365b": "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx", + "c4caeba8-ed9a-4cbc-9888-38cc8ca55362": "client/src/Routes/index.jsx-simstep-a29da01e-8c5a-4e6d-8d3c-e49af528e4fa", + "6f1948fa-e84a-4f8e-8f3b-8f24c511b3d8": "client/src/Pages/v1/Uptime/BulkImport/Upload.jsx-simstep-bbd04d55-fc6a-4ba8-bcde-5a4dc263d6eb", + "dd2f9356-fad3-47d4-81d5-a5e038298e2b": "client/src/Pages/v1/Uptime/BulkImport/index.jsx-simstep-0139fcaa-eb46-4176-b7bd-1cb15e7f1f4c", + "5967e576-f542-4c0b-a3f4-9446ae992721": "client/src/Utils/NetworkService.js-simstep-2fc087b3-033d-4cf5-81f2-e35b99a14bce", + "c1ac3c26-996a-43ef-9c34-4c2776188784": "server/src/routes/v1/monitorRoute.js-simstep-07bf3d83-047d-4e51-9f96-8c9c64848500", + "0e520a6d-34bf-467b-a894-e73979364478": "server/src/controllers/v1/monitorController.js-simstep-a3799570-64bf-481e-9ffe-31dbad8a36e4", + "6139e487-1dd5-405a-ba7d-ededec4b282e": "server/src/service/v1/business/monitorService.js-simstep-2c7c3082-e4b6-4525-860f-2ffcd9e86db1", + "d9d519af-5c4d-4494-929e-a869581c1dc4": "server/src/db/v1/modules/monitorModule.js-simstep-70f18c21-2918-402e-9c35-1bdce8bc82d4", + "1e1d975f-7f0f-4132-bd71-2563b35e99b3": "server/src/service/v1/business/monitorService.js-simstep-66fbf913-e662-4bd9-ada6-077be9e37f07", + "72b55fa1-d944-4e46-ac98-f6a6cb201afa": "server/src/controllers/v1/monitorController.js-simstep-f5377571-156f-4c5c-8bab-b6dacdf0fea6", + "ae3b7e23-ed64-40aa-9760-389f0a869f8d": "client/src/Pages/v1/Uptime/BulkImport/index.jsx-simstep-72b3f16d-290d-4a61-8748-2ad90d63ec68", + "26d76516-fd7e-4ae8-956a-d077de4257fa": "server/src/routes/v1/monitorRoute.js-simstep-63d8d81f-f436-4594-a6eb-8c0fd2110bc1", + "a3106388-6a3d-4b68-a3fc-3761a0d54f98": "server/src/controllers/v1/monitorController.js-simstep-d9db3f40-8add-4551-9323-274348b9800f", + "ab9cb862-0ac7-45a3-9888-f64e87bdda54": "server/src/service/v1/business/monitorService.js-simstep-85cd1d5d-7ffb-49c5-aacb-907048003fc5", + "f151f89d-6399-48f0-aecd-5eb1572b48cc": "server/src/controllers/v1/monitorController.js-simstep-256095b8-5e3a-4c20-abd9-be9e6a9d0bad", + "d8877474-7203-484f-bd47-fc69e3c7c482": "server/src/controllers/v1/monitorController.js-simstep-60abdb27-4078-4678-a010-0f6938d4be48", + "c9a012bc-d53f-494f-82f2-1ee3f40ce646": "generated-edge-simstep-ef0bbcff-9535-4c0e-a5ca-96c457f89139-c9a012bc-d53f-494f-82f2-1ee3f40ce646", + "e03a8ce2-c2cd-4147-8bab-63d6c2cf0a48": "generated-edge-simstep-610120d2-0521-4444-b245-feb97239a389-e03a8ce2-c2cd-4147-8bab-63d6c2cf0a48", + "b8c0292d-afbb-4648-b484-1c27be96d52f": "generated-edge-simstep-a365d018-e80c-419c-80b3-5ffc8f14dc5c-b8c0292d-afbb-4648-b484-1c27be96d52f", + "23715cce-c83a-44c0-9853-4c95b85e5ecd": "generated-edge-simstep-e6dec1dc-33cb-4669-a3b0-7a3c0b233185-23715cce-c83a-44c0-9853-4c95b85e5ecd", + "20d0e690-a569-4c6f-8238-916cac920553": "generated-edge-simstep-c53d3f62-73fe-4fad-9835-ef2c8cf52894-20d0e690-a569-4c6f-8238-916cac920553", + "7292bf6e-431d-4a9d-94ce-2bcfabcaa79d": "generated-edge-simstep-6038422c-ab83-4fc6-a58b-c75734276118-7292bf6e-431d-4a9d-94ce-2bcfabcaa79d", + "b55a0dc6-f596-4e5f-9265-63588b46b519": "generated-edge-simstep-0115fb69-4959-4b32-bd90-1dcd3c2231f6-b55a0dc6-f596-4e5f-9265-63588b46b519", + "f27c60ef-fb40-43e2-9750-c2617bdeaf43": "generated-edge-simstep-2ee7bd94-d131-4dd8-bdf8-68433d00472d-f27c60ef-fb40-43e2-9750-c2617bdeaf43", + "c449f81a-d6af-4f7a-b2d8-050a9a9ee0f9": "generated-edge-simstep-94d5772d-3f46-4d43-b764-3493db5c4971-c449f81a-d6af-4f7a-b2d8-050a9a9ee0f9", + "926a330c-a9c5-4983-a4b5-70359813e49a": "generated-edge-simstep-c60a3adb-dd81-41b0-95e4-a0a6d23f9dd0-926a330c-a9c5-4983-a4b5-70359813e49a", + "febea87e-5033-479c-ba7a-eb35d98db899": "generated-edge-simstep-53cebb8e-cf45-49eb-a7d3-98fd87af21b4-febea87e-5033-479c-ba7a-eb35d98db899", + "3e0f0966-5c2f-41af-9bcc-e8f615e03a00": "generated-edge-simstep-74e95fb5-863c-4137-a41b-d7a32efa04a5-3e0f0966-5c2f-41af-9bcc-e8f615e03a00", + "ad300b1c-6748-40f9-a7c9-5895e0915991": "generated-edge-simstep-4deb5e47-2ac4-4638-9ee6-48295d027bcd-ad300b1c-6748-40f9-a7c9-5895e0915991", + "3dfb432a-2ce4-4f92-98ce-5d114b5f7f03": "generated-edge-simstep-605f4cbd-1f7f-4e89-b028-f18737ac7fb1-3dfb432a-2ce4-4f92-98ce-5d114b5f7f03", + "50f77798-f41b-4f5b-b096-7afc921a0419": "client/src/Pages/v1/Settings", + "1716ec41-8b42-4386-9850-55b8a607638e": "client/src/Pages/v1/Logs", + "8ee5d524-e61d-4e39-a729-babf54929def": "client/src/Hooks/v1/settingsHooks.js", + "a1fea5b0-fb7c-4189-9484-9d3312660767": "server/src/controllers/v1/settingsController.js", + "5882419e-8a92-4edd-a71a-1252983f9af7": "server/src/controllers/v1/diagnosticController.js", + "d4b34195-7e1e-47a0-9303-603987c81f95": "client/src/Pages/v1/Settings/index.jsx", + "d8aa5b9e-f2a7-44c8-8813-d79c3f3238ee": "client/src/Pages/v1/Settings/SettingsEmail.jsx", + "e0cb598c-e300-4fd3-8554-db7ffd6201f1": "client/src/Pages/v1/Logs/Diagnostics", + "643b26c1-3bfb-4c41-8179-96dfc237d99e": "server/src/service/v1/business/diagnosticService.js", + "87e0e2ea-4a6d-460d-95bc-31e2efd9c9cf": "client/src/Pages/v1/Logs/Diagnostics/index.jsx", + "97908daf-d071-4df5-8366-1f5f4d8d534a": "client/src/Pages/v1/Settings/index.jsx-simstep-64e9f1af-13b7-4361-a2a9-3f45760e316d", + "83596bd1-dfca-4196-b52e-955f6824fafb": "server/src/controllers/v1/settingsController.js-simstep-441b0c45-5f27-478d-8ca8-58fbd5f460ea", + "09cb194a-0ea4-46e2-b32b-36b62fa87f40": "client/src/Pages/v1/Settings/SettingsEmail.jsx-simstep-ae5c5146-4dbb-4612-9d23-086e57f72474", + "1b2cf182-9ee3-4218-a7ec-b74e427ccddf": "client/src/Hooks/v1/settingsHooks.js-simstep-d7098072-9a83-4369-8176-9646ba34e186", + "cce3b9c8-f3b7-434b-b3c6-d8d164714f86": "server/src/controllers/v1/settingsController.js-simstep-7632e8d2-4086-4169-ae3f-bfbf8d657987", + "fff5dde4-8eb8-4c21-9c6d-0845a9749ff3": "client/src/Hooks/v1/settingsHooks.js-simstep-a157bd07-b969-44f8-914c-3d8393c1605a", + "704e2ec8-9f2f-47e5-852f-c6ada67a02e0": "client/src/Pages/v1/Logs/Diagnostics/index.jsx-simstep-a9b24b5f-534b-4baf-a185-bac7c32a1c42", + "09f07e80-185e-4547-aab4-edaf8ae7501e": "server/src/service/v1/business/diagnosticService.js-simstep-8fb704ca-67d9-437c-90d8-01302634d568", + "4b418736-6b72-47e0-917a-d86d4bed9ecd": "client/src/Pages/v1/Logs/Diagnostics/index.jsx-simstep-e6c94f52-a599-4568-a06f-2db18f963817", + "f6d80f09-ae83-4c95-aba0-4efffeda38e0": "generated-edge-simstep-dca4a33c-b666-47b8-896b-370ed9d6f9c5-f6d80f09-ae83-4c95-aba0-4efffeda38e0", + "195ad532-9858-493e-876a-6ec457811d1d": "generated-edge-simstep-aacc9302-6ca8-45c6-acb8-956d8d0242c2-195ad532-9858-493e-876a-6ec457811d1d", + "8c107d79-f796-49eb-ab88-f553dd6f9d95": "generated-edge-simstep-e7e82a97-1409-4bc5-ae27-b4ff5ba746df-8c107d79-f796-49eb-ab88-f553dd6f9d95", + "ce3ed3ba-a35b-4881-8dfd-f8425b9bf812": "generated-edge-simstep-383387e5-4623-44ab-b2c4-d9ea046f4547-ce3ed3ba-a35b-4881-8dfd-f8425b9bf812", + "fbe9b160-307e-4f51-8f72-713fda5373c8": "generated-edge-simstep-0afcde35-7d51-4939-aae8-bac5ac83c49d-fbe9b160-307e-4f51-8f72-713fda5373c8", + "499dfe1f-77d4-4b26-a12c-45240691109e": "generated-edge-simstep-23b8ad7d-8967-4e48-814c-1a7bf5b4dfac-499dfe1f-77d4-4b26-a12c-45240691109e", + "d0f92604-a6fa-4d85-9716-ea8f32db78e0": "generated-edge-simstep-44a4372f-a903-4c43-a9a5-c7549c875e11-d0f92604-a6fa-4d85-9716-ea8f32db78e0" + } +} \ No newline at end of file From 5fc2c8c14b977c8c9ebdff8e0fa44b00add29333 Mon Sep 17 00:00:00 2001 From: Abi <46168827+Abdullah85MBA@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:15:26 -0400 Subject: [PATCH 105/400] Add CodeCanvas diagram to README --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index b93794d4f..580e0caf5 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,15 @@ Here's how you can contribute: 1. Check [Contributor's guideline](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). First timers are encouraged to check `good-first-issue` tag. 2. Check [project structure](https://docs.checkmate.so/checkmate-2.1/developers-guide/general-project-structure) and [high level overview](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview). 3. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture. + +### How does `Checkmate` work under the hood? + +Check out this interactive walkthrough of the `Checkmate` codebase on CodeCanvas [here](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). + +To refine existing dataflow simulation or create new ones, follow the quick tutorial [here](https://docs.code-canvas.com/updating-diagram). + +CodeCanvas Screenshot + 4. Open an issue if you believe you've encountered a bug. 5. Check for good-first-issue's if you are a newcomer. 6. Make a pull request to add new features/make quality-of-life improvements/fix bugs. From ab64cd192b10af5240d134803adad022554f2113 Mon Sep 17 00:00:00 2001 From: Abi <46168827+Abdullah85MBA@users.noreply.github.com> Date: Tue, 28 Oct 2025 12:29:59 -0400 Subject: [PATCH 106/400] Updated Readme screenshot --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 580e0caf5..82fe999b9 100644 --- a/README.md +++ b/README.md @@ -166,7 +166,7 @@ Check out this interactive walkthrough of the `Checkmate` codebase on CodeCanvas To refine existing dataflow simulation or create new ones, follow the quick tutorial [here](https://docs.code-canvas.com/updating-diagram). -CodeCanvas Screenshot +image 4. Open an issue if you believe you've encountered a bug. 5. Check for good-first-issue's if you are a newcomer. From 30a9309bd8ca8bf920ba2f1ac6f43a9523fc0144 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 28 Oct 2025 22:41:05 -0700 Subject: [PATCH 107/400] add cacheable dns, stagger start times --- server/package.json | 3 ++- .../SuperSimpleQueue/SuperSimpleQueue.js | 6 +++++- .../src/service/v1/infrastructure/networkService.js | 12 +++++++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/server/package.json b/server/package.json index 380755b5b..7486e81a2 100755 --- a/server/package.json +++ b/server/package.json @@ -25,6 +25,7 @@ "axios": "^1.7.2", "bcryptjs": "3.0.2", "bullmq": "5.41.2", + "cacheable-lookup": "7.0.0", "compression": "1.8.1", "cookie-parser": "^1.4.7", "cors": "^2.8.5", @@ -51,7 +52,7 @@ "ping": "0.4.4", "sharp": "0.33.5", "ssl-checker": "2.0.10", - "super-simple-scheduler": "1.4.4", + "super-simple-scheduler": "1.4.5", "swagger-ui-express": "5.0.1", "winston": "^3.13.0" }, diff --git a/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js b/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js index c1e82af65..ac1286e10 100644 --- a/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js +++ b/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js @@ -35,8 +35,12 @@ class SuperSimpleQueue { this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob()); const monitors = await this.db.monitorModule.getAllMonitors(); for (const monitor of monitors) { - await this.addJob(monitor._id, monitor); + const randomOffset = Math.floor(Math.random() * monitor.interval); + setTimeout(() => { + this.addJob(monitor); + }, randomOffset); } + return true; } catch (error) { this.logger.error({ diff --git a/server/src/service/v1/infrastructure/networkService.js b/server/src/service/v1/infrastructure/networkService.js index ade86b1dc..d364f373e 100644 --- a/server/src/service/v1/infrastructure/networkService.js +++ b/server/src/service/v1/infrastructure/networkService.js @@ -1,3 +1,4 @@ +import CacheableLookup from "cacheable-lookup"; const SERVICE_NAME = "NetworkService"; class NetworkService { @@ -15,7 +16,6 @@ class NetworkService { this.NETWORK_ERROR = 5000; this.PING_ERROR = 5001; this.axios = axios; - this.got = got; this.https = https; this.jmespath = jmespath; this.GameDig = GameDig; @@ -26,6 +26,16 @@ class NetworkService { this.net = net; this.stringService = stringService; this.settingsService = settingsService; + + const cacheable = new CacheableLookup(); + + this.got = got.extend({ + dnsCache: cacheable, + timeout: { + request: 30000, + }, + retry: { limit: 1 }, + }); } // Helper functions From e100d54eedce3e0ad09d9f18a64b78d9d2aaaa2f Mon Sep 17 00:00:00 2001 From: "Gorkem Cetin (BWL)" <167266851+gorkem-bwl@users.noreply.github.com> Date: Wed, 29 Oct 2025 07:28:09 -0400 Subject: [PATCH 108/400] Simplify README by removing redundant content Removed redundant section about Checkmate's codebase walkthrough and dataflow simulation tutorial. --- README.md | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/README.md b/README.md index 82fe999b9..8083a5204 100644 --- a/README.md +++ b/README.md @@ -159,18 +159,10 @@ Here's how you can contribute: 1. Check [Contributor's guideline](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). First timers are encouraged to check `good-first-issue` tag. 2. Check [project structure](https://docs.checkmate.so/checkmate-2.1/developers-guide/general-project-structure) and [high level overview](https://bluewavelabs.gitbook.io/checkmate/developers-guide/high-level-overview). 3. Read a detailed structure of [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) if you would like to deep dive into the architecture. - -### How does `Checkmate` work under the hood? - -Check out this interactive walkthrough of the `Checkmate` codebase on CodeCanvas [here](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). - -To refine existing dataflow simulation or create new ones, follow the quick tutorial [here](https://docs.code-canvas.com/updating-diagram). - -image - 4. Open an issue if you believe you've encountered a bug. 5. Check for good-first-issue's if you are a newcomer. 6. Make a pull request to add new features/make quality-of-life improvements/fix bugs. +7. Check out this interactive walkthrough of the `Checkmate` codebase on CodeCanvas [here](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). To refine existing dataflow simulation or create new ones, follow the quick tutorial [here](https://docs.code-canvas.com/updating-diagram). From 60a3d46e59fbd7d27c911c833bc441438f30898a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 29 Oct 2025 10:03:16 -0700 Subject: [PATCH 109/400] prune --- .github/workflows/staging-deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/staging-deploy.yml b/.github/workflows/staging-deploy.yml index 27b7e8690..84be4e483 100644 --- a/.github/workflows/staging-deploy.yml +++ b/.github/workflows/staging-deploy.yml @@ -88,3 +88,4 @@ jobs: docker compose down docker compose pull docker compose up -d + docker system prune -af From 4ac0f1e87fbd755e6bb9ed8a8064faadc3e5dd05 Mon Sep 17 00:00:00 2001 From: Jannis Fedoruk-Betschki Date: Wed, 29 Oct 2025 20:16:39 +0100 Subject: [PATCH 110/400] feat: add detailed response time breakdown in Response Times table --- .../Components/ResponseTable/index.jsx | 126 ++++++++++++++++++ client/src/locales/en.json | 6 + 2 files changed, 132 insertions(+) diff --git a/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx b/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx index 948489ac1..a131092a0 100644 --- a/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx +++ b/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx @@ -7,6 +7,80 @@ import { StatusLabel } from "@/Components/v1/Label/index.jsx"; import { useTranslation } from "react-i18next"; import { formatDateWithTz } from "../../../../../../Utils/timeUtils.js"; import SkeletonLayout from "./skeleton.jsx"; +import Tooltip from "@mui/material/Tooltip"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import Stack from "@mui/material/Stack"; +import { lighten, useTheme } from "@mui/material"; + +/** + * Creates tooltip content with detailed timing breakdown + * Following the pattern from IncidentTable's GetTooltip function + * @param {Object} timings - Timing object (guaranteed to have phases by caller) + * @param {Object} theme - MUI theme object + * @param {Function} t - Translation function + * @returns {JSX.Element} Tooltip content + */ +const GetTooltip = (timings, theme, t) => { + const phases = timings.phases; + const timingDetails = [ + { label: t("dnsLookup"), value: phases.dns }, + { label: t("tcpConnection"), value: phases.tcp }, + { label: t("tlsHandshake"), value: phases.tls }, + { label: t("waitTime"), value: phases.wait }, + { label: t("timeToFirstByte"), value: phases.firstByte }, + { label: t("download"), value: phases.download }, + { label: t("total"), value: phases.total }, + ].filter((item) => item.value > 0); + + return ( + + + {t("responseTimeBreakdown")} + + {timingDetails.map((detail, index) => ( + + + {detail.label}: + + + {Math.round(detail.value)} ms + + + ))} + + ); +}; + const ResponseTable = ({ isLoading = false, checks = [], @@ -18,6 +92,8 @@ const ResponseTable = ({ setRowsPerPage, }) => { const { t } = useTranslation(); + const theme = useTheme(); + if (isLoading) { return ; } @@ -54,6 +130,56 @@ const ResponseTable = ({ content: t("message"), render: (row) => row.message, }, + { + id: "responseTime", + content: t("responseTime"), + render: (row) => { + const hasTimings = row.timings && row.timings.phases; + const responseTime = row.responseTime; + const responseTimeDisplay = + responseTime !== null && responseTime !== undefined + ? `${Math.round(responseTime)} ms` + : "N/A"; + + if (!hasTimings) { + return responseTimeDisplay; + } + + return ( + + + {responseTimeDisplay} + + + ); + }, + }, ]; return ( diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 13d4a7731..fc8e54a40 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -20,6 +20,12 @@ "delete": "Delete", "configure": "Configure", "responseTime": "Response time", + "responseTimeBreakdown": "Response Time Breakdown", + "dnsLookup": "DNS Lookup", + "tcpConnection": "TCP Connection", + "tlsHandshake": "TLS Handshake", + "waitTime": "Wait Time", + "timeToFirstByte": "Time to First Byte", "ms": "ms", "bar": "Bar", "area": "Area", From 87c549960ec64be42f35d69df5726843423d5abc Mon Sep 17 00:00:00 2001 From: Jannis Fedoruk-Betschki Date: Wed, 29 Oct 2025 20:32:21 +0100 Subject: [PATCH 111/400] fix: use array parameter instead of closure for i18n-safe bold styling --- .../v1/Uptime/Details/Components/ResponseTable/index.jsx | 6 +++--- client/src/locales/en.json | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx b/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx index a131092a0..0d0defc3b 100644 --- a/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx +++ b/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx @@ -50,14 +50,14 @@ const GetTooltip = (timings, theme, t) => { > {t("responseTimeBreakdown")} - {timingDetails.map((detail, index) => ( + {timingDetails.map((detail, index, array) => ( { diff --git a/client/src/locales/en.json b/client/src/locales/en.json index fc8e54a40..85b326917 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -26,6 +26,7 @@ "tlsHandshake": "TLS Handshake", "waitTime": "Wait Time", "timeToFirstByte": "Time to First Byte", + "download": "Download", "ms": "ms", "bar": "Bar", "area": "Area", From 42eaa754375fa46af00a5096be1f3f7e1327d030 Mon Sep 17 00:00:00 2001 From: Jannis Fedoruk-Betschki Date: Thu, 30 Oct 2025 07:25:48 +0100 Subject: [PATCH 112/400] fix: use sentence case for tooltip labels --- client/src/locales/en.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 85b326917..e2e1a4faf 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -20,12 +20,12 @@ "delete": "Delete", "configure": "Configure", "responseTime": "Response time", - "responseTimeBreakdown": "Response Time Breakdown", - "dnsLookup": "DNS Lookup", - "tcpConnection": "TCP Connection", - "tlsHandshake": "TLS Handshake", - "waitTime": "Wait Time", - "timeToFirstByte": "Time to First Byte", + "responseTimeBreakdown": "Response time breakdown", + "dnsLookup": "DNS lookup", + "tcpConnection": "TCP connection", + "tlsHandshake": "TLS handshake", + "waitTime": "Wait time", + "timeToFirstByte": "Time to first byte", "download": "Download", "ms": "ms", "bar": "Bar", From d2ff386f977564b202930e2cd9ad9eccf564756f Mon Sep 17 00:00:00 2001 From: Tudor Gradinaru Date: Thu, 30 Oct 2025 11:37:43 +0200 Subject: [PATCH 113/400] feat: include game monitor type in status page monitor selection --- .../src/Pages/v1/StatusPage/Create/Hooks/useMonitorsFetch.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/Pages/v1/StatusPage/Create/Hooks/useMonitorsFetch.jsx b/client/src/Pages/v1/StatusPage/Create/Hooks/useMonitorsFetch.jsx index 2a7db202a..81fefd65c 100644 --- a/client/src/Pages/v1/StatusPage/Create/Hooks/useMonitorsFetch.jsx +++ b/client/src/Pages/v1/StatusPage/Create/Hooks/useMonitorsFetch.jsx @@ -14,7 +14,7 @@ const useMonitorsFetch = () => { try { const response = await networkService.getMonitorsByTeamId({ limit: null, // donot return any checks for the monitors - types: ["http", "ping", "port"], // status page is available for uptime, ping, and port monitors + types: ["http", "ping", "port", "game"], // include game servers in status page monitor selection }); setMonitors(response.data.data.monitors); } catch (error) { From 5d48cecee8ce727f046efd4d216c66314ab7586f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 30 Oct 2025 10:57:48 -0700 Subject: [PATCH 114/400] fix missing param --- .../v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js | 4 ++-- server/src/service/v1/infrastructure/emailService.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js b/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js index ac1286e10..a3cdea8e7 100644 --- a/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js +++ b/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js @@ -35,9 +35,9 @@ class SuperSimpleQueue { this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob()); const monitors = await this.db.monitorModule.getAllMonitors(); for (const monitor of monitors) { - const randomOffset = Math.floor(Math.random() * monitor.interval); + const randomOffset = Math.floor(Math.random() * 100); setTimeout(() => { - this.addJob(monitor); + this.addJob(monitor._id, monitor); }, randomOffset); } diff --git a/server/src/service/v1/infrastructure/emailService.js b/server/src/service/v1/infrastructure/emailService.js index bc040d51a..ed661ff16 100755 --- a/server/src/service/v1/infrastructure/emailService.js +++ b/server/src/service/v1/infrastructure/emailService.js @@ -46,7 +46,7 @@ class EmailService { */ this.loadTemplate = (templateName) => { try { - const templatePath = this.path.join(__dirname, `../../templates/${templateName}.mjml`); + const templatePath = this.path.join(__dirname, `../../../templates/${templateName}.mjml`); const templateContent = this.fs.readFileSync(templatePath, "utf8"); return this.compile(templateContent); } catch (error) { From 492aed200abfef4c2eb41071aa87f678cd918bba Mon Sep 17 00:00:00 2001 From: matserix Date: Mon, 3 Nov 2025 00:44:57 +0100 Subject: [PATCH 115/400] feat: support Storage Classes other than 'gp3' --- charts/helm/checkmate/INSTALLATION.md | 1 + charts/helm/checkmate/templates/mongodb-statefulsets.yaml | 4 ++-- charts/helm/checkmate/templates/redis-statefulsets.yaml | 4 ++-- charts/helm/checkmate/values.yaml | 8 ++++++-- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/charts/helm/checkmate/INSTALLATION.md b/charts/helm/checkmate/INSTALLATION.md index 9ca321ad3..75420e6f4 100644 --- a/charts/helm/checkmate/INSTALLATION.md +++ b/charts/helm/checkmate/INSTALLATION.md @@ -21,6 +21,7 @@ cd checkmate/charts/helm/checkmate Edit `values.yaml` to update: - `client.ingress.host` and `server.ingress.host` with your domain names - `server.protocol` (usually http or https) +- `persistence.mongo.storageClass` and `persistence.redis.storageClass` with your storageclass - Secrets under the `secrets` section (`JWT_SECRET`, email credentials, API keys, etc.) — replace all change_me values ### 3. Deploy the Helm chart diff --git a/charts/helm/checkmate/templates/mongodb-statefulsets.yaml b/charts/helm/checkmate/templates/mongodb-statefulsets.yaml index f849e4e21..a4469ba31 100644 --- a/charts/helm/checkmate/templates/mongodb-statefulsets.yaml +++ b/charts/helm/checkmate/templates/mongodb-statefulsets.yaml @@ -37,8 +37,8 @@ spec: - metadata: name: checkmate-mongo-persistent-storage spec: - storageClassName: "gp3" + storageClassName: {{ .Values.persistence.mongo.storageClass }} accessModes: ["ReadWriteOnce"] resources: requests: - storage: {{ .Values.persistence.mongodbSize }} \ No newline at end of file + storage: {{ .Values.persistence.mongo.size }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/redis-statefulsets.yaml b/charts/helm/checkmate/templates/redis-statefulsets.yaml index 252361730..6029575b7 100644 --- a/charts/helm/checkmate/templates/redis-statefulsets.yaml +++ b/charts/helm/checkmate/templates/redis-statefulsets.yaml @@ -25,9 +25,9 @@ spec: - metadata: name: checkmate-redis-persistent-storage spec: - storageClassName: "gp3" + storageClassName: {{ .Values.persistence.redis.storageClass }} accessModes: ["ReadWriteOnce"] resources: requests: - storage: {{ .Values.persistence.redisSize }} + storage: {{ .Values.persistence.redis.size }} {{- end }} \ No newline at end of file diff --git a/charts/helm/checkmate/values.yaml b/charts/helm/checkmate/values.yaml index 0ba0051f8..f6d764daa 100644 --- a/charts/helm/checkmate/values.yaml +++ b/charts/helm/checkmate/values.yaml @@ -44,5 +44,9 @@ secrets: # REFRESH_TOKEN_TTL: 99d persistence: - mongodbSize: 5Gi - redisSize: 1Gi \ No newline at end of file + mongo: + size: 5Gi + storageClass: change_me + redis: + size: 1Gi + storageClass: change_me \ No newline at end of file From 19a61b6ce46c9fd137f35344da66380be74b3935 Mon Sep 17 00:00:00 2001 From: matserix Date: Mon, 3 Nov 2025 01:10:45 +0100 Subject: [PATCH 116/400] fix: update installation guide and values for MongoDB and Redis storage classes --- charts/helm/checkmate/INSTALLATION.md | 5 ++++- charts/helm/checkmate/templates/mongodb-statefulsets.yaml | 4 ++-- charts/helm/checkmate/templates/redis-statefulsets.yaml | 4 ++-- charts/helm/checkmate/values.yaml | 4 ++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/charts/helm/checkmate/INSTALLATION.md b/charts/helm/checkmate/INSTALLATION.md index 75420e6f4..4a2a120b5 100644 --- a/charts/helm/checkmate/INSTALLATION.md +++ b/charts/helm/checkmate/INSTALLATION.md @@ -21,7 +21,10 @@ cd checkmate/charts/helm/checkmate Edit `values.yaml` to update: - `client.ingress.host` and `server.ingress.host` with your domain names - `server.protocol` (usually http or https) -- `persistence.mongo.storageClass` and `persistence.redis.storageClass` with your storageclass +- **If upgrading**: Migrate persistence settings from flat structure to nested: + - Old: `persistence.mongodbSize` → New: `persistence.mongo.size` + - Old: `persistence.redisSize` → New: `persistence.redis.size` + - Add: `persistence.mongo.storageClass` and `persistence.redis.storageClass` (leave empty for default) - Secrets under the `secrets` section (`JWT_SECRET`, email credentials, API keys, etc.) — replace all change_me values ### 3. Deploy the Helm chart diff --git a/charts/helm/checkmate/templates/mongodb-statefulsets.yaml b/charts/helm/checkmate/templates/mongodb-statefulsets.yaml index a4469ba31..8149b77e2 100644 --- a/charts/helm/checkmate/templates/mongodb-statefulsets.yaml +++ b/charts/helm/checkmate/templates/mongodb-statefulsets.yaml @@ -37,8 +37,8 @@ spec: - metadata: name: checkmate-mongo-persistent-storage spec: - storageClassName: {{ .Values.persistence.mongo.storageClass }} + storageClassName: {{ .Values.persistence.mongo.storageClass | quote }} accessModes: ["ReadWriteOnce"] resources: requests: - storage: {{ .Values.persistence.mongo.size }} \ No newline at end of file + storage: {{ .Values.persistence.mongo.size | default "5Gi" | quote }} \ No newline at end of file diff --git a/charts/helm/checkmate/templates/redis-statefulsets.yaml b/charts/helm/checkmate/templates/redis-statefulsets.yaml index 6029575b7..acf7c85a8 100644 --- a/charts/helm/checkmate/templates/redis-statefulsets.yaml +++ b/charts/helm/checkmate/templates/redis-statefulsets.yaml @@ -25,9 +25,9 @@ spec: - metadata: name: checkmate-redis-persistent-storage spec: - storageClassName: {{ .Values.persistence.redis.storageClass }} + storageClassName: {{ .Values.persistence.redis.storageClass | quote }} accessModes: ["ReadWriteOnce"] resources: requests: - storage: {{ .Values.persistence.redis.size }} + storage: {{ .Values.persistence.redis.size | default "1Gi" | quote }} {{- end }} \ No newline at end of file diff --git a/charts/helm/checkmate/values.yaml b/charts/helm/checkmate/values.yaml index f6d764daa..a1a1527de 100644 --- a/charts/helm/checkmate/values.yaml +++ b/charts/helm/checkmate/values.yaml @@ -46,7 +46,7 @@ secrets: persistence: mongo: size: 5Gi - storageClass: change_me + storageClass: "" redis: size: 1Gi - storageClass: change_me \ No newline at end of file + storageClass: "" \ No newline at end of file From 0c3fdbc539e4b6bced9a804edef4a31600bef4a3 Mon Sep 17 00:00:00 2001 From: karenvicent Date: Thu, 6 Nov 2025 17:06:33 -0500 Subject: [PATCH 117/400] feat: add incident model, module and service with StatusService integration - Create Incident model with resolution tracking fields - Implement IncidentModule for data access - Implement IncidentService with business logic - Integrate with StatusService to auto-create/resolve incidents on status change --- server/src/config/services.js | 20 +- server/src/db/v1/MongoDB.js | 2 + server/src/db/v1/models/Incident.js | 72 +++++ server/src/db/v1/modules/incidentModule.js | 258 ++++++++++++++++ server/src/service/infrastructure/notes | 83 ++++++ .../service/v1/business/incidentService.js | 277 ++++++++++++++++++ .../v1/infrastructure/statusService.js | 40 ++- 7 files changed, 748 insertions(+), 4 deletions(-) create mode 100644 server/src/db/v1/models/Incident.js create mode 100644 server/src/db/v1/modules/incidentModule.js create mode 100644 server/src/service/infrastructure/notes create mode 100644 server/src/service/v1/business/incidentService.js diff --git a/server/src/config/services.js b/server/src/config/services.js index c1679868d..cecc3bba9 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -17,6 +17,7 @@ import DiagnosticService from "../service/v1/business/diagnosticService.js"; import InviteService from "../service/v1/business/inviteService.js"; import MaintenanceWindowService from "../service/v1/business/maintenanceWindowService.js"; import MonitorService from "../service/v1/business/monitorService.js"; +import IncidentService from "../service/v1/business/incidentService.js"; import papaparse from "papaparse"; import axios from "axios"; import got from "got"; @@ -57,6 +58,7 @@ import MonitorStats from "../db/v1/models/MonitorStats.js"; import Notification from "../db/v1/models/Notification.js"; import RecoveryToken from "../db/v1/models/RecoveryToken.js"; import AppSettings from "../db/v1/models/AppSettings.js"; +import Incident from "../db/v1/models/Incident.js"; import InviteModule from "../db/v1/modules/inviteModule.js"; import CheckModule from "../db/v1/modules/checkModule.js"; @@ -67,6 +69,7 @@ import MonitorModule from "../db/v1/modules/monitorModule.js"; import NotificationModule from "../db/v1/modules/notificationModule.js"; import RecoveryModule from "../db/v1/modules/recoveryModule.js"; import SettingsModule from "../db/v1/modules/settingsModule.js"; +import IncidentModule from "../db/v1/modules/incidentModule.js"; // V2 Business import AuthServiceV2 from "../service/v2/business/AuthService.js"; @@ -120,6 +123,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService const notificationModule = new NotificationModule({ Notification, Monitor }); const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService }); const settingsModule = new SettingsModule({ AppSettings }); + const incidentModule = new IncidentModule({ logger, Incident, Monitor, User }); const db = new MongoDB({ logger, @@ -133,6 +137,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService notificationModule, recoveryModule, settingsModule, + incidentModule, }); await db.connect(); @@ -153,7 +158,17 @@ export const initializeServices = async ({ logger, envSettings, settingsService }); const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); const bufferService = new BufferService({ db, logger, envSettings }); - const statusService = new StatusService({ db, logger, buffer: bufferService }); + + const errorService = new ErrorService(); + + const incidentService = new IncidentService({ + db, + logger, + errorService, + stringService, + }); + + const statusService = new StatusService({ db, logger, buffer: bufferService, incidentService }); const notificationUtils = new NotificationUtils({ stringService, @@ -169,8 +184,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService notificationUtils, }); - const errorService = new ErrorService(); - const superSimpleQueueHelper = new SuperSimpleQueueHelper({ db, logger, @@ -275,6 +288,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService inviteService, maintenanceWindowService, monitorService, + incidentService, errorService, logger, //v2 diff --git a/server/src/db/v1/MongoDB.js b/server/src/db/v1/MongoDB.js index 3bdae3525..115c7e9ee 100755 --- a/server/src/db/v1/MongoDB.js +++ b/server/src/db/v1/MongoDB.js @@ -19,6 +19,7 @@ class MongoDB { pageSpeedCheckModule, recoveryModule, settingsModule, + incidentModule, }) { this.logger = logger; this.envSettings = envSettings; @@ -34,6 +35,7 @@ class MongoDB { this.settingsModule = settingsModule; this.statusPageModule = statusPageModule; this.networkCheckModule = networkCheckModule; + this.incidentModule = incidentModule; } get serviceName() { diff --git a/server/src/db/v1/models/Incident.js b/server/src/db/v1/models/Incident.js new file mode 100644 index 000000000..c37df5bde --- /dev/null +++ b/server/src/db/v1/models/Incident.js @@ -0,0 +1,72 @@ +import mongoose from "mongoose"; + +const IncidentSchema = mongoose.Schema({ + monitorId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Monitor", + required: true, + immutable: true, + index: true + }, + teamId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Team", + required: true, + immutable: true, + index: true + }, + startTime: { + type: Date, + required: true, + immutable: true + }, + endTime: { + type: Date, + default: null + }, + status: { + type: String, + enum: ["active", "resolved"], + default: "active", + index: true + }, + message: { + type: String, + default: null + }, + statusCode: { + type: Number, + index: true, + default: null + }, + resolutionType: { + type: String, + enum: ["automatic", "manual"], + default: null + }, + resolvedBy: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + default: null + }, + comment: { + type: String, + default: null + }, + checks: [{ + type: mongoose.Schema.Types.ObjectId, + ref: "Check" + }] +}, { timestamps: true }); + +IncidentSchema.index({ monitorId: 1, status: 1 }); +IncidentSchema.index({ teamId: 1, status: 1 }); +IncidentSchema.index({ teamId: 1, startTime: -1 }); +IncidentSchema.index({ status: 1, startTime: -1 }); +IncidentSchema.index({ resolutionType: 1, status: 1 }); +IncidentSchema.index({ resolvedBy: 1, status: 1 }); +IncidentSchema.index({ createdAt: -1 }); + +const Incident = mongoose.model("Incident", IncidentSchema); + +export default Incident; diff --git a/server/src/db/v1/modules/incidentModule.js b/server/src/db/v1/modules/incidentModule.js new file mode 100644 index 000000000..8cfa0f0b1 --- /dev/null +++ b/server/src/db/v1/modules/incidentModule.js @@ -0,0 +1,258 @@ +import { ObjectId } from "mongodb"; + +const SERVICE_NAME = "incidentModule"; + +const dateRangeLookup = { + recent: new Date(new Date().setDate(new Date().getDate() - 2)), + hour: new Date(new Date().setHours(new Date().getHours() - 1)), + day: new Date(new Date().setDate(new Date().getDate() - 1)), + week: new Date(new Date().setDate(new Date().getDate() - 7)), + month: new Date(new Date().setMonth(new Date().getMonth() - 1)), + all: undefined, +}; + +class IncidentModule { + constructor({ logger, Incident, Monitor, User }) { + this.logger = logger; + this.Incident = Incident; + this.Monitor = Monitor; + this.User = User; + } + + createIncident = async (incidentData) => { + try { + const incident = await this.Incident.create(incidentData); + return incident; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "createIncident"; + throw error; + } + }; + + createIncidents = async (incidents) => { + try { + await this.Incident.insertMany(incidents, { ordered: false }); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "createIncidents"; + throw error; + } + }; + + getActiveIncidentByMonitor = async (monitorId) => { + try { + return await this.Incident.findOne({ monitorId: new ObjectId(monitorId), status: "active" }); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getActiveIncidentByMonitor"; + throw error; + } + }; + + getIncidentById = async (incidentId) => { + try { + return await this.Incident.findById(incidentId) + .populate("monitorId", "name type url") + .populate("resolvedBy", "name email"); + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getIncidentById"; + throw error; + } + }; + + resolveIncident = async (incidentId, { resolutionType = "automatic", resolvedBy = null, comment = null, endTime = new Date() } = {}) => { + try { + const update = { + status: "resolved", + endTime, + resolutionType, + resolvedBy, + ...(comment !== null && { comment }), + }; + const incident = await this.Incident.findOneAndUpdate( + { _id: new ObjectId(incidentId) }, + { $set: update }, + { new: true } + ); + + if (!incident) { + throw new Error("Incident not found"); + } + + return incident; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "resolveIncident"; + throw error; + } + }; + + addCheckToIncident = async (incidentId, checkId) => { + try { + const incident = await this.Incident.findOneAndUpdate( + { _id: new ObjectId(incidentId) }, + { $addToSet: { checks: new ObjectId(checkId) } }, + { new: true } + ); + + if (!incident) { + throw new Error("Incident not found"); + } + + return incident; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "addCheckToIncident"; + throw error; + } + }; + + getIncidentsByTeam = async ({ teamId, sortOrder, dateRange, filter, page, rowsPerPage, status, monitorId, resolutionType }) => { + try { + page = parseInt(page); + rowsPerPage = parseInt(rowsPerPage); + + const matchStage = { + teamId: new ObjectId(teamId), + ...(status && { status }), + ...(monitorId && { monitorId: new ObjectId(monitorId) }), + ...(resolutionType && { resolutionType }), + ...(dateRangeLookup[dateRange] && { + startTime: { + $gte: dateRangeLookup[dateRange], + }, + }), + }; + + if (filter !== undefined) { + switch (filter) { + case "all": + break; + case "active": + matchStage.status = "active"; + break; + case "resolved": + matchStage.status = "resolved"; + break; + default: + break; + } + } + + sortOrder = sortOrder === "asc" ? 1 : -1; + + let skip = 0; + if (page && rowsPerPage) { + skip = page * rowsPerPage; + } + + const incidents = await this.Incident.aggregate([ + { $match: matchStage }, + { $sort: { startTime: sortOrder } }, + { + $facet: { + summary: [{ $count: "incidentsCount" }], + incidents: [{ $skip: skip }, { $limit: rowsPerPage }], + }, + }, + { + $project: { + incidentsCount: { + $ifNull: [{ $arrayElemAt: ["$summary.incidentsCount", 0] }, 0], + }, + incidents: { + $ifNull: ["$incidents", []], + }, + }, + }, + ]); + + return incidents[0]; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getIncidentsByTeam"; + throw error; + } + }; + + getIncidentSummary = async ({ teamId, dateRange }) => { + try { + const matchStage = { + teamId: new ObjectId(teamId), + ...(dateRangeLookup[dateRange] && { + startTime: { + $gte: dateRangeLookup[dateRange], + }, + }), + }; + + const summary = await this.Incident.aggregate([ + { $match: matchStage }, + { + $group: { + _id: "$status", + count: { $sum: 1 }, + manualResolutions: { + $sum: { + $cond: [{ $eq: ["$resolutionType", "manual"] }, 1, 0], + }, + }, + }, + }, + ]); + + const result = { + total: 0, + active: 0, + resolved: 0, + manual: 0, + }; + + summary.forEach((item) => { + result.total += item.count; + if (item._id === "active") { + result.active = item.count; + } + if (item._id === "resolved") { + result.resolved = item.count; + } + result.manual += item.manualResolutions; + }); + + return result; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getIncidentSummary"; + throw error; + } + }; + + deleteIncidentsByMonitor = async (monitorId) => { + try { + const result = await this.Incident.deleteMany({ monitorId: new ObjectId(monitorId) }); + return result.deletedCount; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteIncidentsByMonitor"; + throw error; + } + }; + + deleteIncidentsByTeamId = async (teamId) => { + try { + const teamMonitors = await this.Monitor.find({ teamId }, { _id: 1 }); + const monitorIds = teamMonitors.map((monitor) => monitor._id); + const deleteResult = await this.Incident.deleteMany({ monitorId: { $in: monitorIds } }); + return deleteResult.deletedCount; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "deleteIncidentsByTeamId"; + throw error; + } + }; +} + +export default IncidentModule; + diff --git a/server/src/service/infrastructure/notes b/server/src/service/infrastructure/notes new file mode 100644 index 000000000..ce5a77ddb --- /dev/null +++ b/server/src/service/infrastructure/notes @@ -0,0 +1,83 @@ +import { Typography, Select } from "@mui/material"; +import MenuItem from "@mui/material/MenuItem"; +import type { SelectProps } from "@mui/material/Select"; +import { useTheme } from "@mui/material/styles"; +import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; + +export const SelectInput: React.FC = ({ ...props }) => { + const theme = useTheme(); + return ( + - {languagesForDisplay} - - ); -}; diff --git a/client/src/Components/v2/Inputs/RadioInput.tsx b/client/src/Components/v2/Inputs/RadioInput.tsx deleted file mode 100644 index d903a7b58..000000000 --- a/client/src/Components/v2/Inputs/RadioInput.tsx +++ /dev/null @@ -1,66 +0,0 @@ -import Radio from "@mui/material/Radio"; -import type { RadioProps } from "@mui/material/Radio"; -import { useTheme } from "@mui/material/styles"; -import RadioChecked from "@/assets/icons/radio-checked.svg?react"; -import FormControlLabel from "@mui/material/FormControlLabel"; -import Typography from "@mui/material/Typography"; - -interface RadioInputProps extends RadioProps {} - -export const RadioInput: React.FC = ({ ...props }) => { - const theme = useTheme(); - return ( - } - sx={{ - color: "transparent", - boxShadow: `inset 0 0 0 1px ${theme.palette.secondary.main}`, - "&:not(.Mui-checked)": { - boxShadow: `inset 0 0 0 1px ${theme.palette.primary.contrastText}70`, // Use theme text color for the outline - }, - mt: theme.spacing(0.5), - padding: 0, - "& .MuiSvgIcon-root": { - fontSize: 16, - }, - }} - /> - ); -}; - -export const RadioWithDescription: React.FC< - RadioInputProps & { label: string; description: string } -> = ({ label, description, ...props }) => { - const theme = useTheme(); - return ( - } - label={ - <> - {label} - - {description} - - - } - sx={{ - alignItems: "flex-start", - p: theme.spacing(2.5), - m: theme.spacing(-2.5), - borderRadius: theme.shape.borderRadius, - - "&:hover": { - backgroundColor: theme.palette.tertiary.main, - }, - "& .MuiButtonBase-root": { - p: 0, - mr: theme.spacing(6), - }, - }} - /> - ); -}; diff --git a/client/src/Components/v2/Inputs/Select.tsx b/client/src/Components/v2/Inputs/Select.tsx deleted file mode 100644 index 54aacf68a..000000000 --- a/client/src/Components/v2/Inputs/Select.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { Typography, Select } from "@mui/material"; -import MenuItem from "@mui/material/MenuItem"; -import type { SelectProps } from "@mui/material/Select"; -import { useTheme } from "@mui/material/styles"; -import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; - -export const SelectInput: React.FC = ({ ...props }) => { - const theme = useTheme(); - return ( - setSelectedMonitor(e.target.value)} + items={monitorNames} + sx={{ + backgroundColor: theme.palette.primary.main, + color: theme.palette.primary.contrastTextSecondary, + }} + maxWidth={250} + /> +
+ + + {t("incidentsOptionsHeaderFilterBy")} + + - ); -}; - -type ItemTypes = string | number; -interface SelectItem { - _id: ItemTypes; - name: string; -} -export type CustomSelectProps = SelectProps & { - items: SelectItem[]; - placeholder?: string; - isHidden?: boolean; - hasError?: boolean; -}; - -export const SelectFromItems: React.FC = ( - { items, placeholder, isHidden = false, hasError = false, ...props } -) => { - return ( - { - if (!selected) { - return ( - - {placeholder ?? ""} - - ); - } - const selectedItem = items.find((item) => item._id === selected); - const displayName = selectedItem ? selectedItem.name : placeholder; - return ( - - {displayName} - - ); - }} - {...props} - > - {items.map((item) => ( - - {item.name} - - ))} - - ); -} - -SelectInput.displayName = "SelectInput"; diff --git a/server/src/service/v1/infrastructure/bufferService.js b/server/src/service/v1/infrastructure/bufferService.js index 9b05d05c8..a218f56a0 100755 --- a/server/src/service/v1/infrastructure/bufferService.js +++ b/server/src/service/v1/infrastructure/bufferService.js @@ -1,9 +1,12 @@ +import { config } from "@/config/index.js"; + const SERVICE_NAME = "BufferService"; class BufferService { static SERVICE_NAME = SERVICE_NAME; constructor({ db, logger, envSettings, incidentService }) { - this.BUFFER_TIMEOUT = envSettings.nodeEnv === "development" ? 1000 : 1000 * 60 * 1; // 1 minute + console.log(envSettings); + this.BUFFER_TIMEOUT = config.NODE_ENV === "development" ? 10 : 1000 * 60 * 1; // 1 minute this.db = db; this.logger = logger; this.incidentService = incidentService; diff --git a/server/src/types/express.d.ts b/server/src/types/express.d.ts index 7c78a053c..b9f7386f6 100644 --- a/server/src/types/express.d.ts +++ b/server/src/types/express.d.ts @@ -3,6 +3,7 @@ import { ITokenizedUser } from "../db/models/index.ts"; declare global { namespace Express { interface Request { + file?: Multer.File; user?: ITokenizedUser; resource?: any; } diff --git a/server/src/utils/AppError.ts b/server/src/utils/AppError.ts new file mode 100644 index 000000000..93baeb78e --- /dev/null +++ b/server/src/utils/AppError.ts @@ -0,0 +1,24 @@ +export interface AppErrorConfig { + message: string; + status?: number; + service?: string | null; + method?: string | null; + details?: any; +} + +export class AppError extends Error { + private status: number; + private service: string | null; + private method: string | null; + private details: any; + + constructor({ message, status = 500, service = null, method = null, details = null }: AppErrorConfig) { + super(message); + this.status = status; + this.service = service; + this.method = method; + this.details = details; + + Error.captureStackTrace(this, this.constructor); + } +} From 64320c7acacb2f440ebc299cc3d36f30d124649d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 Jan 2026 15:02:19 -0800 Subject: [PATCH 161/400] js -> ts --- server/src/config/controllers.js | 5 +- .../controllers/v1/notificationController.js | 180 ---------------- .../controllers/v1/notificationController.ts | 197 ++++++++++++++++++ 3 files changed, 198 insertions(+), 184 deletions(-) delete mode 100644 server/src/controllers/v1/notificationController.js create mode 100644 server/src/controllers/v1/notificationController.ts diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js index 4589ddd85..fff25351c 100644 --- a/server/src/config/controllers.js +++ b/server/src/config/controllers.js @@ -51,10 +51,7 @@ export const initializeControllers = (services) => { }); controllers.logController = new LogController(commonDependencies); controllers.statusPageController = new StatusPageController(commonDependencies); - controllers.notificationController = new NotificationController(commonDependencies, { - notificationService: services.notificationService, - statusService: services.statusService, - }); + controllers.notificationController = new NotificationController(services.notificationService, services.db); controllers.diagnosticController = new DiagnosticController(commonDependencies, { diagnosticService: services.diagnosticService, }); diff --git a/server/src/controllers/v1/notificationController.js b/server/src/controllers/v1/notificationController.js deleted file mode 100644 index 37c2b4e60..000000000 --- a/server/src/controllers/v1/notificationController.js +++ /dev/null @@ -1,180 +0,0 @@ -import { createNotificationBodyValidation } from "../../validation/joi.js"; -import BaseController from "./baseController.js"; - -const SERVICE_NAME = "NotificationController"; - -class NotificationController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - constructor(commonDependencies, { notificationService, statusService }) { - super(commonDependencies); - this.notificationService = notificationService; - this.statusService = statusService; - } - - get serviceName() { - return NotificationController.SERVICE_NAME; - } - - testNotification = this.asyncHandler( - async (req, res) => { - const notification = req.body; - - const success = await this.notificationService.sendTestNotification(notification); - - if (!success) { - throw this.errorService.createServerError("Sending notification failed"); - } - - return res.success({ - msg: "Notification sent successfully", - }); - }, - SERVICE_NAME, - "testNotification" - ); - - createNotification = this.asyncHandler( - async (req, res) => { - await createNotificationBodyValidation.validateAsync(req.body, { - abortEarly: false, - }); - - const body = req.body; - - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const userId = req?.user?._id; - if (!userId) { - throw this.errorService.createBadRequestError("User ID is required"); - } - body.userId = userId; - body.teamId = teamId; - - const notification = await this.db.notificationModule.createNotification(body); - return res.success({ - msg: "Notification created successfully", - data: notification, - }); - }, - SERVICE_NAME, - "createNotification" - ); - - getNotificationsByTeamId = this.asyncHandler( - async (req, res) => { - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const notifications = await this.db.notificationModule.getNotificationsByTeamId(teamId); - - return res.success({ - msg: "Notifications fetched successfully", - data: notifications, - }); - }, - SERVICE_NAME, - "getNotificationsByTeamId" - ); - - deleteNotification = this.asyncHandler( - async (req, res) => { - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const notification = await this.db.notificationModule.getNotificationById(req.params.id); - if (!notification.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } - - await this.db.notificationModule.deleteNotificationById(req.params.id); - return res.success({ - msg: "Notification deleted successfully", - }); - }, - SERVICE_NAME, - "deleteNotification" - ); - - getNotificationById = this.asyncHandler( - async (req, res) => { - const notification = await this.db.notificationModule.getNotificationById(req.params.id); - - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - if (!notification.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } - return res.success({ - msg: "Notification fetched successfully", - data: notification, - }); - }, - SERVICE_NAME, - "getNotificationById" - ); - - editNotification = this.asyncHandler( - async (req, res) => { - await createNotificationBodyValidation.validateAsync(req.body, { - abortEarly: false, - }); - - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const notification = await this.db.notificationModule.getNotificationById(req.params.id); - - if (!notification.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } - - const editedNotification = await this.db.notificationModule.editNotification(req.params.id, req.body); - return res.success({ - msg: "Notification updated successfully", - data: editedNotification, - }); - }, - SERVICE_NAME, - "editNotification" - ); - - testAllNotifications = this.asyncHandler( - async (req, res) => { - const monitorId = req.body.monitorId; - const teamId = req?.user?.teamId; - if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); - } - - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - - if (!monitor.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } - - const notifications = monitor.notifications; - if (notifications.length === 0) throw this.errorService.createBadRequestError("No notifications"); - const result = await this.notificationService.testAllNotifications(notifications); - if (!result) throw this.errorService.createServerError("Failed to send all notifications"); - return res.success({ - msg: "All notifications sent successfully", - }); - }, - SERVICE_NAME, - "testAllNotifications" - ); -} - -export default NotificationController; diff --git a/server/src/controllers/v1/notificationController.ts b/server/src/controllers/v1/notificationController.ts new file mode 100644 index 000000000..71f154a2e --- /dev/null +++ b/server/src/controllers/v1/notificationController.ts @@ -0,0 +1,197 @@ +import { Request, Response, NextFunction } from "express"; + +import { createNotificationBodyValidation } from "@/validation/joi.js"; +import { AppError } from "@/utils/AppError.js"; + +const SERVICE_NAME = "NotificationController"; + +class NotificationController { + static SERVICE_NAME = SERVICE_NAME; + private db: any; + private notificationService: any; + constructor(notificationService: any, statusService: any, db: any) { + this.notificationService = notificationService; + this.db = db; + } + + get serviceName() { + return NotificationController.SERVICE_NAME; + } + + testNotification = async (req: Request, res: Response, next: NextFunction) => { + try { + const notification = req.body; + + const success = await this.notificationService.sendTestNotification(notification); + + if (!success) { + throw new AppError({ message: "Sending notification failed", status: 500 }); + } + + return res.status(200).json({ + success: true, + msg: "Notification sent successfully", + }); + } catch (error) { + next(error); + } + }; + + createNotification = async (req: Request, res: Response, next: NextFunction) => { + try { + await createNotificationBodyValidation.validateAsync(req.body, { + abortEarly: false, + }); + + const body = req.body; + + const teamId = req?.user?.teamId; + if (!teamId) { + throw new AppError({ message: "Team ID is required", status: 400 }); + } + + const userId = req?.user?._id; + if (!userId) { + throw new AppError({ message: "User ID is required", status: 400 }); + } + body.userId = userId; + body.teamId = teamId; + + const notification = await this.db.notificationModule.createNotification(body); + return res.status(200).json({ + success: true, + msg: "Notification created successfully", + data: notification, + }); + } catch (error) { + next(error); + } + }; + + getNotificationsByTeamId = async (req: Request, res: Response, next: NextFunction) => { + try { + const teamId = req?.user?.teamId; + if (!teamId) { + throw new AppError({ message: "Team ID is required", status: 400 }); + } + + const notifications = await this.db.notificationModule.getNotificationsByTeamId(teamId); + + return res.status(200).json({ + success: true, + msg: "Notifications fetched successfully", + data: notifications, + }); + } catch (error) { + next(error); + } + }; + + deleteNotification = async (req: Request, res: Response, next: NextFunction) => { + try { + const teamId = req?.user?.teamId; + if (!teamId) { + throw new AppError({ message: "Team ID is required", status: 400 }); + } + + const notification = await this.db.notificationModule.getNotificationById(req.params.id); + if (!notification.teamId.equals(teamId)) { + throw new AppError({ message: "Unauthorized", status: 403 }); + } + + await this.db.notificationModule.deleteNotificationById(req.params.id); + return res.status(200).json({ + success: true, + msg: "Notification deleted successfully", + }); + } catch (error) { + next(error); + } + }; + + getNotificationById = async (req: Request, res: Response, next: NextFunction) => { + try { + const notification = await this.db.notificationModule.getNotificationById(req.params.id); + + const teamId = req?.user?.teamId; + if (!teamId) { + throw new AppError({ message: "Team ID is required", status: 400 }); + } + + if (!notification.teamId.equals(teamId)) { + throw new AppError({ message: "Unauthorized", status: 403 }); + } + return res.status(200).json({ + success: true, + msg: "Notification fetched successfully", + data: notification, + }); + } catch (error) { + next(error); + } + }; + + editNotification = async (req: Request, res: Response, next: NextFunction) => { + try { + await createNotificationBodyValidation.validateAsync(req.body, { + abortEarly: false, + }); + + const teamId = req?.user?.teamId; + if (!teamId) { + throw new AppError({ message: "Team ID is required", status: 400 }); + } + + const notification = await this.db.notificationModule.getNotificationById(req.params.id); + + if (!notification.teamId.equals(teamId)) { + throw new AppError({ message: "Unauthorized", status: 403 }); + } + + const editedNotification = await this.db.notificationModule.editNotification(req.params.id, req.body); + return res.status(200).json({ + success: true, + msg: "Notification updated successfully", + data: editedNotification, + }); + } catch (error) { + next(error); + } + }; + + testAllNotifications = async (req: Request, res: Response, next: NextFunction) => { + try { + const monitorId = req.body.monitorId; + const teamId = req?.user?.teamId; + if (!teamId) { + throw new AppError({ message: "Team ID is required", status: 400 }); + } + + const monitor = await this.db.monitorModule.getMonitorById(monitorId); + + if (!monitor.teamId.equals(teamId)) { + throw new AppError({ message: "Unauthorized", status: 403 }); + } + + const notifications = monitor.notifications; + if (notifications.length === 0) { + throw new AppError({ message: "No notifications", status: 400 }); + } + + const result = await this.notificationService.testAllNotifications(notifications); + + if (!result) { + throw new AppError({ message: "Failed to send all notifications", status: 500 }); + } + + return res.status(200).json({ + success: true, + msg: "All notifications sent successfully", + }); + } catch (error) { + next(error); + } + }; +} + +export default NotificationController; From 3f631ff3825339b53d553ada8cb78de2565f7cf2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 Jan 2026 15:07:02 -0800 Subject: [PATCH 162/400] js -> ts --- server/src/config/controllers.js | 4 +- server/src/controllers/v1/queueController.js | 87 ------------------- server/src/controllers/v1/queueController.ts | 88 ++++++++++++++++++++ 3 files changed, 89 insertions(+), 90 deletions(-) delete mode 100755 server/src/controllers/v1/queueController.js create mode 100755 server/src/controllers/v1/queueController.ts diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js index fff25351c..f3a1cf7b2 100644 --- a/server/src/config/controllers.js +++ b/server/src/config/controllers.js @@ -46,9 +46,7 @@ export const initializeControllers = (services) => { settingsService: services.settingsService, maintenanceWindowService: services.maintenanceWindowService, }); - controllers.queueController = new QueueController(commonDependencies, { - jobQueue: services.jobQueue, - }); + controllers.queueController = new QueueController(services.jobQueue); controllers.logController = new LogController(commonDependencies); controllers.statusPageController = new StatusPageController(commonDependencies); controllers.notificationController = new NotificationController(services.notificationService, services.db); diff --git a/server/src/controllers/v1/queueController.js b/server/src/controllers/v1/queueController.js deleted file mode 100755 index d2c3fd06f..000000000 --- a/server/src/controllers/v1/queueController.js +++ /dev/null @@ -1,87 +0,0 @@ -import BaseController from "./baseController.js"; -const SERVICE_NAME = "JobQueueController"; - -class JobQueueController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - constructor(commonDependencies, { jobQueue }) { - super(commonDependencies); - this.jobQueue = jobQueue; - } - - get serviceName() { - return JobQueueController.SERVICE_NAME; - } - - getMetrics = this.asyncHandler( - async (req, res) => { - const metrics = await this.jobQueue.getMetrics(); - res.success({ - msg: this.stringService.queueGetMetrics, - data: metrics, - }); - }, - SERVICE_NAME, - "getMetrics" - ); - - getJobs = this.asyncHandler( - async (req, res) => { - const jobs = await this.jobQueue.getJobs(); - return res.success({ - msg: this.stringService.queueGetJobs, - data: jobs, - }); - }, - SERVICE_NAME, - "getJobs" - ); - - getAllMetrics = this.asyncHandler( - async (req, res) => { - const jobs = await this.jobQueue.getJobs(); - const metrics = await this.jobQueue.getMetrics(); - return res.success({ - msg: this.stringService.queueGetAllMetrics, - data: { jobs, metrics }, - }); - }, - SERVICE_NAME, - "getAllMetrics" - ); - - addJob = this.asyncHandler( - async (req, res) => { - await this.jobQueue.addJob(Math.random().toString(36).substring(7)); - return res.success({ - msg: this.stringService.queueAddJob, - }); - }, - SERVICE_NAME, - "addJob" - ); - - flushQueue = this.asyncHandler( - async (req, res) => { - const result = await this.jobQueue.flushQueues(); - return res.success({ - msg: this.stringService.jobQueueFlush, - data: result, - }); - }, - SERVICE_NAME, - "flushQueue" - ); - - checkQueueHealth = this.asyncHandler( - async (req, res) => { - const stuckQueues = await this.jobQueue.checkQueueHealth(); - return res.success({ - msg: this.stringService.queueHealthCheck, - data: stuckQueues, - }); - }, - SERVICE_NAME, - "checkQueueHealth" - ); -} -export default JobQueueController; diff --git a/server/src/controllers/v1/queueController.ts b/server/src/controllers/v1/queueController.ts new file mode 100755 index 000000000..cb39be826 --- /dev/null +++ b/server/src/controllers/v1/queueController.ts @@ -0,0 +1,88 @@ +import { Request, Response, NextFunction } from "express"; + +const SERVICE_NAME = "JobQueueController"; + +class JobQueueController { + static SERVICE_NAME = SERVICE_NAME; + private jobQueue: any; + constructor(jobQueue: any) { + this.jobQueue = jobQueue; + } + + get serviceName() { + return JobQueueController.SERVICE_NAME; + } + + getMetrics = async (req: Request, res: Response, next: NextFunction) => { + try { + const metrics = await this.jobQueue.getMetrics(); + res.status(200).json({ + msg: "Queue metrics fetched successfully", + data: metrics, + }); + } catch (error) { + next(error); + } + }; + + getJobs = async (req: Request, res: Response, next: NextFunction) => { + try { + const jobs = await this.jobQueue.getJobs(); + return res.status(200).json({ + msg: "Queue jobs fetched successfully", + data: jobs, + }); + } catch (error) { + next(error); + } + }; + + getAllMetrics = async (req: Request, res: Response, next: NextFunction) => { + try { + const jobs = await this.jobQueue.getJobs(); + const metrics = await this.jobQueue.getMetrics(); + return res.status(200).json({ + msg: "Queue metrics fetched successfully", + data: { jobs, metrics }, + }); + } catch (error) { + next(error); + } + }; + + addJob = async (req: Request, res: Response, next: NextFunction) => { + try { + await this.jobQueue.addJob(Math.random().toString(36).substring(7)); + return res.status(200).json({ + msg: "Job added to queue successfully", + }); + } catch (error) { + next(error); + } + }; + + flushQueue = async (req: Request, res: Response, next: NextFunction) => { + try { + const result = await this.jobQueue.flushQueues(); + return res.status(200).json({ + msg: "Queue flushed successfully", + data: result, + }); + } catch (error) { + next(error); + } + }; + + checkQueueHealth = async (req: Request, res: Response, next: NextFunction) => { + try { + const stuckQueues = await this.jobQueue.checkQueueHealth(); + return res.status(200).json({ + msg: "Queue health checked successfully", + data: stuckQueues, + }); + } catch (error) { + next(error); + } + }; +} +export default JobQueueController; From 846f1dfa6ccb5f38b04000957c1299d33101f941 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 Jan 2026 15:07:38 -0800 Subject: [PATCH 163/400] remove extra param --- server/src/controllers/v1/notificationController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/controllers/v1/notificationController.ts b/server/src/controllers/v1/notificationController.ts index 71f154a2e..74849c6aa 100644 --- a/server/src/controllers/v1/notificationController.ts +++ b/server/src/controllers/v1/notificationController.ts @@ -9,7 +9,7 @@ class NotificationController { static SERVICE_NAME = SERVICE_NAME; private db: any; private notificationService: any; - constructor(notificationService: any, statusService: any, db: any) { + constructor(notificationService: any, db: any) { this.notificationService = notificationService; this.db = db; } From 7b2ff593a4efacaa519ca2c9474000defba6a1b1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 Jan 2026 15:16:17 -0800 Subject: [PATCH 164/400] js->ts --- server/src/config/controllers.js | 5 +- ...ngsController.js => settingsController.ts} | 76 +++++++++---------- 2 files changed, 39 insertions(+), 42 deletions(-) rename server/src/controllers/v1/{settingsController.js => settingsController.ts} (58%) diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js index f3a1cf7b2..5f9cb14a1 100644 --- a/server/src/config/controllers.js +++ b/server/src/config/controllers.js @@ -30,10 +30,7 @@ export const initializeControllers = (services) => { controllers.monitorController = new MonitorController(services.monitorService); - controllers.settingsController = new SettingsController(commonDependencies, { - settingsService: services.settingsService, - emailService: services.emailService, - }); + controllers.settingsController = new SettingsController(services.settingsService, services.emailService, services.db); controllers.checkController = new CheckController(commonDependencies, { settingsService: services.settingsService, checkService: services.checkService, diff --git a/server/src/controllers/v1/settingsController.js b/server/src/controllers/v1/settingsController.ts similarity index 58% rename from server/src/controllers/v1/settingsController.js rename to server/src/controllers/v1/settingsController.ts index 3db73a05a..659e0200e 100644 --- a/server/src/controllers/v1/settingsController.js +++ b/server/src/controllers/v1/settingsController.ts @@ -1,28 +1,32 @@ -import { updateAppSettingsBodyValidation } from "../../validation/joi.js"; -import { sendTestEmailBodyValidation } from "../../validation/joi.js"; -import BaseController from "./baseController.js"; +import { Request, Response, NextFunction } from "express"; +import { sendTestEmailBodyValidation, updateAppSettingsBodyValidation } from "@/validation/joi.js"; +import { AppError } from "@/utils/AppError.js"; const SERVICE_NAME = "SettingsController"; -class SettingsController extends BaseController { +class SettingsController { static SERVICE_NAME = SERVICE_NAME; - constructor(commonDependencies, { settingsService, emailService }) { - super(commonDependencies); + private settingsService: any; + private emailService: any; + private db: any; + constructor(settingsService: any, emailService: any, db: any) { this.settingsService = settingsService; this.emailService = emailService; + this.db = db; } get serviceName() { return SettingsController.SERVICE_NAME; } - buildAppSettings = (dbSettings) => { + buildAppSettings = (dbSettings: any) => { const sanitizedSettings = { ...dbSettings }; delete sanitizedSettings.version; const returnSettings = { pagespeedKeySet: false, emailPasswordSet: false, + settings: null, }; if (typeof sanitizedSettings.pagespeedApiKey !== "undefined") { @@ -37,37 +41,33 @@ class SettingsController extends BaseController { return returnSettings; }; - getAppSettings = this.asyncHandler( - async (req, res) => { + getAppSettings = async (req: Request, res: Response, next: NextFunction) => { + try { const dbSettings = await this.settingsService.getDBSettings(); const returnSettings = this.buildAppSettings(dbSettings); - return res.success({ - msg: this.stringService.getAppSettings, + return res.status(200).json({ + msg: "App settings fetched successfully", data: returnSettings, }); - }, - SERVICE_NAME, - "getAppSettings" - ); + } catch (error) { + next(error); + } + }; - updateAppSettings = this.asyncHandler( - async (req, res) => { - await updateAppSettingsBodyValidation.validateAsync(req.body); + updateAppSettings = async (req: Request, res: Response, next: NextFunction) => { + await updateAppSettingsBodyValidation.validateAsync(req.body); - const updatedSettings = await this.db.settingsModule.updateAppSettings(req.body); - const returnSettings = this.buildAppSettings(updatedSettings); - return res.success({ - msg: this.stringService.updateAppSettings, - data: returnSettings, - }); - }, - SERVICE_NAME, - "updateAppSettings" - ); + const updatedSettings = await this.db.settingsModule.updateAppSettings(req.body); + const returnSettings = this.buildAppSettings(updatedSettings); + return res.status(200).json({ + msg: "App settings updated successfully", + data: returnSettings, + }); + }; - sendTestEmail = this.asyncHandler( - async (req, res) => { + sendTestEmail = async (req: Request, res: Response, next: NextFunction) => { + try { await sendTestEmailBodyValidation.validateAsync(req.body); const { @@ -86,7 +86,7 @@ class SettingsController extends BaseController { systemEmailTLSServername, } = req.body; - const subject = this.stringService.testEmailSubject; + const subject = "This is a test email from Checkmate"; const context = { testName: "Monitoring System" }; const html = await this.emailService.buildEmail("testEmailTemplate", context); @@ -106,17 +106,17 @@ class SettingsController extends BaseController { }); if (!messageId) { - throw this.errorService.createServerError("Failed to send test email."); + throw new AppError({ message: "Failed to send test email.", status: 500 }); } - return res.success({ - msg: this.stringService.sendTestEmail, + return res.status(200).json({ + msg: "Test email sent successfully", data: { messageId }, }); - }, - SERVICE_NAME, - "sendTestEmail" - ); + } catch (error) { + next(error); + } + }; } export default SettingsController; From 1d7f3b15a4275f9cfe786cd60ea5b41a8ba022d9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 Jan 2026 15:21:56 -0800 Subject: [PATCH 165/400] js->ts --- server/src/config/controllers.js | 2 +- .../controllers/v1/statusPageController.js | 108 ----------------- .../controllers/v1/statusPageController.ts | 109 ++++++++++++++++++ 3 files changed, 110 insertions(+), 109 deletions(-) delete mode 100644 server/src/controllers/v1/statusPageController.js create mode 100644 server/src/controllers/v1/statusPageController.ts diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js index 5f9cb14a1..7d29fbb71 100644 --- a/server/src/config/controllers.js +++ b/server/src/config/controllers.js @@ -45,7 +45,7 @@ export const initializeControllers = (services) => { }); controllers.queueController = new QueueController(services.jobQueue); controllers.logController = new LogController(commonDependencies); - controllers.statusPageController = new StatusPageController(commonDependencies); + controllers.statusPageController = new StatusPageController(services.db); controllers.notificationController = new NotificationController(services.notificationService, services.db); controllers.diagnosticController = new DiagnosticController(commonDependencies, { diagnosticService: services.diagnosticService, diff --git a/server/src/controllers/v1/statusPageController.js b/server/src/controllers/v1/statusPageController.js deleted file mode 100644 index de6a1ca7c..000000000 --- a/server/src/controllers/v1/statusPageController.js +++ /dev/null @@ -1,108 +0,0 @@ -import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation } from "../../validation/joi.js"; -import BaseController from "./baseController.js"; - -const SERVICE_NAME = "statusPageController"; - -class StatusPageController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - constructor(commonDependencies) { - super(commonDependencies); - } - - get serviceName() { - return StatusPageController.SERVICE_NAME; - } - - createStatusPage = this.asyncHandler( - async (req, res) => { - await createStatusPageBodyValidation.validateAsync(req.body); - await imageValidation.validateAsync(req.file); - - const { _id, teamId } = req.user; - const statusPage = await this.db.statusPageModule.createStatusPage({ - statusPageData: req.body, - image: req.file, - userId: _id, - teamId, - }); - return res.success({ - msg: this.stringService.statusPageCreate, - data: statusPage, - }); - }, - SERVICE_NAME, - "createStatusPage" - ); - - updateStatusPage = this.asyncHandler( - async (req, res) => { - await createStatusPageBodyValidation.validateAsync(req.body); - await imageValidation.validateAsync(req.file); - - const statusPage = await this.db.statusPageModule.updateStatusPage(req.body, req.file); - if (statusPage === null) { - throw this.errorService.createNotFoundError(this.stringService.statusPageNotFound); - } - return res.success({ - msg: this.stringService.statusPageUpdate, - data: statusPage, - }); - }, - SERVICE_NAME, - "updateStatusPage" - ); - - getStatusPage = this.asyncHandler( - async (req, res) => { - const statusPage = await this.db.statusPageModule.getStatusPage(); - return res.success({ - msg: this.stringService.statusPageByUrl, - data: statusPage, - }); - }, - SERVICE_NAME, - "getStatusPage" - ); - - getStatusPageByUrl = this.asyncHandler( - async (req, res) => { - await getStatusPageParamValidation.validateAsync(req.params); - await getStatusPageQueryValidation.validateAsync(req.query); - - const statusPage = await this.db.statusPageModule.getStatusPageByUrl(req.params.url); - return res.success({ - msg: this.stringService.statusPageByUrl, - data: statusPage, - }); - }, - SERVICE_NAME, - "getStatusPageByUrl" - ); - - getStatusPagesByTeamId = this.asyncHandler( - async (req, res) => { - const teamId = req.user.teamId; - const statusPages = await this.db.statusPageModule.getStatusPagesByTeamId(teamId); - - return res.success({ - msg: this.stringService.statusPageByTeamId, - data: statusPages, - }); - }, - SERVICE_NAME, - "getStatusPagesByTeamId" - ); - - deleteStatusPage = this.asyncHandler( - async (req, res) => { - await this.db.statusPageModule.deleteStatusPage(req.params.url); - return res.success({ - msg: this.stringService.statusPageDelete, - }); - }, - SERVICE_NAME, - "deleteStatusPage" - ); -} - -export default StatusPageController; diff --git a/server/src/controllers/v1/statusPageController.ts b/server/src/controllers/v1/statusPageController.ts new file mode 100644 index 000000000..79e0f5481 --- /dev/null +++ b/server/src/controllers/v1/statusPageController.ts @@ -0,0 +1,109 @@ +import { Request, Response, NextFunction } from "express"; + +import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation } from "@/validation/joi.js"; +import { AppError } from "@/utils/AppError.js"; + +const SERVICE_NAME = "statusPageController"; + +class StatusPageController { + static SERVICE_NAME = SERVICE_NAME; + private db: any; + constructor(db: any) { + this.db = db; + } + + get serviceName() { + return StatusPageController.SERVICE_NAME; + } + + createStatusPage = async (req: Request, res: Response, next: NextFunction) => { + try { + await createStatusPageBodyValidation.validateAsync(req.body); + await imageValidation.validateAsync(req.file); + + const { _id, teamId } = req.user; + const statusPage = await this.db.statusPageModule.createStatusPage({ + statusPageData: req.body, + image: req.file, + userId: _id, + teamId, + }); + return res.status(200).json({ + msg: "Status page created successfully", + data: statusPage, + }); + } catch (error) { + next(error); + } + }; + + updateStatusPage = async (req: Request, res: Response, next: NextFunction) => { + try { + await createStatusPageBodyValidation.validateAsync(req.body); + await imageValidation.validateAsync(req.file); + + const statusPage = await this.db.statusPageModule.updateStatusPage(req.body, req.file); + if (statusPage === null) { + throw new AppError({ message: "Status page not found", status: 404 }); + } + return res.status(200).json({ + msg: "Status page updated successfully", + data: statusPage, + }); + } catch (error) { + next(error); + } + }; + + getStatusPage = async (req: Request, res: Response, next: NextFunction) => { + try { + const statusPage = await this.db.statusPageModule.getStatusPage(); + return res.status(200).json({ + msg: "Status page retrieved successfully", + data: statusPage, + }); + } catch (error) { + next(error); + } + }; + + getStatusPageByUrl = async (req: Request, res: Response, next: NextFunction) => { + try { + await getStatusPageParamValidation.validateAsync(req.params); + await getStatusPageQueryValidation.validateAsync(req.query); + + const statusPage = await this.db.statusPageModule.getStatusPageByUrl(req.params.url); + return res.status(200).json({ + msg: "Status page retrieved successfully", + data: statusPage, + }); + } catch (error) { + next(error); + } + }; + getStatusPagesByTeamId = async (req: Request, res: Response, next: NextFunction) => { + try { + const teamId = req.user.teamId; + const statusPages = await this.db.statusPageModule.getStatusPagesByTeamId(teamId); + + return res.status(200).json({ + msg: "Status pages retrieved successfully", + data: statusPages, + }); + } catch (error) { + next(error); + } + }; + deleteStatusPage = async (req: Request, res: Response, next: NextFunction) => { + try { + await this.db.statusPageModule.deleteStatusPage(req.params.url); + return res.status(200).json({ + msg: "Status page deleted successfully", + }); + } catch (error) { + next(error); + } + }; +} + +export default StatusPageController; From 3ab4fcb2776b7cb7bce0972c5ec26cf7a9db0479 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 Jan 2026 15:32:53 -0800 Subject: [PATCH 166/400] js->ts --- server/src/config/controllers.js | 5 +- ...ller.js => maintenanceWindowController.ts} | 128 +++++++++--------- server/src/controllers/v1/queueController.ts | 6 + .../src/controllers/v1/settingsController.ts | 3 + .../controllers/v1/statusPageController.ts | 6 + 5 files changed, 82 insertions(+), 66 deletions(-) rename server/src/controllers/v1/{maintenanceWindowController.js => maintenanceWindowController.ts} (52%) diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js index 7d29fbb71..4c910763a 100644 --- a/server/src/config/controllers.js +++ b/server/src/config/controllers.js @@ -39,10 +39,7 @@ export const initializeControllers = (services) => { inviteService: services.inviteService, }); - controllers.maintenanceWindowController = new MaintenanceWindowController(commonDependencies, { - settingsService: services.settingsService, - maintenanceWindowService: services.maintenanceWindowService, - }); + controllers.maintenanceWindowController = new MaintenanceWindowController(services.maintenanceWindowService); controllers.queueController = new QueueController(services.jobQueue); controllers.logController = new LogController(commonDependencies); controllers.statusPageController = new StatusPageController(services.db); diff --git a/server/src/controllers/v1/maintenanceWindowController.js b/server/src/controllers/v1/maintenanceWindowController.ts similarity index 52% rename from server/src/controllers/v1/maintenanceWindowController.js rename to server/src/controllers/v1/maintenanceWindowController.ts index be373ace5..d850bdf82 100644 --- a/server/src/controllers/v1/maintenanceWindowController.js +++ b/server/src/controllers/v1/maintenanceWindowController.ts @@ -1,3 +1,4 @@ +import { AppError } from "@/utils/AppError.js"; import { createMaintenanceWindowBodyValidation, editMaintenanceWindowByIdParamValidation, @@ -6,16 +7,15 @@ import { getMaintenanceWindowsByMonitorIdParamValidation, getMaintenanceWindowsByTeamIdQueryValidation, deleteMaintenanceWindowByIdParamValidation, -} from "../../validation/joi.js"; -import BaseController from "./baseController.js"; +} from "@/validation/joi.js"; +import { Request, Response, NextFunction } from "express"; const SERVICE_NAME = "maintenanceWindowController"; -class MaintenanceWindowController extends BaseController { +class MaintenanceWindowController { static SERVICE_NAME = SERVICE_NAME; - constructor(commonDependencies, { settingsService, maintenanceWindowService }) { - super(commonDependencies); - this.settingsService = settingsService; + private maintenanceWindowService: any; + constructor(maintenanceWindowService: any) { this.maintenanceWindowService = maintenanceWindowService; } @@ -23,124 +23,128 @@ class MaintenanceWindowController extends BaseController { return MaintenanceWindowController.SERVICE_NAME; } - createMaintenanceWindows = this.asyncHandler( - async (req, res) => { + createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => { + try { await createMaintenanceWindowBodyValidation.validateAsync(req.body); const teamId = req?.user?.teamId; if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); + throw new AppError({ message: "Team ID is required", status: 400 }); } await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body }); - return res.success({ - msg: this.stringService.maintenanceWindowCreate, + return res.status(200).json({ + success: true, + msg: "Maintenance window created successfully", }); - }, - SERVICE_NAME, - "createMaintenanceWindows" - ); - - getMaintenanceWindowById = this.asyncHandler( - async (req, res) => { + } catch (error) { + next(error); + } + }; + getMaintenanceWindowById = async (req: Request, res: Response, next: NextFunction) => { + try { await getMaintenanceWindowByIdParamValidation.validateAsync(req.params); const teamId = req.user.teamId; if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); + throw new AppError({ message: "Team ID is required", status: 400 }); } const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id, teamId }); - return res.success({ - msg: this.stringService.maintenanceWindowGetById, + return res.status(200).json({ + success: true, + msg: "Maintenance window fetched successfully", data: maintenanceWindow, }); - }, - SERVICE_NAME, - "getMaintenanceWindowById" - ); + } catch (error) { + next(error); + } + }; - getMaintenanceWindowsByTeamId = this.asyncHandler( - async (req, res) => { + getMaintenanceWindowsByTeamId = async (req: Request, res: Response, next: NextFunction) => { + try { await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query); const teamId = req?.user?.teamId; if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); + throw new AppError({ message: "Team ID is required", status: 400 }); } const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ teamId, query: req.query }); - return res.success({ - msg: this.stringService.maintenanceWindowGetByTeam, + return res.status(200).json({ + success: true, + msg: "Maintenance windows fetched successfully", data: maintenanceWindows, }); - }, - SERVICE_NAME, - "getMaintenanceWindowsByTeamId" - ); + } catch (error) { + next(error); + } + }; - getMaintenanceWindowsByMonitorId = this.asyncHandler( - async (req, res) => { + getMaintenanceWindowsByMonitorId = async (req: Request, res: Response, next: NextFunction) => { + try { await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params); const teamId = req?.user?.teamId; if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); + throw new AppError({ message: "Team ID is required", status: 400 }); } const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ monitorId: req.params.monitorId, teamId }); - return res.success({ - msg: this.stringService.maintenanceWindowGetByUser, + return res.status(200).json({ + success: true, + msg: "Maintenance windows fetched successfully", data: maintenanceWindows, }); - }, - SERVICE_NAME, - "getMaintenanceWindowsByMonitorId" - ); - - deleteMaintenanceWindow = this.asyncHandler( - async (req, res) => { + } catch (error) { + next(error); + } + }; + deleteMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => { + try { await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params); const teamId = req?.user?.teamId; if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); + throw new AppError({ message: "Team ID is required", status: 400 }); } await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id, teamId }); - return res.success({ - msg: this.stringService.maintenanceWindowDelete, + return res.status(200).json({ + success: true, + msg: "Maintenance window deleted successfully", }); - }, - SERVICE_NAME, - "deleteMaintenanceWindow" - ); + } catch (error) { + next(error); + } + }; - editMaintenanceWindow = this.asyncHandler( - async (req, res) => { + editMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => { + try { await editMaintenanceWindowByIdParamValidation.validateAsync(req.params); await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body); const teamId = req.user.teamId; if (!teamId) { - throw this.errorService.createBadRequestError("Team ID is required"); + throw new AppError({ message: "Team ID is required", status: 400 }); } const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ id: req.params.id, body: req.body, teamId }); - return res.success({ - msg: this.stringService.maintenanceWindowEdit, + return res.status(200).json({ + success: true, + msg: "Maintenance window edited successfully", data: editedMaintenanceWindow, }); - }, - SERVICE_NAME, - "editMaintenanceWindow" - ); + } catch (error) { + next(error); + } + }; } export default MaintenanceWindowController; diff --git a/server/src/controllers/v1/queueController.ts b/server/src/controllers/v1/queueController.ts index cb39be826..9885bab87 100755 --- a/server/src/controllers/v1/queueController.ts +++ b/server/src/controllers/v1/queueController.ts @@ -17,6 +17,7 @@ class JobQueueController { try { const metrics = await this.jobQueue.getMetrics(); res.status(200).json({ + success: true, msg: "Queue metrics fetched successfully", data: metrics, }); @@ -29,6 +30,7 @@ class JobQueueController { try { const jobs = await this.jobQueue.getJobs(); return res.status(200).json({ + success: true, msg: "Queue jobs fetched successfully", data: jobs, }); @@ -42,6 +44,7 @@ class JobQueueController { const jobs = await this.jobQueue.getJobs(); const metrics = await this.jobQueue.getMetrics(); return res.status(200).json({ + success: true, msg: "Queue metrics fetched successfully", data: { jobs, metrics }, }); @@ -54,6 +57,7 @@ class JobQueueController { try { await this.jobQueue.addJob(Math.random().toString(36).substring(7)); return res.status(200).json({ + success: true, msg: "Job added to queue successfully", }); } catch (error) { @@ -65,6 +69,7 @@ class JobQueueController { try { const result = await this.jobQueue.flushQueues(); return res.status(200).json({ + success: true, msg: "Queue flushed successfully", data: result, }); @@ -77,6 +82,7 @@ class JobQueueController { try { const stuckQueues = await this.jobQueue.checkQueueHealth(); return res.status(200).json({ + success: true, msg: "Queue health checked successfully", data: stuckQueues, }); diff --git a/server/src/controllers/v1/settingsController.ts b/server/src/controllers/v1/settingsController.ts index 659e0200e..5d3f6ed0e 100644 --- a/server/src/controllers/v1/settingsController.ts +++ b/server/src/controllers/v1/settingsController.ts @@ -47,6 +47,7 @@ class SettingsController { const returnSettings = this.buildAppSettings(dbSettings); return res.status(200).json({ + success: true, msg: "App settings fetched successfully", data: returnSettings, }); @@ -61,6 +62,7 @@ class SettingsController { const updatedSettings = await this.db.settingsModule.updateAppSettings(req.body); const returnSettings = this.buildAppSettings(updatedSettings); return res.status(200).json({ + success: true, msg: "App settings updated successfully", data: returnSettings, }); @@ -110,6 +112,7 @@ class SettingsController { } return res.status(200).json({ + success: true, msg: "Test email sent successfully", data: { messageId }, }); diff --git a/server/src/controllers/v1/statusPageController.ts b/server/src/controllers/v1/statusPageController.ts index 79e0f5481..105beb230 100644 --- a/server/src/controllers/v1/statusPageController.ts +++ b/server/src/controllers/v1/statusPageController.ts @@ -29,6 +29,7 @@ class StatusPageController { teamId, }); return res.status(200).json({ + success: true, msg: "Status page created successfully", data: statusPage, }); @@ -47,6 +48,7 @@ class StatusPageController { throw new AppError({ message: "Status page not found", status: 404 }); } return res.status(200).json({ + success: true, msg: "Status page updated successfully", data: statusPage, }); @@ -59,6 +61,7 @@ class StatusPageController { try { const statusPage = await this.db.statusPageModule.getStatusPage(); return res.status(200).json({ + success: true, msg: "Status page retrieved successfully", data: statusPage, }); @@ -74,6 +77,7 @@ class StatusPageController { const statusPage = await this.db.statusPageModule.getStatusPageByUrl(req.params.url); return res.status(200).json({ + success: true, msg: "Status page retrieved successfully", data: statusPage, }); @@ -87,6 +91,7 @@ class StatusPageController { const statusPages = await this.db.statusPageModule.getStatusPagesByTeamId(teamId); return res.status(200).json({ + success: true, msg: "Status pages retrieved successfully", data: statusPages, }); @@ -98,6 +103,7 @@ class StatusPageController { try { await this.db.statusPageModule.deleteStatusPage(req.params.url); return res.status(200).json({ + success: true, msg: "Status page deleted successfully", }); } catch (error) { From dd03c554c95b81453c5d1e5c48c434c3b9362ff2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 Jan 2026 15:35:48 -0800 Subject: [PATCH 167/400] js->ts --- server/src/config/controllers.js | 2 +- server/src/controllers/v1/logController.js | 26 -------------------- server/src/controllers/v1/logController.ts | 28 ++++++++++++++++++++++ 3 files changed, 29 insertions(+), 27 deletions(-) delete mode 100644 server/src/controllers/v1/logController.js create mode 100644 server/src/controllers/v1/logController.ts diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js index 4c910763a..90b820bd6 100644 --- a/server/src/config/controllers.js +++ b/server/src/config/controllers.js @@ -41,7 +41,7 @@ export const initializeControllers = (services) => { controllers.maintenanceWindowController = new MaintenanceWindowController(services.maintenanceWindowService); controllers.queueController = new QueueController(services.jobQueue); - controllers.logController = new LogController(commonDependencies); + controllers.logController = new LogController(services.logger); controllers.statusPageController = new StatusPageController(services.db); controllers.notificationController = new NotificationController(services.notificationService, services.db); controllers.diagnosticController = new DiagnosticController(commonDependencies, { diff --git a/server/src/controllers/v1/logController.js b/server/src/controllers/v1/logController.js deleted file mode 100644 index 3537246bb..000000000 --- a/server/src/controllers/v1/logController.js +++ /dev/null @@ -1,26 +0,0 @@ -import BaseController from "./baseController.js"; -const SERVICE_NAME = "LogController"; - -class LogController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - constructor(commonDependencies) { - super(commonDependencies); - } - - get serviceName() { - return LogController.SERVICE_NAME; - } - - getLogs = this.asyncHandler( - async (req, res) => { - const logs = await this.logger.getLogs(); - res.success({ - msg: "Logs fetched successfully", - data: logs, - }); - }, - SERVICE_NAME, - "getLogs" - ); -} -export default LogController; diff --git a/server/src/controllers/v1/logController.ts b/server/src/controllers/v1/logController.ts new file mode 100644 index 000000000..d99de1e9c --- /dev/null +++ b/server/src/controllers/v1/logController.ts @@ -0,0 +1,28 @@ +import { Request, Response, NextFunction } from "express"; + +const SERVICE_NAME = "LogController"; + +class LogController { + static SERVICE_NAME = SERVICE_NAME; + private logger: any; + constructor(logger: any) { + this.logger = logger; + } + get serviceName() { + return LogController.SERVICE_NAME; + } + + getLogs = async (req: Request, res: Response, next: NextFunction) => { + try { + const logs = await this.logger.getLogs(); + res.status(200).json({ + success: true, + msg: "Logs fetched successfully", + data: logs, + }); + } catch (error) { + next(error); + } + }; +} +export default LogController; From 8ceac493ef24e6f62192e055f68bb2ebc90b2b67 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 Jan 2026 15:39:30 -0800 Subject: [PATCH 168/400] js -> ts --- server/src/config/controllers.js | 4 +- server/src/controllers/v1/inviteController.js | 101 ------------------ server/src/controllers/v1/inviteController.ts | 68 ++++++++++++ .../v1/maintenanceWindowController.ts | 2 +- 4 files changed, 70 insertions(+), 105 deletions(-) delete mode 100755 server/src/controllers/v1/inviteController.js create mode 100755 server/src/controllers/v1/inviteController.ts diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js index 90b820bd6..825d478aa 100644 --- a/server/src/config/controllers.js +++ b/server/src/config/controllers.js @@ -35,9 +35,7 @@ export const initializeControllers = (services) => { settingsService: services.settingsService, checkService: services.checkService, }); - controllers.inviteController = new InviteController(commonDependencies, { - inviteService: services.inviteService, - }); + controllers.inviteController = new InviteController(services.inviteService); controllers.maintenanceWindowController = new MaintenanceWindowController(services.maintenanceWindowService); controllers.queueController = new QueueController(services.jobQueue); diff --git a/server/src/controllers/v1/inviteController.js b/server/src/controllers/v1/inviteController.js deleted file mode 100755 index 1ec586f31..000000000 --- a/server/src/controllers/v1/inviteController.js +++ /dev/null @@ -1,101 +0,0 @@ -import { inviteBodyValidation, inviteVerificationBodyValidation } from "../../validation/joi.js"; -import BaseController from "./baseController.js"; -const SERVICE_NAME = "inviteController"; - -/** - * Controller for handling user invitation operations - * Manages invite token generation, email sending, and token verification - */ -class InviteController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - /** - * Creates a new InviteController instance - * @param {Object} commonDependencies - Common dependencies injected into the controller - * @param {Object} dependencies.inviteService - Service for invite-related operations - */ - constructor(commonDependencies, { inviteService }) { - super(commonDependencies); - this.inviteService = inviteService; - } - - get serviceName() { - return InviteController.SERVICE_NAME; - } - - /** - * Generates an invite token for a user invitation - * @param {Object} req - Express request object - * @param {Object} req.body - Request body containing invite details - * @param {Object} req.user - Authenticated user object - * @param {string} req.user.teamId - Team ID of the authenticated user - * @param {Object} res - Express response object - * @returns {Promise} Response with invite token data - */ - getInviteToken = this.asyncHandler( - async (req, res) => { - const invite = req.body; - const teamId = req?.user?.teamId; - invite.teamId = teamId; - await inviteBodyValidation.validateAsync(invite); - const inviteToken = await this.inviteService.getInviteToken({ invite, teamId }); - return res.success({ - msg: this.stringService.inviteIssued, - data: inviteToken, - }); - }, - SERVICE_NAME, - "getInviteToken" - ); - - /** - * Sends an invitation email to a user - * @param {Object} req - Express request object - * @param {Object} req.body - Request body containing invite details - * @param {Object} req.user - Authenticated user object - * @param {string} req.user.teamId - Team ID of the authenticated user - * @param {string} req.user.firstName - First name of the authenticated user - * @param {Object} res - Express response object - * @returns {Promise} Response with invite token data - */ - sendInviteEmail = this.asyncHandler( - async (req, res) => { - const inviteRequest = req.body; - inviteRequest.teamId = req?.user?.teamId; - await inviteBodyValidation.validateAsync(inviteRequest); - - const inviteToken = await this.inviteService.sendInviteEmail({ - inviteRequest, - firstName: req?.user?.firstName, - }); - return res.success({ - msg: this.stringService.inviteIssued, - data: inviteToken, - }); - }, - SERVICE_NAME, - "sendInviteEmail" - ); - - /** - * Verifies an invite token and returns invite details - * @param {Object} req - Express request object - * @param {Object} req.body - Request body containing the invite token - * @param {string} req.body.token - The invite token to verify - * @param {Object} res - Express response object - * @returns {Promise} Response with verified invite data - */ - verifyInviteToken = this.asyncHandler( - async (req, res) => { - await inviteVerificationBodyValidation.validateAsync(req.body); - const invite = await this.inviteService.verifyInviteToken({ inviteToken: req?.body?.token }); - return res.success({ - msg: this.stringService.inviteVerified, - data: invite, - }); - }, - SERVICE_NAME, - "verifyInviteToken" - ); -} - -export default InviteController; diff --git a/server/src/controllers/v1/inviteController.ts b/server/src/controllers/v1/inviteController.ts new file mode 100755 index 000000000..c3d63aa80 --- /dev/null +++ b/server/src/controllers/v1/inviteController.ts @@ -0,0 +1,68 @@ +import { Request, Response, NextFunction } from "express"; +import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/validation/joi.js"; +const SERVICE_NAME = "inviteController"; + +class InviteController { + static SERVICE_NAME = SERVICE_NAME; + private inviteService: any; + constructor(inviteService: any) { + this.inviteService = inviteService; + } + + get serviceName() { + return InviteController.SERVICE_NAME; + } + + getInviteToken = async (req: Request, res: Response, next: NextFunction) => { + try { + const invite = req.body; + const teamId = req?.user?.teamId; + invite.teamId = teamId; + await inviteBodyValidation.validateAsync(invite); + const inviteToken = await this.inviteService.getInviteToken({ invite, teamId }); + return res.status(200).json({ + success: true, + msg: "Invite token generated successfully", + data: inviteToken, + }); + } catch (error) { + next(error); + } + }; + + sendInviteEmail = async (req: Request, res: Response, next: NextFunction) => { + try { + const inviteRequest = req.body; + inviteRequest.teamId = req?.user?.teamId; + await inviteBodyValidation.validateAsync(inviteRequest); + + const inviteToken = await this.inviteService.sendInviteEmail({ + inviteRequest, + firstName: req?.user?.firstName, + }); + return res.status(200).json({ + success: true, + msg: "Invite issued successfully", + data: inviteToken, + }); + } catch (error) { + next(error); + } + }; + + verifyInviteToken = async (req: Request, res: Response, next: NextFunction) => { + try { + await inviteVerificationBodyValidation.validateAsync(req.body); + const invite = await this.inviteService.verifyInviteToken({ inviteToken: req?.body?.token }); + return res.status(200).json({ + success: true, + msg: "Invite verified successfully", + data: invite, + }); + } catch (error) { + next(error); + } + }; +} + +export default InviteController; diff --git a/server/src/controllers/v1/maintenanceWindowController.ts b/server/src/controllers/v1/maintenanceWindowController.ts index d850bdf82..afeba9219 100644 --- a/server/src/controllers/v1/maintenanceWindowController.ts +++ b/server/src/controllers/v1/maintenanceWindowController.ts @@ -1,3 +1,4 @@ +import { Request, Response, NextFunction } from "express"; import { AppError } from "@/utils/AppError.js"; import { createMaintenanceWindowBodyValidation, @@ -8,7 +9,6 @@ import { getMaintenanceWindowsByTeamIdQueryValidation, deleteMaintenanceWindowByIdParamValidation, } from "@/validation/joi.js"; -import { Request, Response, NextFunction } from "express"; const SERVICE_NAME = "maintenanceWindowController"; From ed77fc3a9dee836e288df551826025fa2cdadbbf Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 Jan 2026 15:44:11 -0800 Subject: [PATCH 169/400] js->ts --- server/src/config/controllers.js | 4 +- .../src/controllers/v1/incidentController.js | 191 ------------------ .../src/controllers/v1/incidentController.ts | 88 ++++++++ 3 files changed, 89 insertions(+), 194 deletions(-) delete mode 100644 server/src/controllers/v1/incidentController.js create mode 100644 server/src/controllers/v1/incidentController.ts diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js index 825d478aa..357c33a4b 100644 --- a/server/src/config/controllers.js +++ b/server/src/config/controllers.js @@ -46,9 +46,7 @@ export const initializeControllers = (services) => { diagnosticService: services.diagnosticService, }); - controllers.incidentController = new IncidentController(commonDependencies, { - incidentService: services.incidentService, - }); + controllers.incidentController = new IncidentController(services.incidentService); return controllers; }; diff --git a/server/src/controllers/v1/incidentController.js b/server/src/controllers/v1/incidentController.js deleted file mode 100644 index d20e3cdb8..000000000 --- a/server/src/controllers/v1/incidentController.js +++ /dev/null @@ -1,191 +0,0 @@ -import BaseController from "./baseController.js"; - -const SERVICE_NAME = "incidentController"; - -/** - * Incident Controller - * - * Handles all incident-related HTTP requests including retrieving incidents, - * resolving incidents manually, and getting incident summaries. - * - * @class IncidentController - * @description Manages incident operations and tracking - */ -class IncidentController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - - /** - * Creates an instance of IncidentController. - * - * @param {Object} commonDependencies - Common dependencies injected into the controller - * @param {Object} dependencies - The dependencies required by the controller - * @param {Object} dependencies.incidentService - Incident business logic service - */ - constructor(commonDependencies, { incidentService }) { - super(commonDependencies); - this.incidentService = incidentService; - } - - get serviceName() { - return IncidentController.SERVICE_NAME; - } - - /** - * Retrieves all incidents for the current user's team with filtering and pagination. - * - * @async - * @function getIncidentsByTeam - * @param {Object} req - Express request object - * @param {Object} req.query - Query parameters for filtering and pagination - * @param {string} [req.query.sortOrder] - Sort order (asc/desc) - * @param {string} [req.query.dateRange] - Date range filter - * @param {string} [req.query.filter] - General filter string - * @param {number} [req.query.page] - Page number for pagination - * @param {number} [req.query.rowsPerPage] - Number of rows per page - * @param {boolean} [req.query.status] - Filter by incident status (true=active, false=resolved) - * @param {string} [req.query.monitorId] - Filter by monitor ID - * @param {string} [req.query.resolutionType] - Filter by resolution type (automatic/manual) - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user.teamId - User's team ID - * @param {Object} res - Express response object - * @returns {Promise} Success response with incidents data - * @throws {Error} 422 - Validation error if query parameters are invalid - * @example - * GET /incidents/team?page=1&rowsPerPage=20&status=true&filter=active - * // Requires JWT authentication - */ - getIncidentsByTeam = this.asyncHandler( - async (req, res) => { - const result = await this.incidentService.getIncidentsByTeam({ - teamId: req?.user?.teamId, - query: req?.query, - }); - - return res.success({ - msg: "Incidents retrieved successfully", - data: result, - }); - }, - SERVICE_NAME, - "getIncidentsByTeam" - ); - - /** - * Retrieves a summary of incidents for the current user's team. - * - * @async - * @function getIncidentSummary - * @param {Object} req - Express request object - * @param {Object} req.query - Query parameters - * @param {number} [req.query.limit=10] - Number of latest incidents to return - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user.teamId - User's team ID - * @param {Object} res - Express response object - * @returns {Promise} Success response with incidents summary containing: - * - totalActive: Number of active incidents - * - avgResolutionTimeHours: Average time to resolve incidents in hours - * - topMonitor: Monitor with most incidents (monitorId, monitorName, incidentCount) - * - total: Total number of incidents - * - totalManualResolutions: Total incidents resolved manually - * - totalAutomaticResolutions: Total incidents resolved automatically - * - latestIncidents: Array of latest incidents created - * @example - * GET /incidents/team/summary?limit=5 - * // Requires JWT authentication - */ - getIncidentSummary = this.asyncHandler( - async (req, res) => { - const summary = await this.incidentService.getIncidentSummary({ - teamId: req?.user?.teamId, - query: req?.query, - }); - - return res.success({ - msg: "Incident summary retrieved successfully", - data: summary, - }); - }, - SERVICE_NAME, - "getIncidentSummary" - ); - - /** - * Retrieves a specific incident by ID. - * - * @async - * @function getIncidentById - * @param {Object} req - Express request object - * @param {Object} req.params - URL parameters - * @param {string} req.params.incidentId - ID of the incident to retrieve - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user.teamId - User's team ID - * @param {Object} res - Express response object - * @returns {Promise} Success response with incident data - * @throws {Error} 404 - Not found if incident doesn't exist - * @throws {Error} 403 - Forbidden if user doesn't have access to incident - * @example - * GET /incidents/507f1f77bcf86cd799439011 - * // Requires JWT authentication - */ - getIncidentById = this.asyncHandler( - async (req, res) => { - const incident = await this.incidentService.getIncidentById({ - incidentId: req?.params?.incidentId, - teamId: req?.user?.teamId, - }); - - return res.success({ - msg: "Incident retrieved successfully", - data: incident, - }); - }, - SERVICE_NAME, - "getIncidentById" - ); - - /** - * Manually resolves a specific incident by ID. - * - * @async - * @function resolveIncidentManually - * @param {Object} req - Express request object - * @param {Object} req.params - URL parameters - * @param {string} req.params.incidentId - ID of the incident to resolve - * @param {Object} req.body - Request body containing resolution data - * @param {string} [req.body.comment] - Optional comment about the resolution - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user.teamId - User's team ID - * @param {string} req.user._id - User's ID - * @param {Object} res - Express response object - * @returns {Promise} Success response with updated incident data - * @throws {Error} 422 - Validation error if request body is invalid - * @throws {Error} 404 - Not found if incident doesn't exist - * @throws {Error} 403 - Forbidden if user doesn't have access to incident - * @throws {Error} 400 - Bad request if incident is already resolved - * @example - * PUT /incidents/507f1f77bcf86cd799439011/resolve - * { - * "comment": "Issue resolved by restarting the service" - * } - * // Requires JWT authentication - */ - resolveIncidentManually = this.asyncHandler( - async (req, res) => { - const resolvedIncident = await this.incidentService.resolveIncidentManually({ - incidentId: req?.params?.incidentId, - userId: req?.user?._id, - teamId: req?.user?.teamId, - comment: req?.body?.comment, - }); - - return res.success({ - msg: "Incident resolved successfully", - data: resolvedIncident, - }); - }, - SERVICE_NAME, - "resolveIncidentManually" - ); -} - -export default IncidentController; diff --git a/server/src/controllers/v1/incidentController.ts b/server/src/controllers/v1/incidentController.ts new file mode 100644 index 000000000..8a56b5ef8 --- /dev/null +++ b/server/src/controllers/v1/incidentController.ts @@ -0,0 +1,88 @@ +import { Request, Response, NextFunction } from "express"; + +const SERVICE_NAME = "incidentController"; + +class IncidentController { + static SERVICE_NAME = SERVICE_NAME; + + private incidentService: any; + constructor(incidentService: any) { + this.incidentService = incidentService; + } + + get serviceName() { + return IncidentController.SERVICE_NAME; + } + + getIncidentsByTeam = async (req: Request, res: Response, next: NextFunction) => { + try { + const result = await this.incidentService.getIncidentsByTeam({ + teamId: req?.user?.teamId, + query: req?.query, + }); + + return res.status(200).json({ + success: true, + msg: "Incidents retrieved successfully", + data: result, + }); + } catch (error) { + next(error); + } + }; + + getIncidentSummary = async (req: Request, res: Response, next: NextFunction) => { + try { + const summary = await this.incidentService.getIncidentSummary({ + teamId: req?.user?.teamId, + query: req?.query, + }); + + return res.status(200).json({ + success: true, + msg: "Incident summary retrieved successfully", + data: summary, + }); + } catch (error) { + next(error); + } + }; + + getIncidentById = async (req: Request, res: Response, next: NextFunction) => { + try { + const incident = await this.incidentService.getIncidentById({ + incidentId: req?.params?.incidentId, + teamId: req?.user?.teamId, + }); + + return res.status(200).json({ + success: true, + msg: "Incident retrieved successfully", + data: incident, + }); + } catch (error) { + next(error); + } + }; + + resolveIncidentManually = async (req: Request, res: Response, next: NextFunction) => { + try { + const resolvedIncident = await this.incidentService.resolveIncidentManually({ + incidentId: req?.params?.incidentId, + userId: req?.user?._id, + teamId: req?.user?.teamId, + comment: req?.body?.comment, + }); + + return res.status(200).json({ + success: true, + msg: "Incident resolved successfully", + data: resolvedIncident, + }); + } catch (error) { + next(error); + } + }; +} + +export default IncidentController; From fee9b8a0de4bfc05f065f524987b63ea444ba7fe Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 Jan 2026 15:46:30 -0800 Subject: [PATCH 170/400] js->ts --- server/src/config/controllers.js | 4 +- .../controllers/v1/diagnosticController.js | 61 ------------------- .../controllers/v1/diagnosticController.ts | 31 ++++++++++ 3 files changed, 32 insertions(+), 64 deletions(-) delete mode 100755 server/src/controllers/v1/diagnosticController.js create mode 100755 server/src/controllers/v1/diagnosticController.ts diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js index 357c33a4b..199068f92 100644 --- a/server/src/config/controllers.js +++ b/server/src/config/controllers.js @@ -42,9 +42,7 @@ export const initializeControllers = (services) => { controllers.logController = new LogController(services.logger); controllers.statusPageController = new StatusPageController(services.db); controllers.notificationController = new NotificationController(services.notificationService, services.db); - controllers.diagnosticController = new DiagnosticController(commonDependencies, { - diagnosticService: services.diagnosticService, - }); + controllers.diagnosticController = new DiagnosticController(services.diagnosticService); controllers.incidentController = new IncidentController(services.incidentService); diff --git a/server/src/controllers/v1/diagnosticController.js b/server/src/controllers/v1/diagnosticController.js deleted file mode 100755 index 0723a9f6c..000000000 --- a/server/src/controllers/v1/diagnosticController.js +++ /dev/null @@ -1,61 +0,0 @@ -const SERVICE_NAME = "diagnosticController"; -import BaseController from "./baseController.js"; -/** - * Diagnostic Controller - * - * Handles system diagnostic and monitoring requests including system statistics, - * performance metrics, and health checks. - * - * @class DiagnosticController - * @description Manages system diagnostics and performance monitoring - */ -class DiagnosticController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - /** - * Creates an instance of DiagnosticController. - * @param {Object} commonDependencies - Common dependencies injected into the controller - * @param {Object} dependencies - The dependencies required by the controller - * @param {Object} dependencies.diagnosticService - Service for system diagnostics and monitoring - */ - constructor(commonDependencies, { diagnosticService }) { - super(commonDependencies); - this.diagnosticService = diagnosticService; - } - - get serviceName() { - return DiagnosticController.SERVICE_NAME; - } - /** - * Retrieves comprehensive system statistics and performance metrics. - * - * @async - * @function getSystemStats - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @returns {Promise} Success response with system diagnostics data - * @description Returns system performance metrics, memory usage, CPU statistics, - * and other diagnostic information useful for monitoring system health. - * @example - * GET /diagnostics/stats - * // Response includes: - * // - Memory usage (heap, external, arrayBuffers) - * // - CPU usage statistics - * // - System uptime - * // - Performance metrics - * // - Database connection status - * // - Active processes/connections - */ - getSystemStats = this.asyncHandler( - async (req, res) => { - const diagnostics = await this.diagnosticService.getSystemStats(); - return res.success({ - msg: "OK", - data: diagnostics, - }); - }, - SERVICE_NAME, - "getSystemStats" - ); -} - -export default DiagnosticController; diff --git a/server/src/controllers/v1/diagnosticController.ts b/server/src/controllers/v1/diagnosticController.ts new file mode 100755 index 000000000..b4d59ceaf --- /dev/null +++ b/server/src/controllers/v1/diagnosticController.ts @@ -0,0 +1,31 @@ +import { Request, Response, NextFunction } from "express"; + +const SERVICE_NAME = "diagnosticController"; +class DiagnosticController { + static SERVICE_NAME = SERVICE_NAME; + + private diagnosticService: any; + + constructor(diagnosticService: any) { + this.diagnosticService = diagnosticService; + } + + get serviceName() { + return DiagnosticController.SERVICE_NAME; + } + + getSystemStats = async (req: Request, res: Response, next: NextFunction) => { + try { + const diagnostics = await this.diagnosticService.getSystemStats(); + return res.status(200).json({ + success: true, + msg: "OK", + data: diagnostics, + }); + } catch (error) { + next(error); + } + }; +} + +export default DiagnosticController; From b24339f2327da635feaf50e299fcdc60cc6194d7 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 Jan 2026 15:53:53 -0800 Subject: [PATCH 171/400] js->ts --- server/src/config/controllers.js | 5 +- server/src/controllers/v1/checkController.js | 360 ------------------- server/src/controllers/v1/checkController.ts | 181 ++++++++++ 3 files changed, 182 insertions(+), 364 deletions(-) delete mode 100755 server/src/controllers/v1/checkController.js create mode 100755 server/src/controllers/v1/checkController.ts diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js index 199068f92..afd558930 100644 --- a/server/src/config/controllers.js +++ b/server/src/config/controllers.js @@ -31,10 +31,7 @@ export const initializeControllers = (services) => { controllers.monitorController = new MonitorController(services.monitorService); controllers.settingsController = new SettingsController(services.settingsService, services.emailService, services.db); - controllers.checkController = new CheckController(commonDependencies, { - settingsService: services.settingsService, - checkService: services.checkService, - }); + controllers.checkController = new CheckController(services.checkService); controllers.inviteController = new InviteController(services.inviteService); controllers.maintenanceWindowController = new MaintenanceWindowController(services.maintenanceWindowService); diff --git a/server/src/controllers/v1/checkController.js b/server/src/controllers/v1/checkController.js deleted file mode 100755 index ea293d74d..000000000 --- a/server/src/controllers/v1/checkController.js +++ /dev/null @@ -1,360 +0,0 @@ -import BaseController from "./baseController.js"; -import { - getChecksParamValidation, - getChecksQueryValidation, - getTeamChecksQueryValidation, - deleteChecksParamValidation, - deleteChecksByTeamIdParamValidation, - updateChecksTTLBodyValidation, - ackCheckBodyValidation, - ackAllChecksParamValidation, - ackAllChecksBodyValidation, -} from "../../validation/joi.js"; - -const SERVICE_NAME = "checkController"; - -/** - * Check Controller - * - * Handles all check-related HTTP requests including retrieving checks, - * acknowledging checks, deleting checks, and managing check TTL settings. - * - * @class CheckController - * @description Manages check operations and monitoring data - */ -class CheckController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - /** - * Creates an instance of CheckController. - * - * @param {Object} commonDependencies - Common dependencies injected into the controller - * @param {Object} dependencies - The dependencies required by the controller - * @param {Object} dependencies.settingsService - Service for application settings - * @param {Object} dependencies.checkService - Check business logic service - */ - constructor(commonDependencies, { settingsService, checkService }) { - super(commonDependencies); - this.settingsService = settingsService; - this.checkService = checkService; - } - - get serviceName() { - return CheckController.SERVICE_NAME; - } - - /** - * Retrieves checks for a specific monitor with filtering and pagination. - * - * @async - * @function getChecksByMonitor - * @param {Object} req - Express request object - * @param {Object} req.params - URL parameters - * @param {string} req.params.monitorId - ID of the monitor to get checks for - * @param {Object} req.query - Query parameters for filtering and pagination - * @param {string} [req.query.type] - Type of checks to filter by - * @param {string} [req.query.sortOrder] - Sort order (asc/desc) - * @param {string} [req.query.dateRange] - Date range filter - * @param {string} [req.query.filter] - General filter string - * @param {boolean} [req.query.ack] - Filter by acknowledgment status - * @param {number} [req.query.page] - Page number for pagination - * @param {number} [req.query.rowsPerPage] - Number of rows per page - * @param {string} [req.query.status] - Filter by check status - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user.teamId - User's team ID - * @param {Object} res - Express response object - * @returns {Promise} Success response with checks data - * @throws {Error} 422 - Validation error if parameters are invalid - * @throws {Error} 404 - Not found if monitor doesn't exist - * @throws {Error} 403 - Forbidden if user doesn't have access to monitor - * @example - * GET /checks/monitor/507f1f77bcf86cd799439011?page=1&rowsPerPage=10&status=down - * // Requires JWT authentication - */ - getChecksByMonitor = this.asyncHandler( - async (req, res) => { - await getChecksParamValidation.validateAsync(req.params); - await getChecksQueryValidation.validateAsync(req.query); - - const result = await this.checkService.getChecksByMonitor({ - monitorId: req?.params?.monitorId, - query: req?.query, - teamId: req?.user?.teamId, - }); - - return res.success({ - msg: this.stringService.checkGet, - data: result, - }); - }, - SERVICE_NAME, - "getChecksByMonitor" - ); - - /** - * Retrieves all checks for the current user's team with filtering and pagination. - * - * @async - * @function getChecksByTeam - * @param {Object} req - Express request object - * @param {Object} req.query - Query parameters for filtering and pagination - * @param {string} [req.query.sortOrder] - Sort order (asc/desc) - * @param {string} [req.query.dateRange] - Date range filter - * @param {string} [req.query.filter] - General filter string - * @param {boolean} [req.query.ack] - Filter by acknowledgment status - * @param {number} [req.query.page] - Page number for pagination - * @param {number} [req.query.rowsPerPage] - Number of rows per page - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user.teamId - User's team ID - * @param {Object} res - Express response object - * @returns {Promise} Success response with checks data - * @throws {Error} 422 - Validation error if query parameters are invalid - * @example - * GET /checks/team?page=1&rowsPerPage=20&status=down&ack=false - * // Requires JWT authentication - */ - getChecksByTeam = this.asyncHandler( - async (req, res) => { - await getTeamChecksQueryValidation.validateAsync(req.query); - const checkData = await this.checkService.getChecksByTeam({ - teamId: req?.user?.teamId, - query: req?.query, - }); - return res.success({ - msg: this.stringService.checkGet, - data: checkData, - }); - }, - SERVICE_NAME, - "getChecksByTeam" - ); - - /** - * Retrieves a summary of checks for the current user's team. - * - * @async - * @function getChecksSummaryByTeamId - * @param {Object} req - Express request object - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user.teamId - User's team ID - * @param {Object} res - Express response object - * @returns {Promise} Success response with checks summary - * @example - * GET /checks/summary - * // Requires JWT authentication - * // Response includes counts by status, time ranges, etc. - */ - getChecksSummaryByTeamId = this.asyncHandler( - async (req, res) => { - const summary = await this.checkService.getChecksSummaryByTeamId({ teamId: req?.user?.teamId }); - return res.success({ - msg: this.stringService.checkGetSummary, - data: summary, - }); - }, - SERVICE_NAME, - "getChecksSummaryByTeamId" - ); - - /** - * Acknowledges a specific check by ID. - * - * @async - * @function ackCheck - * @param {Object} req - Express request object - * @param {Object} req.params - URL parameters - * @param {string} req.params.checkId - ID of the check to acknowledge - * @param {Object} req.body - Request body containing acknowledgment data - * @param {boolean} req.body.ack - Acknowledgment status (true/false) - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user.teamId - User's team ID - * @param {Object} res - Express response object - * @returns {Promise} Success response with updated check data - * @throws {Error} 422 - Validation error if request body is invalid - * @throws {Error} 404 - Not found if check doesn't exist - * @throws {Error} 403 - Forbidden if user doesn't have access to check - * @example - * PUT /checks/507f1f77bcf86cd799439011/ack - * { - * "ack": true - * } - * // Requires JWT authentication - */ - ackCheck = this.asyncHandler( - async (req, res) => { - await ackCheckBodyValidation.validateAsync(req.body); - - const updatedCheck = await this.checkService.ackCheck({ - checkId: req?.params?.checkId, - teamId: req?.user?.teamId, - ack: req?.body?.ack, - }); - - return res.success({ - msg: this.stringService.checkUpdateStatus, - data: updatedCheck, - }); - }, - SERVICE_NAME, - "ackCheck" - ); - - /** - * Acknowledges all checks for a specific monitor or path. - * - * @async - * @function ackAllChecks - * @param {Object} req - Express request object - * @param {Object} req.params - URL parameters - * @param {string} req.params.monitorId - ID of the monitor - * @param {string} req.params.path - Path for acknowledgment (e.g., "monitor") - * @param {Object} req.body - Request body containing acknowledgment data - * @param {boolean} req.body.ack - Acknowledgment status (true/false) - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user.teamId - User's team ID - * @param {Object} res - Express response object - * @returns {Promise} Success response with updated checks data - * @throws {Error} 422 - Validation error if parameters or body are invalid - * @throws {Error} 404 - Not found if monitor doesn't exist - * @throws {Error} 403 - Forbidden if user doesn't have access to monitor - * @example - * PUT /checks/monitor/507f1f77bcf86cd799439011/ack - * { - * "ack": true - * } - * // Requires JWT authentication - */ - ackAllChecks = this.asyncHandler( - async (req, res) => { - await ackAllChecksParamValidation.validateAsync(req.params); - await ackAllChecksBodyValidation.validateAsync(req.body); - - const teamId = req?.user?.teamId; - if (!teamId) { - throw new Error("No team ID in request"); - } - - const updatedChecks = await this.checkService.ackAllChecks({ - monitorId: req?.params?.monitorId, - path: req?.params?.path, - teamId: req?.user?.teamId, - ack: req?.body?.ack, - }); - - return res.success({ - msg: this.stringService.checkUpdateStatus, - data: updatedChecks, - }); - }, - SERVICE_NAME, - "ackAllChecks" - ); - - /** - * Deletes all checks for a specific monitor. - * - * @async - * @function deleteChecks - * @param {Object} req - Express request object - * @param {Object} req.params - URL parameters - * @param {string} req.params.monitorId - ID of the monitor whose checks to delete - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user.teamId - User's team ID - * @param {Object} res - Express response object - * @returns {Promise} Success response with deletion count - * @throws {Error} 422 - Validation error if monitorId is invalid - * @throws {Error} 404 - Not found if monitor doesn't exist - * @throws {Error} 403 - Forbidden if user doesn't have access to monitor - * @example - * DELETE /checks/monitor/507f1f77bcf86cd799439011 - * // Requires JWT authentication - * // Response: { "data": { "deletedCount": 150 } } - */ - deleteChecks = this.asyncHandler( - async (req, res) => { - await deleteChecksParamValidation.validateAsync(req.params); - - const deletedCount = await this.checkService.deleteChecks({ - monitorId: req.params.monitorId, - teamId: req?.user?.teamId, - }); - - return res.success({ - msg: this.stringService.checkDelete, - data: { deletedCount }, - }); - }, - SERVICE_NAME, - "deleteChecks" - ); - - /** - * Deletes all checks for the current user's team. - * - * @async - * @function deleteChecksByTeamId - * @param {Object} req - Express request object - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user.teamId - User's team ID - * @param {Object} res - Express response object - * @returns {Promise} Success response with deletion count - * @throws {Error} 422 - Validation error if parameters are invalid - * @example - * DELETE /checks/team - * // Requires JWT authentication - * // Response: { "data": { "deletedCount": 1250 } } - */ - deleteChecksByTeamId = this.asyncHandler( - async (req, res) => { - await deleteChecksByTeamIdParamValidation.validateAsync(req.params); - - const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId: req?.user?.teamId }); - - return res.success({ - msg: this.stringService.checkDelete, - data: { deletedCount }, - }); - }, - SERVICE_NAME, - "deleteChecksByTeamId" - ); - - /** - * Updates the TTL (Time To Live) setting for checks in the current user's team. - * - * @async - * @function updateChecksTTL - * @param {Object} req - Express request object - * @param {Object} req.body - Request body containing TTL data - * @param {number} req.body.ttl - TTL value in days - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user.teamId - User's team ID - * @param {Object} res - Express response object - * @returns {Promise} Success response confirming TTL update - * @throws {Error} 422 - Validation error if TTL value is invalid - * @example - * PUT /checks/ttl - * { - * "ttl": 30 - * } - * // Requires JWT authentication - * // Sets check TTL to 30 days - */ - updateChecksTTL = this.asyncHandler( - async (req, res) => { - await updateChecksTTLBodyValidation.validateAsync(req.body); - - await this.checkService.updateChecksTTL({ - teamId: req?.user?.teamId, - ttl: req?.body?.ttl, - }); - - return res.success({ - msg: this.stringService.checkUpdateTTL, - }); - }, - SERVICE_NAME, - "updateChecksTtl" - ); -} - -export default CheckController; diff --git a/server/src/controllers/v1/checkController.ts b/server/src/controllers/v1/checkController.ts new file mode 100755 index 000000000..8ae6fd785 --- /dev/null +++ b/server/src/controllers/v1/checkController.ts @@ -0,0 +1,181 @@ +import { Request, Response, NextFunction } from "express"; + +import { + getChecksParamValidation, + getChecksQueryValidation, + getTeamChecksQueryValidation, + deleteChecksParamValidation, + deleteChecksByTeamIdParamValidation, + updateChecksTTLBodyValidation, + ackCheckBodyValidation, + ackAllChecksParamValidation, + ackAllChecksBodyValidation, +} from "@/validation/joi.js"; + +const SERVICE_NAME = "checkController"; + +class CheckController { + static SERVICE_NAME = SERVICE_NAME; + + private checkService: any; + constructor(checkService: any) { + this.checkService = checkService; + } + + get serviceName() { + return CheckController.SERVICE_NAME; + } + + getChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => { + try { + await getChecksParamValidation.validateAsync(req.params); + await getChecksQueryValidation.validateAsync(req.query); + + const result = await this.checkService.getChecksByMonitor({ + monitorId: req?.params?.monitorId, + query: req?.query, + teamId: req?.user?.teamId, + }); + + return res.status(200).json({ + success: true, + msg: "Checks retrieved successfully", + data: result, + }); + } catch (error) { + next(error); + } + }; + + getChecksByTeam = async (req: Request, res: Response, next: NextFunction) => { + try { + await getTeamChecksQueryValidation.validateAsync(req.query); + const checkData = await this.checkService.getChecksByTeam({ + teamId: req?.user?.teamId, + query: req?.query, + }); + return res.status(200).json({ + success: true, + msg: "Team checks retrieved successfully", + data: checkData, + }); + } catch (error) { + next(error); + } + }; + + getChecksSummaryByTeamId = async (req: Request, res: Response, next: NextFunction) => { + try { + const summary = await this.checkService.getChecksSummaryByTeamId({ teamId: req?.user?.teamId }); + return res.status(200).json({ + success: true, + msg: "Checks summary retrieved successfully", + data: summary, + }); + } catch (error) { + next(error); + } + }; + + ackCheck = async (req: Request, res: Response, next: NextFunction) => { + try { + await ackCheckBodyValidation.validateAsync(req.body); + + const updatedCheck = await this.checkService.ackCheck({ + checkId: req?.params?.checkId, + teamId: req?.user?.teamId, + ack: req?.body?.ack, + }); + + return res.status(200).json({ + success: true, + msg: "Check acknowledged successfully", + data: updatedCheck, + }); + } catch (error) { + next(error); + } + }; + + ackAllChecks = async (req: Request, res: Response, next: NextFunction) => { + try { + await ackAllChecksParamValidation.validateAsync(req.params); + await ackAllChecksBodyValidation.validateAsync(req.body); + + const teamId = req?.user?.teamId; + if (!teamId) { + throw new Error("No team ID in request"); + } + + const updatedChecks = await this.checkService.ackAllChecks({ + monitorId: req?.params?.monitorId, + path: req?.params?.path, + teamId: req?.user?.teamId, + ack: req?.body?.ack, + }); + + return res.status(200).json({ + success: true, + msg: "All checks acknowledged successfully", + data: updatedChecks, + }); + } catch (error) { + next(error); + } + }; + + deleteChecks = async (req: Request, res: Response, next: NextFunction) => { + try { + await deleteChecksParamValidation.validateAsync(req.params); + + const deletedCount = await this.checkService.deleteChecks({ + monitorId: req.params.monitorId, + teamId: req?.user?.teamId, + }); + + return res.status(200).json({ + success: true, + msg: "Checks deleted successfully", + data: { deletedCount }, + }); + } catch (error) { + next(error); + } + }; + + deleteChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { + try { + await deleteChecksByTeamIdParamValidation.validateAsync(req.params); + + const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId: req?.user?.teamId }); + + return res.status(200).json({ + success: true, + msg: "Checks deleted successfully", + data: { deletedCount }, + }); + } catch (error) { + next(error); + } + }; + + updateChecksTTL = async (req: Request, res: Response, next: NextFunction) => { + try { + await updateChecksTTLBodyValidation.validateAsync(req.body); + + await this.checkService.updateChecksTTL({ + teamId: req?.user?.teamId, + ttl: req?.body?.ttl, + }); + + return res.status(200).json({ + success: true, + msg: "Checks TTL updated successfully", + }); + } catch (error) { + next(error); + } + }; +} + +export default CheckController; From bdb07220ed88e10e743c0f9ba17ba73198a325bb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 9 Jan 2026 15:58:31 -0800 Subject: [PATCH 172/400] js -> ts --- .../controllers/v1/{controllerUtils.js => controllerUtils.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename server/src/controllers/v1/{controllerUtils.js => controllerUtils.ts} (79%) diff --git a/server/src/controllers/v1/controllerUtils.js b/server/src/controllers/v1/controllerUtils.ts similarity index 79% rename from server/src/controllers/v1/controllerUtils.js rename to server/src/controllers/v1/controllerUtils.ts index f21ba154b..4d9f9a279 100755 --- a/server/src/controllers/v1/controllerUtils.js +++ b/server/src/controllers/v1/controllerUtils.ts @@ -1,4 +1,4 @@ -const fetchMonitorCertificate = async (sslChecker, monitor) => { +const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise => { const monitorUrl = new URL(monitor.url); const hostname = monitorUrl.hostname; const cert = await sslChecker(hostname); From fb3795bd7058eb4b1e1ab1ad7fb95c979ac0dd84 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sat, 10 Jan 2026 12:15:29 +0000 Subject: [PATCH 173/400] 3120 remove URL's from data when they are supposed to be disabled --- server/src/config/services.js | 2 +- server/src/db/v1/modules/statusPageModule.js | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/server/src/config/services.js b/server/src/config/services.js index 9744c647d..6d6a613d3 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -83,7 +83,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService // Create DB const checkModule = new CheckModule({ logger, Check, Monitor, User }); const inviteModule = new InviteModule({ InviteToken, crypto, stringService }); - const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService }); + const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService, AppSettings }); const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); const monitorModule = new MonitorModule({ diff --git a/server/src/db/v1/modules/statusPageModule.js b/server/src/db/v1/modules/statusPageModule.js index aadde0c81..c9027125a 100755 --- a/server/src/db/v1/modules/statusPageModule.js +++ b/server/src/db/v1/modules/statusPageModule.js @@ -1,4 +1,3 @@ -// import StatusPage from "../../models/StatusPage.js"; // import { NormalizeData } from "../../../utils/dataUtils.js"; // import ServiceRegistry from "../../../service/system/serviceRegistry.js"; // import StringService from "../../../service/system/stringService.js"; @@ -6,10 +5,11 @@ const SERVICE_NAME = "statusPageModule"; class StatusPageModule { - constructor({ StatusPage, NormalizeData, stringService }) { + constructor({ StatusPage, NormalizeData, stringService, AppSettings }) { this.StatusPage = StatusPage; this.NormalizeData = NormalizeData; this.stringService = stringService; + this.AppSettings = AppSettings; } createStatusPage = async ({ statusPageData, image, userId, teamId }) => { @@ -217,7 +217,7 @@ class StatusPageModule { showUptimePercentage: 1, timezone: 1, showAdminLoginLink: 1, - url: 1, + url: { $cond: [{ $eq: ["$statusPage.showMonitorUrl", true] }, "$monitors.url", "$$REMOVE"] }, }, monitors: { _id: 1, @@ -260,11 +260,16 @@ class StatusPageModule { const { statusPage, monitors } = statusPageQuery[0]; + const appSettings = await this.AppSettings.findOne({ singleton: true }).lean(); + const showURL = appSettings?.showURL === true; + const normalizedMonitors = monitors.map((monitor) => { - return { - ...monitor, - checks: this.NormalizeData(monitor.checks, 10, 100), - }; + const normalizedChecks = this.NormalizeData(monitor.checks, 10, 100); + if (showURL !== true) { + const { url, ...rest } = monitor; + return { ...rest, checks: normalizedChecks }; + } + return { ...monitor, checks: normalizedChecks }; }); return { statusPage, monitors: normalizedMonitors }; From ab4cc80628528cae3b65c69a15b22e972c5e6095 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sat, 10 Jan 2026 21:29:15 +0000 Subject: [PATCH 174/400] 3120 persist admin value --- client/src/Pages/v1/Settings/index.jsx | 10 +++++----- server/src/controllers/v1/settingsController.ts | 4 ++++ server/src/db/v1/modules/statusPageModule.js | 12 ++++++------ server/src/service/v1/system/settingsService.js | 1 + server/src/validation/joi.js | 2 +- 5 files changed, 17 insertions(+), 12 deletions(-) diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index b6137121b..2cb794b77 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -80,11 +80,6 @@ const Settings = () => { const handleChange = async (e) => { const { name, value, checked } = e.target; - // Special case for showURL until handled properly in the backend - if (name === "showURL") { - dispatch(setShowURL(value)); - return; - } let newValue; if ( name === "systemEmailIgnoreTLS" || @@ -167,6 +162,11 @@ const Settings = () => { } setSettingsData(newSettingsData); + + // Update Redux immediately for UI feedback + if (name === "showURL") { + dispatch(setShowURL(newValue ?? value)); + } }; const handleSave = () => { diff --git a/server/src/controllers/v1/settingsController.ts b/server/src/controllers/v1/settingsController.ts index 5d3f6ed0e..30b4cf8ea 100644 --- a/server/src/controllers/v1/settingsController.ts +++ b/server/src/controllers/v1/settingsController.ts @@ -57,6 +57,10 @@ class SettingsController { }; updateAppSettings = async (req: Request, res: Response, next: NextFunction) => { + // Diagnostics: capture inbound payload + // eslint-disable-next-line no-console + console.log("=== SETTINGS CONTROLLER REQUEST BODY ===", req.body); + await updateAppSettingsBodyValidation.validateAsync(req.body); const updatedSettings = await this.db.settingsModule.updateAppSettings(req.body); diff --git a/server/src/db/v1/modules/statusPageModule.js b/server/src/db/v1/modules/statusPageModule.js index c9027125a..48c46ad4c 100755 --- a/server/src/db/v1/modules/statusPageModule.js +++ b/server/src/db/v1/modules/statusPageModule.js @@ -217,7 +217,7 @@ class StatusPageModule { showUptimePercentage: 1, timezone: 1, showAdminLoginLink: 1, - url: { $cond: [{ $eq: ["$statusPage.showMonitorUrl", true] }, "$monitors.url", "$$REMOVE"] }, + url: 1, }, monitors: { _id: 1, @@ -261,12 +261,12 @@ class StatusPageModule { const { statusPage, monitors } = statusPageQuery[0]; const appSettings = await this.AppSettings.findOne({ singleton: true }).lean(); - const showURL = appSettings?.showURL === true; + const showURL = appSettings?.showURL === true || appSettings?.showURL === "true"; - const normalizedMonitors = monitors.map((monitor) => { - const normalizedChecks = this.NormalizeData(monitor.checks, 10, 100); - if (showURL !== true) { - const { url, ...rest } = monitor; + const normalizedMonitors = monitors.map((monitor) => { + const normalizedChecks = this.NormalizeData(monitor.checks, 10, 100); + if (showURL !== true) { + const { url, port, secret, notifications, ...rest } = monitor; return { ...rest, checks: normalizedChecks }; } return { ...monitor, checks: normalizedChecks }; diff --git a/server/src/service/v1/system/settingsService.js b/server/src/service/v1/system/settingsService.js index 70d8d192c..cea46f7ed 100755 --- a/server/src/service/v1/system/settingsService.js +++ b/server/src/service/v1/system/settingsService.js @@ -60,6 +60,7 @@ class SettingsService { await this.AppSettings.create({}); settings = await this.AppSettings.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean(); } + return settings; } } diff --git a/server/src/validation/joi.js b/server/src/validation/joi.js index b3342de83..d0e1aeeb3 100755 --- a/server/src/validation/joi.js +++ b/server/src/validation/joi.js @@ -413,7 +413,7 @@ const updateAppSettingsBodyValidation = joi.object({ pagespeedApiKey: joi.string().allow(""), language: joi.string().allow(""), timezone: joi.string().allow(""), - // showURL: joi.bool().required(), + showURL: joi.bool().optional(), systemEmailHost: joi.string().allow(""), systemEmailPort: joi.number().allow(""), systemEmailAddress: joi.string().allow(""), From 643a743d707d04f1eab2ec3915dad9ed9b15b109 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sat, 10 Jan 2026 21:49:10 +0000 Subject: [PATCH 175/400] more logging --- client/src/Pages/v1/Settings/index.jsx | 5 ++++- server/src/db/v1/modules/statusPageModule.js | 21 +++++++++++++++----- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index 2cb794b77..69369daed 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -80,7 +80,8 @@ const Settings = () => { const handleChange = async (e) => { const { name, value, checked } = e.target; - let newValue; + // eslint-disable-next-line no-console + console.log("=== SETTINGS HANDLER CHANGE ===", { name, value, checked, type: typeof value }); if ( name === "systemEmailIgnoreTLS" || name === "systemEmailRequireTLS" || @@ -170,6 +171,8 @@ const Settings = () => { }; const handleSave = () => { + // eslint-disable-next-line no-console + console.log("=== HANDLE SAVE ===", { settingsDataSettings: settingsData?.settings }); const { error } = settingsValidation.validate(settingsData.settings, { abortEarly: false, }); diff --git a/server/src/db/v1/modules/statusPageModule.js b/server/src/db/v1/modules/statusPageModule.js index 48c46ad4c..48622c5d6 100755 --- a/server/src/db/v1/modules/statusPageModule.js +++ b/server/src/db/v1/modules/statusPageModule.js @@ -263,16 +263,27 @@ class StatusPageModule { const appSettings = await this.AppSettings.findOne({ singleton: true }).lean(); const showURL = appSettings?.showURL === true || appSettings?.showURL === "true"; + // eslint-disable-next-line no-console + console.log("=== STATUS PAGE FETCH ==="); + // eslint-disable-next-line no-console + console.log("appSettings object:", JSON.stringify(appSettings, null, 2)); + // eslint-disable-next-line no-console + console.log("appSettings.showURL value:", appSettings?.showURL); + // eslint-disable-next-line no-console + console.log("appSettings.showURL type:", typeof appSettings?.showURL); + // eslint-disable-next-line no-console + console.log("showURL after check:", showURL); + const normalizedMonitors = monitors.map((monitor) => { const normalizedChecks = this.NormalizeData(monitor.checks, 10, 100); if (showURL !== true) { const { url, port, secret, notifications, ...rest } = monitor; - return { ...rest, checks: normalizedChecks }; - } - return { ...monitor, checks: normalizedChecks }; - }); + return { ...rest, checks: normalizedChecks }; + } + return { ...monitor, checks: normalizedChecks }; + }); - return { statusPage, monitors: normalizedMonitors }; + return { statusPage, monitors: normalizedMonitors }; } catch (error) { error.service = SERVICE_NAME; error.method = "getStatusPageByUrl"; From f659876845a5ae98ad39b5710901a2e6394bc1ee Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sat, 10 Jan 2026 21:58:13 +0000 Subject: [PATCH 176/400] select bug --- client/src/Pages/v1/Settings/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index 69369daed..b50c1887a 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -166,7 +166,7 @@ const Settings = () => { // Update Redux immediately for UI feedback if (name === "showURL") { - dispatch(setShowURL(newValue ?? value)); + dispatch(setShowURL(value)); } }; From a13f6a34c46e3b5ebb81d7021a43e34949fa6424 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sat, 10 Jan 2026 22:02:36 +0000 Subject: [PATCH 177/400] missing value --- client/src/Pages/v1/Settings/index.jsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index b50c1887a..a1335777c 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -82,6 +82,8 @@ const Settings = () => { // eslint-disable-next-line no-console console.log("=== SETTINGS HANDLER CHANGE ===", { name, value, checked, type: typeof value }); + + let newValue; if ( name === "systemEmailIgnoreTLS" || name === "systemEmailRequireTLS" || From 526fa24eab7b00d9d0af030d04afe8abf24b36b3 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sat, 10 Jan 2026 22:23:31 +0000 Subject: [PATCH 178/400] validation bug --- client/src/Validation/validation.js | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 112bbb349..4663afd37 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -328,6 +328,7 @@ const settingsValidation = joi.object({ systemEmailIgnoreTLS: joi.boolean(), systemEmailRequireTLS: joi.boolean(), systemEmailRejectUnauthorized: joi.boolean(), + showURL: joi.boolean().optional(), globalThresholds: joi .object({ cpu: joi.number().min(1).max(100).allow("").optional(), From 12b28ffb3f9858ac24eea269e6b9622e88d1b819 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sat, 10 Jan 2026 22:30:03 +0000 Subject: [PATCH 179/400] type issue --- client/src/Pages/v1/Settings/index.jsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index a1335777c..a0a99d943 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -93,6 +93,12 @@ const Settings = () => { ) { newValue = checked; } + + // Ensure showURL is a proper boolean + if (name === "showURL") { + newValue = value === true || value === "true"; + } + // Build next state early const newSettingsData = { ...settingsData, From 418068e70298b88edba1ae0db9df27eecf864fbc Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sun, 11 Jan 2026 18:18:38 +0000 Subject: [PATCH 180/400] loggin settings --- server/src/db/v1/modules/settingsModule.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/server/src/db/v1/modules/settingsModule.js b/server/src/db/v1/modules/settingsModule.js index 37b851bf2..35622c4c2 100755 --- a/server/src/db/v1/modules/settingsModule.js +++ b/server/src/db/v1/modules/settingsModule.js @@ -8,6 +8,11 @@ class SettingsModule { updateAppSettings = async (newSettings) => { try { + // eslint-disable-next-line no-console + console.log("=== SETTINGS MODULE UPDATE ==="); + // eslint-disable-next-line no-console + console.log("Incoming showURL:", newSettings.showURL, "type:", typeof newSettings.showURL); + const update = { $set: { ...newSettings } }; if (newSettings.pagespeedApiKey === "") { @@ -24,6 +29,8 @@ class SettingsModule { upsert: true, }); const settings = await this.AppSettings.findOne().select("-__v -_id -createdAt -updatedAt -singleton").lean(); + // eslint-disable-next-line no-console + console.log("Saved showURL:", settings.showURL, "type:", typeof settings.showURL); return settings; } catch (error) { error.service = SERVICE_NAME; From 76ab2695db585572e23ef95236bf07abc1862f1e Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sun, 11 Jan 2026 18:23:58 +0000 Subject: [PATCH 181/400] missing setting --- server/src/db/v1/models/AppSettings.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/server/src/db/v1/models/AppSettings.js b/server/src/db/v1/models/AppSettings.js index 2313d638c..a1a9964f2 100755 --- a/server/src/db/v1/models/AppSettings.js +++ b/server/src/db/v1/models/AppSettings.js @@ -55,6 +55,10 @@ const AppSettingsSchema = mongoose.Schema( type: Boolean, default: true, }, + showURL: { + type: Boolean, + default: false, + }, singleton: { type: Boolean, required: true, From 7b619e6b313343a7f50219ccffd835f4ea1c0e14 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sun, 11 Jan 2026 18:56:16 +0000 Subject: [PATCH 182/400] Remove debug logging and add feature documentation - Remove all console.log statements added during development - Clean up code for production readiness --- client/src/Pages/v1/Settings/index.jsx | 5 ----- server/src/controllers/v1/settingsController.ts | 4 ---- server/src/db/v1/modules/settingsModule.js | 7 ------- server/src/db/v1/modules/statusPageModule.js | 15 ++------------- 4 files changed, 2 insertions(+), 29 deletions(-) diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index a0a99d943..907479b6a 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -80,9 +80,6 @@ const Settings = () => { const handleChange = async (e) => { const { name, value, checked } = e.target; - // eslint-disable-next-line no-console - console.log("=== SETTINGS HANDLER CHANGE ===", { name, value, checked, type: typeof value }); - let newValue; if ( name === "systemEmailIgnoreTLS" || @@ -179,8 +176,6 @@ const Settings = () => { }; const handleSave = () => { - // eslint-disable-next-line no-console - console.log("=== HANDLE SAVE ===", { settingsDataSettings: settingsData?.settings }); const { error } = settingsValidation.validate(settingsData.settings, { abortEarly: false, }); diff --git a/server/src/controllers/v1/settingsController.ts b/server/src/controllers/v1/settingsController.ts index 30b4cf8ea..5d3f6ed0e 100644 --- a/server/src/controllers/v1/settingsController.ts +++ b/server/src/controllers/v1/settingsController.ts @@ -57,10 +57,6 @@ class SettingsController { }; updateAppSettings = async (req: Request, res: Response, next: NextFunction) => { - // Diagnostics: capture inbound payload - // eslint-disable-next-line no-console - console.log("=== SETTINGS CONTROLLER REQUEST BODY ===", req.body); - await updateAppSettingsBodyValidation.validateAsync(req.body); const updatedSettings = await this.db.settingsModule.updateAppSettings(req.body); diff --git a/server/src/db/v1/modules/settingsModule.js b/server/src/db/v1/modules/settingsModule.js index 35622c4c2..37b851bf2 100755 --- a/server/src/db/v1/modules/settingsModule.js +++ b/server/src/db/v1/modules/settingsModule.js @@ -8,11 +8,6 @@ class SettingsModule { updateAppSettings = async (newSettings) => { try { - // eslint-disable-next-line no-console - console.log("=== SETTINGS MODULE UPDATE ==="); - // eslint-disable-next-line no-console - console.log("Incoming showURL:", newSettings.showURL, "type:", typeof newSettings.showURL); - const update = { $set: { ...newSettings } }; if (newSettings.pagespeedApiKey === "") { @@ -29,8 +24,6 @@ class SettingsModule { upsert: true, }); const settings = await this.AppSettings.findOne().select("-__v -_id -createdAt -updatedAt -singleton").lean(); - // eslint-disable-next-line no-console - console.log("Saved showURL:", settings.showURL, "type:", typeof settings.showURL); return settings; } catch (error) { error.service = SERVICE_NAME; diff --git a/server/src/db/v1/modules/statusPageModule.js b/server/src/db/v1/modules/statusPageModule.js index 48622c5d6..870a1ee55 100755 --- a/server/src/db/v1/modules/statusPageModule.js +++ b/server/src/db/v1/modules/statusPageModule.js @@ -261,20 +261,9 @@ class StatusPageModule { const { statusPage, monitors } = statusPageQuery[0]; const appSettings = await this.AppSettings.findOne({ singleton: true }).lean(); - const showURL = appSettings?.showURL === true || appSettings?.showURL === "true"; + const showURL = appSettings?.showURL === true; - // eslint-disable-next-line no-console - console.log("=== STATUS PAGE FETCH ==="); - // eslint-disable-next-line no-console - console.log("appSettings object:", JSON.stringify(appSettings, null, 2)); - // eslint-disable-next-line no-console - console.log("appSettings.showURL value:", appSettings?.showURL); - // eslint-disable-next-line no-console - console.log("appSettings.showURL type:", typeof appSettings?.showURL); - // eslint-disable-next-line no-console - console.log("showURL after check:", showURL); - - const normalizedMonitors = monitors.map((monitor) => { + const normalizedMonitors = monitors.map((monitor) => { const normalizedChecks = this.NormalizeData(monitor.checks, 10, 100); if (showURL !== true) { const { url, port, secret, notifications, ...rest } = monitor; From 7556086f713a86c2358207fd8f7927333feb9a7c Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sun, 11 Jan 2026 20:51:25 +0000 Subject: [PATCH 183/400] code review updates Summary of what we fixed based on AI review feedback: Formatting - Ran Prettier to fix code formatting Redux state rollback - Moved Redux dispatch to after successful save in settingsHooks.js Performance - Added caching layer in settingsModule (1-minute TTL) to prevent DB query on every status page request Architecture - Inject settingsModule instead of raw AppSettings model for better encapsulation --- client/src/Hooks/v1/settingsHooks.js | 8 ++++++ client/src/Pages/v1/Settings/index.jsx | 5 ---- server/src/config/services.js | 4 +-- server/src/db/v1/modules/settingsModule.js | 26 ++++++++++++++++++++ server/src/db/v1/modules/statusPageModule.js | 22 ++++++++--------- 5 files changed, 47 insertions(+), 18 deletions(-) diff --git a/client/src/Hooks/v1/settingsHooks.js b/client/src/Hooks/v1/settingsHooks.js index 787e46ff9..ac754ca69 100644 --- a/client/src/Hooks/v1/settingsHooks.js +++ b/client/src/Hooks/v1/settingsHooks.js @@ -56,6 +56,14 @@ const useSaveSettings = ({ setEmailPasswordHasBeenReset(false); } setSettingsData(settingsResponse.data.data); + + // Update Redux state after successful save + if (typeof settings.showURL !== "undefined") { + const { setShowURL } = await import("../../Features/UI/uiSlice.js"); + const { default: store } = await import("../../store.js"); + store.dispatch(setShowURL(settings.showURL)); + } + createToast({ body: t("settingsSuccessSaved") }); } catch (error) { createToast({ body: t("settingsFailedToSave") }); diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index 907479b6a..926f2ce65 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -168,11 +168,6 @@ const Settings = () => { } setSettingsData(newSettingsData); - - // Update Redux immediately for UI feedback - if (name === "showURL") { - dispatch(setShowURL(value)); - } }; const handleSave = () => { diff --git a/server/src/config/services.js b/server/src/config/services.js index 6d6a613d3..f3768be25 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -83,7 +83,8 @@ export const initializeServices = async ({ logger, envSettings, settingsService // Create DB const checkModule = new CheckModule({ logger, Check, Monitor, User }); const inviteModule = new InviteModule({ InviteToken, crypto, stringService }); - const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService, AppSettings }); + const settingsModule = new SettingsModule({ AppSettings }); + const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService, settingsModule }); const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); const monitorModule = new MonitorModule({ @@ -100,7 +101,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService }); const notificationModule = new NotificationModule({ Notification, Monitor }); const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService }); - const settingsModule = new SettingsModule({ AppSettings }); const incidentModule = new IncidentModule({ logger, Incident, Monitor, User }); const db = new MongoDB({ diff --git a/server/src/db/v1/modules/settingsModule.js b/server/src/db/v1/modules/settingsModule.js index 37b851bf2..cb689f467 100755 --- a/server/src/db/v1/modules/settingsModule.js +++ b/server/src/db/v1/modules/settingsModule.js @@ -4,8 +4,29 @@ const SERVICE_NAME = "SettingsModule"; class SettingsModule { constructor({ AppSettings }) { this.AppSettings = AppSettings; + this.settingsCache = null; + this.cacheTimestamp = null; + this.CACHE_TTL = 60000; // 1 minute cache } + getAppSettings = async () => { + try { + const now = Date.now(); + if (this.settingsCache && this.cacheTimestamp && (now - this.cacheTimestamp) < this.CACHE_TTL) { + return this.settingsCache; + } + + const settings = await this.AppSettings.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean(); + this.settingsCache = settings; + this.cacheTimestamp = now; + return settings; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getAppSettings"; + throw error; + } + }; + updateAppSettings = async (newSettings) => { try { const update = { $set: { ...newSettings } }; @@ -24,6 +45,11 @@ class SettingsModule { upsert: true, }); const settings = await this.AppSettings.findOne().select("-__v -_id -createdAt -updatedAt -singleton").lean(); + + // Invalidate cache after update + this.settingsCache = settings; + this.cacheTimestamp = Date.now(); + return settings; } catch (error) { error.service = SERVICE_NAME; diff --git a/server/src/db/v1/modules/statusPageModule.js b/server/src/db/v1/modules/statusPageModule.js index 870a1ee55..8c48d93a0 100755 --- a/server/src/db/v1/modules/statusPageModule.js +++ b/server/src/db/v1/modules/statusPageModule.js @@ -5,11 +5,11 @@ const SERVICE_NAME = "statusPageModule"; class StatusPageModule { - constructor({ StatusPage, NormalizeData, stringService, AppSettings }) { + constructor({ StatusPage, NormalizeData, stringService, settingsModule }) { this.StatusPage = StatusPage; this.NormalizeData = NormalizeData; this.stringService = stringService; - this.AppSettings = AppSettings; + this.settingsModule = settingsModule; } createStatusPage = async ({ statusPageData, image, userId, teamId }) => { @@ -260,19 +260,19 @@ class StatusPageModule { const { statusPage, monitors } = statusPageQuery[0]; - const appSettings = await this.AppSettings.findOne({ singleton: true }).lean(); + const appSettings = await this.settingsModule.getAppSettings(); const showURL = appSettings?.showURL === true; const normalizedMonitors = monitors.map((monitor) => { - const normalizedChecks = this.NormalizeData(monitor.checks, 10, 100); - if (showURL !== true) { - const { url, port, secret, notifications, ...rest } = monitor; - return { ...rest, checks: normalizedChecks }; - } - return { ...monitor, checks: normalizedChecks }; - }); + const normalizedChecks = this.NormalizeData(monitor.checks, 10, 100); + if (showURL !== true) { + const { url, port, secret, notifications, ...rest } = monitor; + return { ...rest, checks: normalizedChecks }; + } + return { ...monitor, checks: normalizedChecks }; + }); - return { statusPage, monitors: normalizedMonitors }; + return { statusPage, monitors: normalizedMonitors }; } catch (error) { error.service = SERVICE_NAME; error.method = "getStatusPageByUrl"; From e4d101237e0385a18e2800ce8cf75110c66680f5 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sun, 11 Jan 2026 21:51:59 +0000 Subject: [PATCH 184/400] bug fix - settings handlechange --- client/src/Hooks/v1/settingsHooks.js | 7 ------- client/src/Pages/v1/Settings/index.jsx | 5 +++++ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/client/src/Hooks/v1/settingsHooks.js b/client/src/Hooks/v1/settingsHooks.js index ac754ca69..f4a929e7f 100644 --- a/client/src/Hooks/v1/settingsHooks.js +++ b/client/src/Hooks/v1/settingsHooks.js @@ -57,13 +57,6 @@ const useSaveSettings = ({ } setSettingsData(settingsResponse.data.data); - // Update Redux state after successful save - if (typeof settings.showURL !== "undefined") { - const { setShowURL } = await import("../../Features/UI/uiSlice.js"); - const { default: store } = await import("../../store.js"); - store.dispatch(setShowURL(settings.showURL)); - } - createToast({ body: t("settingsSuccessSaved") }); } catch (error) { createToast({ body: t("settingsFailedToSave") }); diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index 926f2ce65..ea714c34a 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -168,6 +168,11 @@ const Settings = () => { } setSettingsData(newSettingsData); + + // Update Redux immediately for UI feedback + if (name === "showURL") { + dispatch(setShowURL(newValue ?? value)); + } }; const handleSave = () => { From b6f48a910fed7f3909b1999825f74d61db448d6c Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sun, 11 Jan 2026 22:08:15 +0000 Subject: [PATCH 185/400] Revert caching changes - restore working implementation - Remove settingsModule caching that caused inconsistent state - Restore direct AppSettings query in statusPageModule - Keep immediate Redux dispatch for UI feedback - All functionality verified working" --- server/src/config/services.js | 4 +-- server/src/db/v1/modules/settingsModule.js | 26 -------------------- server/src/db/v1/modules/statusPageModule.js | 6 ++--- 3 files changed, 5 insertions(+), 31 deletions(-) diff --git a/server/src/config/services.js b/server/src/config/services.js index f3768be25..6d6a613d3 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -83,8 +83,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService // Create DB const checkModule = new CheckModule({ logger, Check, Monitor, User }); const inviteModule = new InviteModule({ InviteToken, crypto, stringService }); - const settingsModule = new SettingsModule({ AppSettings }); - const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService, settingsModule }); + const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService, AppSettings }); const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); const monitorModule = new MonitorModule({ @@ -101,6 +100,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService }); const notificationModule = new NotificationModule({ Notification, Monitor }); const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService }); + const settingsModule = new SettingsModule({ AppSettings }); const incidentModule = new IncidentModule({ logger, Incident, Monitor, User }); const db = new MongoDB({ diff --git a/server/src/db/v1/modules/settingsModule.js b/server/src/db/v1/modules/settingsModule.js index cb689f467..37b851bf2 100755 --- a/server/src/db/v1/modules/settingsModule.js +++ b/server/src/db/v1/modules/settingsModule.js @@ -4,29 +4,8 @@ const SERVICE_NAME = "SettingsModule"; class SettingsModule { constructor({ AppSettings }) { this.AppSettings = AppSettings; - this.settingsCache = null; - this.cacheTimestamp = null; - this.CACHE_TTL = 60000; // 1 minute cache } - getAppSettings = async () => { - try { - const now = Date.now(); - if (this.settingsCache && this.cacheTimestamp && (now - this.cacheTimestamp) < this.CACHE_TTL) { - return this.settingsCache; - } - - const settings = await this.AppSettings.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean(); - this.settingsCache = settings; - this.cacheTimestamp = now; - return settings; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getAppSettings"; - throw error; - } - }; - updateAppSettings = async (newSettings) => { try { const update = { $set: { ...newSettings } }; @@ -45,11 +24,6 @@ class SettingsModule { upsert: true, }); const settings = await this.AppSettings.findOne().select("-__v -_id -createdAt -updatedAt -singleton").lean(); - - // Invalidate cache after update - this.settingsCache = settings; - this.cacheTimestamp = Date.now(); - return settings; } catch (error) { error.service = SERVICE_NAME; diff --git a/server/src/db/v1/modules/statusPageModule.js b/server/src/db/v1/modules/statusPageModule.js index 8c48d93a0..f276814f9 100755 --- a/server/src/db/v1/modules/statusPageModule.js +++ b/server/src/db/v1/modules/statusPageModule.js @@ -5,11 +5,11 @@ const SERVICE_NAME = "statusPageModule"; class StatusPageModule { - constructor({ StatusPage, NormalizeData, stringService, settingsModule }) { + constructor({ StatusPage, NormalizeData, stringService, AppSettings }) { this.StatusPage = StatusPage; this.NormalizeData = NormalizeData; this.stringService = stringService; - this.settingsModule = settingsModule; + this.AppSettings = AppSettings; } createStatusPage = async ({ statusPageData, image, userId, teamId }) => { @@ -260,7 +260,7 @@ class StatusPageModule { const { statusPage, monitors } = statusPageQuery[0]; - const appSettings = await this.settingsModule.getAppSettings(); + const appSettings = await this.AppSettings.findOne({ singleton: true }).lean(); const showURL = appSettings?.showURL === true; const normalizedMonitors = monitors.map((monitor) => { From a70959e4a66769604c9298a125d44a455b75aed3 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sun, 11 Jan 2026 22:36:05 +0000 Subject: [PATCH 186/400] Nitpick fix - ref - client/src/Pages/v1/Settings/index.jsx (1) 171-175: LGTM! The Redux dispatch provides immediate UI feedback while the actual persistence happens on Save. This is the correct pattern for a server-persisted setting. Minor note: The ?? value fallback is unreachable for showURL since newValue is always set to a boolean at line 96. Consider simplifying to dispatch(setShowURL(newValue)) for clarity, though the current code is functionally correct. --- client/src/Pages/v1/Settings/index.jsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index ea714c34a..23478e2e8 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -171,11 +171,7 @@ const Settings = () => { // Update Redux immediately for UI feedback if (name === "showURL") { - dispatch(setShowURL(newValue ?? value)); - } - }; - - const handleSave = () => { + dispatch(setShowURL(newValue)); const { error } = settingsValidation.validate(settingsData.settings, { abortEarly: false, }); From 1494df9eb39b18044978045f339d3cd7e27a1832 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sun, 11 Jan 2026 22:39:49 +0000 Subject: [PATCH 187/400] missing brace --- client/src/Pages/v1/Settings/index.jsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index 23478e2e8..b988b9e2c 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -172,10 +172,10 @@ const Settings = () => { // Update Redux immediately for UI feedback if (name === "showURL") { dispatch(setShowURL(newValue)); - const { error } = settingsValidation.validate(settingsData.settings, { - abortEarly: false, - }); - if (!error || error.details.length === 0) { + } +}; + +const handleSave = () => { setErrors({}); } else { const newErrors = {}; From 3ce40b1ab8839ead5789f98d39feb9619ab151b4 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Sun, 11 Jan 2026 22:48:17 +0000 Subject: [PATCH 188/400] Fix handleSave function syntax errors --- client/package-lock.json | 1678 +++++++++++++++++------- client/src/Pages/v1/Settings/index.jsx | 27 +- 2 files changed, 1212 insertions(+), 493 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 75dbe43da..7ca70d20f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -56,19 +56,6 @@ "vite": "6.3.6" } }, - "node_modules/@ampproject/remapping": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", - "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -84,31 +71,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.2.tgz", - "integrity": "sha512-TUtMJYRPyUb/9aU8f3K0mjmjf6M9N5Woshn2CS6nqJSeJtTtQcpLUXjGt9vbF8ZGff0El99sWkLgzwW3VXnxZQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", - "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", "license": "MIT", - "peer": true, "dependencies": { - "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helpers": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -130,15 +116,15 @@ "license": "MIT" }, "node_modules/@babel/generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", - "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.27.1", - "@babel/types": "^7.27.1", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" }, "engines": { @@ -161,6 +147,15 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", @@ -175,14 +170,14 @@ } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz", - "integrity": "sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g==", + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "license": "MIT", "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" + "@babel/traverse": "^7.28.3" }, "engines": { "node": ">=6.9.0" @@ -211,9 +206,9 @@ } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -229,25 +224,25 @@ } }, "node_modules/@babel/helpers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.1.tgz", - "integrity": "sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "license": "MIT", "dependencies": { - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/parser": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.2.tgz", - "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "license": "MIT", "dependencies": { - "@babel/types": "^7.27.1" + "@babel/types": "^7.28.5" }, "bin": { "parser": "bin/babel-parser.js" @@ -289,9 +284,9 @@ } }, "node_modules/@babel/runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", - "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", "license": "MIT", "engines": { "node": ">=6.9.0" @@ -312,40 +307,40 @@ } }, "node_modules/@babel/traverse": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.1.tgz", - "integrity": "sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", "license": "MIT", "dependencies": { "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.27.1", - "@babel/parser": "^7.27.1", - "@babel/template": "^7.27.1", - "@babel/types": "^7.27.1", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/types": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.1.tgz", - "integrity": "sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q==", + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", "license": "MIT", "dependencies": { "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "@babel/helper-validator-identifier": "^7.28.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@ctrl/tinycolor": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.1.0.tgz", - "integrity": "sha512-WyOx8cJQ+FQus4Mm4uPIZA64gbk3Wxh0so5Lcii0aJifqwoVOlfFtorjLE0Hen4OYyHZMXDWqMmaQemBhgxFRQ==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", + "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", "license": "MIT", "engines": { "node": ">=14" @@ -390,9 +385,9 @@ "license": "MIT" }, "node_modules/@emotion/is-prop-valid": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.3.1.tgz", - "integrity": "sha512-/ACwoqx7XQi9knQs/G0qKvv5teDMhD7bXYns9N/wM8ah8iNb8jZ2uNO0YOgiq2o2poIvVtJS2YALasQuMSQ7Kw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.4.0.tgz", + "integrity": "sha512-QgD4fyscGcbbKwJmqNvUMSE02OsHUa+lAWKdEUIJKgqe5IwRSKd7+KhibEWdaKwgjLj0DRSHA9biAIqGBk05lw==", "license": "MIT", "dependencies": { "@emotion/memoize": "^0.9.0" @@ -409,7 +404,6 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -449,11 +443,10 @@ "license": "MIT" }, "node_modules/@emotion/styled": { - "version": "11.14.0", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", - "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", + "version": "11.14.1", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.1.tgz", + "integrity": "sha512-qEEJt42DuToa3gurlH4Qqc1kVpNq8wO8cJtDzU46TjlzWjDlsVyevtYCRijVq3SrHsROS+gVQ8Fnea108GnKzw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -499,10 +492,266 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/linux-x64": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", - "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -515,10 +764,154 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -535,9 +928,9 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, "license": "MIT", "engines": { @@ -568,22 +961,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@eslint/js": { "version": "8.57.1", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", @@ -595,31 +972,31 @@ } }, "node_modules/@floating-ui/core": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.0.tgz", - "integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==", + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", "license": "MIT", "dependencies": { - "@floating-ui/utils": "^0.2.9" + "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/dom": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.0.tgz", - "integrity": "sha512-lGTor4VlXcesUMh1cupTUTDoCxMb0V6bm3CnxHzQcw8Eaf1jQbgQX4i02fYgT0vJ82tb5MZ4CZk1LRGkktJCzg==", + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", "license": "MIT", "dependencies": { - "@floating-ui/core": "^1.7.0", - "@floating-ui/utils": "^0.2.9" + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" } }, "node_modules/@floating-ui/react-dom": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", - "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.6.tgz", + "integrity": "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw==", "license": "MIT", "dependencies": { - "@floating-ui/dom": "^1.0.0" + "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", @@ -627,9 +1004,9 @@ } }, "node_modules/@floating-ui/utils": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", - "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", "license": "MIT" }, "node_modules/@hapi/hoek": { @@ -715,17 +1092,23 @@ "license": "BSD-3-Clause" }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", - "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -737,25 +1120,16 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", - "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -766,7 +1140,7 @@ "version": "5.0.0-beta.42", "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.42.tgz", "integrity": "sha512-fWRiUJVCHCPF+mxd5drn08bY2qRw3jj5f1SSQdUXmaJ/yKpk23ys8MgLO2KGVTRtbks/+ctRfgffGPbXifj0Ug==", - "deprecated": "This package has been replaced by @base-ui-components/react", + "deprecated": "This package has been replaced by @base-ui/react", "license": "MIT", "dependencies": { "@babel/runtime": "^7.24.4", @@ -796,9 +1170,9 @@ } }, "node_modules/@mui/core-downloads-tracker": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.4.11.tgz", - "integrity": "sha512-CzAQs9CTzlwbsF9ZYB4o4lLwBv1/qNE264NjuYao+ctAXsmlPtYa8RtER4UsUXSMxNN9Qi+aQdYcKl2sUpnmAw==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-6.5.0.tgz", + "integrity": "sha512-LGb8t8i6M2ZtS3Drn3GbTI1DVhDY6FJ9crEey2lZ0aN2EMZo8IZBZj9wRf4vqbZHaWjsYgtbOnJw5V8UWbmK2Q==", "license": "MIT", "funding": { "type": "opencollective", @@ -900,9 +1274,9 @@ } }, "node_modules/@mui/lab/node_modules/@mui/styled-engine": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.11.tgz", - "integrity": "sha512-74AUmlHXaGNbyUqdK/+NwDJOZqgRQw6BcNvhoWYLq3LGbLTkE+khaJ7soz6cIabE4CPYqO2/QAIU1Z/HEjjpcw==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.5.0.tgz", + "integrity": "sha512-8woC2zAqF4qUDSPIBZ8v3sakj+WgweolpyM/FXf8jAx6FMls+IE4Y8VDZc+zS805J7PRz31vz73n2SovKGaYgw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", @@ -934,14 +1308,14 @@ } }, "node_modules/@mui/lab/node_modules/@mui/system": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.11.tgz", - "integrity": "sha512-gibtsrZEwnDaT5+I/KloOj/yHluX5G8heknuxBpQOdEQ3Gc0avjSImn5hSeKp8D4thiwZiApuggIjZw1dQguUA==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.5.0.tgz", + "integrity": "sha512-XcbBYxDS+h/lgsoGe78ExXFZXtuIlSBpn/KsZq8PtZcIkUNJInkuDqcLd2rVBQrDC1u+rvVovdaWPf2FHKJf3w==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", "@mui/private-theming": "^6.4.9", - "@mui/styled-engine": "^6.4.11", + "@mui/styled-engine": "^6.5.0", "@mui/types": "~7.2.24", "@mui/utils": "^6.4.9", "clsx": "^2.1.1", @@ -992,7 +1366,6 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.11.tgz", "integrity": "sha512-k2D3FLJS+/qD0qnd6ZlAjGFvaaxe1Dl10NyvpeDzIebMuYdn8VqYe6XBgGueEAtnzSJM4V03VD9kb5Fi24dnTA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.26.0", "@mui/core-downloads-tracker": "^6.4.11", @@ -1065,9 +1438,9 @@ } }, "node_modules/@mui/material/node_modules/@mui/styled-engine": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.4.11.tgz", - "integrity": "sha512-74AUmlHXaGNbyUqdK/+NwDJOZqgRQw6BcNvhoWYLq3LGbLTkE+khaJ7soz6cIabE4CPYqO2/QAIU1Z/HEjjpcw==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-6.5.0.tgz", + "integrity": "sha512-8woC2zAqF4qUDSPIBZ8v3sakj+WgweolpyM/FXf8jAx6FMls+IE4Y8VDZc+zS805J7PRz31vz73n2SovKGaYgw==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", @@ -1099,14 +1472,14 @@ } }, "node_modules/@mui/material/node_modules/@mui/system": { - "version": "6.4.11", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.4.11.tgz", - "integrity": "sha512-gibtsrZEwnDaT5+I/KloOj/yHluX5G8heknuxBpQOdEQ3Gc0avjSImn5hSeKp8D4thiwZiApuggIjZw1dQguUA==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-6.5.0.tgz", + "integrity": "sha512-XcbBYxDS+h/lgsoGe78ExXFZXtuIlSBpn/KsZq8PtZcIkUNJInkuDqcLd2rVBQrDC1u+rvVovdaWPf2FHKJf3w==", "license": "MIT", "dependencies": { "@babel/runtime": "^7.26.0", "@mui/private-theming": "^6.4.9", - "@mui/styled-engine": "^6.4.11", + "@mui/styled-engine": "^6.5.0", "@mui/types": "~7.2.24", "@mui/utils": "^6.4.9", "clsx": "^2.1.1", @@ -1153,13 +1526,14 @@ } }, "node_modules/@mui/private-theming": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.0.tgz", - "integrity": "sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.3.7.tgz", + "integrity": "sha512-w7r1+CYhG0syCAQUWAuV5zSaU2/67WA9JXUderdb7DzCIJdp/5RmJv6L85wRjgKCMsxFF0Kfn0kPgPbPgw/jdw==", "license": "MIT", + "peer": true, "dependencies": { - "@babel/runtime": "^7.27.1", - "@mui/utils": "^7.1.0", + "@babel/runtime": "^7.28.4", + "@mui/utils": "^7.3.7", "prop-types": "^15.8.1" }, "engines": { @@ -1180,17 +1554,18 @@ } }, "node_modules/@mui/private-theming/node_modules/@mui/utils": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz", - "integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.7.tgz", + "integrity": "sha512-+YjnjMRnyeTkWnspzoxRdiSOgkrcpTikhNPoxOZW0APXx+urHtUoXJ9lbtCZRCA5a4dg5gSbd19alL1DvRs5fg==", "license": "MIT", + "peer": true, "dependencies": { - "@babel/runtime": "^7.27.1", - "@mui/types": "^7.4.2", - "@types/prop-types": "^15.7.14", + "@babel/runtime": "^7.28.4", + "@mui/types": "^7.4.10", + "@types/prop-types": "^15.7.15", "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^19.1.0" + "react-is": "^19.2.3" }, "engines": { "node": ">=14.0.0" @@ -1210,16 +1585,17 @@ } }, "node_modules/@mui/styled-engine": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.0.tgz", - "integrity": "sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.3.7.tgz", + "integrity": "sha512-y/QkNXv6cF6dZ5APztd/dFWfQ6LHKPx3skyYO38YhQD4+Cxd6sFAL3Z38WMSSC8LQz145Mpp3CcLrSCLKPwYAg==", "license": "MIT", + "peer": true, "dependencies": { - "@babel/runtime": "^7.27.1", - "@emotion/cache": "^11.13.5", + "@babel/runtime": "^7.28.4", + "@emotion/cache": "^11.14.0", "@emotion/serialize": "^1.3.3", "@emotion/sheet": "^1.4.0", - "csstype": "^3.1.3", + "csstype": "^3.2.3", "prop-types": "^15.8.1" }, "engines": { @@ -1244,19 +1620,19 @@ } }, "node_modules/@mui/system": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.1.0.tgz", - "integrity": "sha512-iedAWgRJMCxeMHvkEhsDlbvkK+qKf9me6ofsf7twk/jfT4P1ImVf7Rwb5VubEA0sikrVL+1SkoZM41M4+LNAVA==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-7.3.7.tgz", + "integrity": "sha512-DovL3k+FBRKnhmatzUMyO5bKkhMLlQ9L7Qw5qHrre3m8zCZmE+31NDVBFfqrbrA7sq681qaEIHdkWD5nmiAjyQ==", "license": "MIT", "peer": true, "dependencies": { - "@babel/runtime": "^7.27.1", - "@mui/private-theming": "^7.1.0", - "@mui/styled-engine": "^7.1.0", - "@mui/types": "^7.4.2", - "@mui/utils": "^7.1.0", + "@babel/runtime": "^7.28.4", + "@mui/private-theming": "^7.3.7", + "@mui/styled-engine": "^7.3.7", + "@mui/types": "^7.4.10", + "@mui/utils": "^7.3.7", "clsx": "^2.1.1", - "csstype": "^3.1.3", + "csstype": "^3.2.3", "prop-types": "^15.8.1" }, "engines": { @@ -1285,17 +1661,18 @@ } }, "node_modules/@mui/system/node_modules/@mui/utils": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz", - "integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==", + "version": "7.3.7", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.3.7.tgz", + "integrity": "sha512-+YjnjMRnyeTkWnspzoxRdiSOgkrcpTikhNPoxOZW0APXx+urHtUoXJ9lbtCZRCA5a4dg5gSbd19alL1DvRs5fg==", "license": "MIT", + "peer": true, "dependencies": { - "@babel/runtime": "^7.27.1", - "@mui/types": "^7.4.2", - "@types/prop-types": "^15.7.14", + "@babel/runtime": "^7.28.4", + "@mui/types": "^7.4.10", + "@types/prop-types": "^15.7.15", "clsx": "^2.1.1", "prop-types": "^15.8.1", - "react-is": "^19.1.0" + "react-is": "^19.2.3" }, "engines": { "node": ">=14.0.0" @@ -1315,12 +1692,12 @@ } }, "node_modules/@mui/types": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.2.tgz", - "integrity": "sha512-edRc5JcLPsrlNFYyTPxds+d5oUovuUxnnDtpJUbP6WMeV4+6eaX/mqai1ZIWT62lCOe0nlrON0s9HDiv5en5bA==", + "version": "7.4.10", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.4.10.tgz", + "integrity": "sha512-0+4mSjknSu218GW3isRqoxKRTOrTLd/vHi/7UC4+wZcUrOAqD9kRk7UQRL1mcrzqRoe7s3UT6rsRpbLkW5mHpQ==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.27.1" + "@babel/runtime": "^7.28.4" }, "peerDependencies": { "@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0" @@ -1666,18 +2043,25 @@ } }, "node_modules/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", "license": "MIT", "engines": { "node": ">=14.0.0" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/pluginutils": { - "version": "5.1.4", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", - "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -1696,10 +2080,114 @@ } } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz", - "integrity": "sha512-l+QK99je2zUKGd31Gh+45c4pGDAqZSuWQiuRFCdHYC2CSiO47qUWsCcenrI6p22hvHZrDje9QjwSMAFL3iwXwQ==", + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", + "integrity": "sha512-9R0DM/ykwfGIlNu6+2U09ga0WXeZ9MRC2Ter8jnz8415VbuIykVuc6bhdrbORFZANDmTDvq26mJrEVTl8TdnDg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.1.tgz", + "integrity": "sha512-eFZCb1YUqhTysgW3sj/55du5cG57S7UTNtdMjCW7LwVcj3dTTcowCsC8p7uBdzKsZYa8J7IDE8lhMI+HX1vQvg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.1.tgz", + "integrity": "sha512-p3grE2PHcQm2e8PSGZdzIhCKbMCw/xi9XvMPErPhwO17vxtvCN5FEA2mSLgmKlCjHGMQTP6phuQTYWUnKewwGg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.1.tgz", + "integrity": "sha512-rDUjG25C9qoTm+e02Esi+aqTKSBYwVTaoS1wxcN47/Luqef57Vgp96xNANwt5npq9GDxsH7kXxNkJVEsWEOEaQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.1.tgz", + "integrity": "sha512-+JiU7Jbp5cdxekIgdte0jfcu5oqw4GCKr6i3PJTlXTCU5H5Fvtkpbs4XJHRmWNXF+hKmn4v7ogI5OQPaupJgOg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.1.tgz", + "integrity": "sha512-V5xC1tOVWtLLmr3YUk2f6EJK4qksksOYiz/TCsFHu/R+woubcLWdC9nZQmwjOAbmExBIVKsm1/wKmEy4z4u4Bw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.1.tgz", + "integrity": "sha512-Rn3n+FUk2J5VWx+ywrG/HGPTD9jXNbicRtTM11e/uorplArnXZYsVifnPPqNNP5BsO3roI4n8332ukpY/zN7rQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.1.tgz", + "integrity": "sha512-grPNWydeKtc1aEdrJDWk4opD7nFtQbMmV7769hiAaYyUKCT1faPRm2av8CX1YJsZ4TLAZcg9gTR1KvEzoLjXkg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.1.tgz", + "integrity": "sha512-a59mwd1k6x8tXKcUxSyISiquLwB5pX+fJW9TkWU46lCqD/GRDe9uDN31jrMmVP3feI3mhAdvcCClhV8V5MhJFQ==", "cpu": [ "arm64" ], @@ -1709,10 +2197,114 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.1.tgz", + "integrity": "sha512-puS1MEgWX5GsHSoiAsF0TYrpomdvkaXm0CofIMG5uVkP6IBV+ZO9xhC5YEN49nsgYo1DuuMquF9+7EDBVYu4uA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.1.tgz", + "integrity": "sha512-r3Wv40in+lTsULSb6nnoudVbARdOwb2u5fpeoOAZjFLznp6tDU8kd+GTHmJoqZ9lt6/Sys33KdIHUaQihFcu7g==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.1.tgz", + "integrity": "sha512-MR8c0+UxAlB22Fq4R+aQSPBayvYa3+9DrwG/i1TKQXFYEaoW3B5b/rkSRIypcZDdWjWnpcvxbNaAJDcSbJU3Lw==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.1.tgz", + "integrity": "sha512-3KhoECe1BRlSYpMTeVrD4sh2Pw2xgt4jzNSZIIPLFEsnQn9gAnZagW9+VqDqAHgm1Xc77LzJOo2LdigS5qZ+gw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.1.tgz", + "integrity": "sha512-ziR1OuZx0vdYZZ30vueNZTg73alF59DicYrPViG0NEgDVN8/Jl87zkAPu4u6VjZST2llgEUjaiNl9JM6HH1Vdw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.1.tgz", + "integrity": "sha512-uW0Y12ih2XJRERZ4jAfKamTyIHVMPQnTZcQjme2HMVDAHY4amf5u414OqNYC+x+LzRdRcnIG1YodLrrtA8xsxw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.1.tgz", + "integrity": "sha512-u9yZ0jUkOED1BFrqu3BwMQoixvGHGZ+JhJNkNKY/hyoEgOwlqKb62qu+7UjbPSHYjiVy8kKJHvXKv5coH4wDeg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.1.tgz", + "integrity": "sha512-/0PenBCmqM4ZUd0190j7J0UsQ/1nsi735iPRakO8iPciE7BQ495Y6msPzaOmvx0/pn+eJVVlZrNrSh4WSYLxNg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz", - "integrity": "sha512-XMLeKjyH8NsEDCRptf6LO8lJk23o9wvB+dJwcXMaH6ZQbbkHu2dbGIUindbMtRN6ux1xKi16iXWu6q9mu7gDhQ==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.1.tgz", + "integrity": "sha512-a8G4wiQxQG2BAvo+gU6XrReRRqj+pLS2NGXKm8io19goR+K8lw269eTrPkSdDTALwMmJp4th2Uh0D8J9bEV1vg==", "cpu": [ "x64" ], @@ -1723,9 +2315,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.41.0.tgz", - "integrity": "sha512-m/P7LycHZTvSQeXhFmgmdqEiTqSV80zn6xHaQ1JSqwCtD1YGtwEK515Qmy9DcB2HK4dOUVypQxvhVSy06cJPEg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.1.tgz", + "integrity": "sha512-bD+zjpFrMpP/hqkfEcnjXWHMw5BIghGisOKPj+2NaNDuVT+8Ds4mPf3XcPHuat1tz89WRL+1wbcxKY3WSbiT7w==", "cpu": [ "x64" ], @@ -1735,6 +2327,84 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.1.tgz", + "integrity": "sha512-eLXw0dOiqE4QmvikfQ6yjgkg/xDM+MdU9YJuP4ySTibXU0oAvnEWXt7UDJmD4UkYialMfOGFPJnIHSe/kdzPxg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.1.tgz", + "integrity": "sha512-xzm44KgEP11te3S2HCSyYf5zIzWmx3n8HDCc7EE59+lTcswEWNpvMLfd9uJvVX8LCg9QWG67Xt75AuHn4vgsXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.1.tgz", + "integrity": "sha512-yR6Bl3tMC/gBok5cz/Qi0xYnVbIxGx5Fcf/ca0eB6/6JwOY+SRUcJfI0OpeTpPls7f194as62thCt/2BjxYN8g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.1.tgz", + "integrity": "sha512-3fZBidchE0eY0oFZBnekYCfg+5wAB0mbpCBuofh5mZuzIU/4jIVkbESmd2dOsFNS78b53CYv3OAtwqkZZmU5nA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.1.tgz", + "integrity": "sha512-xGGY5pXj69IxKb4yv/POoocPy/qmEGhimy/FoTpTSVju3FYXUQQMFCaZZXJVidsmGxRioZAwpThl/4zX41gRKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.1.tgz", + "integrity": "sha512-SPEpaL6DX4rmcXtnhdrQYgzQ5W2uW3SCJch88lB2zImhJRhIIK44fkUrgIV/Q8yUNfw5oyZ5vkeQsZLhCb06lw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -1757,9 +2427,9 @@ "license": "BSD-3-Clause" }, "node_modules/@standard-schema/spec": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", - "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", "license": "MIT" }, "node_modules/@standard-schema/utils": { @@ -2043,19 +2713,19 @@ } }, "node_modules/@types/babel__traverse": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", - "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", "dev": true, "license": "MIT", "dependencies": { - "@babel/types": "^7.20.7" + "@babel/types": "^7.28.2" } }, "node_modules/@types/d3-array": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", - "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", "license": "MIT" }, "node_modules/@types/d3-color": { @@ -2122,9 +2792,9 @@ "license": "MIT" }, "node_modules/@types/estree": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", - "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", "license": "MIT" }, "node_modules/@types/node": { @@ -2133,7 +2803,6 @@ "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.12.0" } @@ -2145,20 +2814,19 @@ "license": "MIT" }, "node_modules/@types/prop-types": { - "version": "15.7.14", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.14.tgz", - "integrity": "sha512-gNMvNH49DJ7OJYv+KAKn0Xp45p8PLl6zo2YnvDIbTd4J6MER2BmWN49TG7n9LvkyihINxeKW8+3bfS2yDC9dzQ==", + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.22", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.22.tgz", - "integrity": "sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==", + "version": "18.3.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.27.tgz", + "integrity": "sha512-cisd7gxkzjBKU2GgdYrTdtQx1SORymWyaAFhaxQPK9bYO9ot3Y5OikQRvY0VYQtvwjeQnizCINJAenh/V7MK2w==", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", - "csstype": "^3.0.2" + "csstype": "^3.2.2" } }, "node_modules/@types/react-dom": { @@ -2194,15 +2862,16 @@ "license": "ISC" }, "node_modules/@vitejs/plugin-react": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.4.1.tgz", - "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", "dev": true, "license": "MIT", "dependencies": { - "@babel/core": "^7.26.10", - "@babel/plugin-transform-react-jsx-self": "^7.25.9", - "@babel/plugin-transform-react-jsx-source": "^7.25.9", + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, @@ -2210,16 +2879,15 @@ "node": "^14.18.0 || >=16.0.0" }, "peerDependencies": { - "vite": "^4.2.0 || ^5.0.0 || ^6.0.0" + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "node_modules/acorn": { - "version": "8.14.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", - "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -2304,18 +2972,20 @@ } }, "node_modules/array-includes": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.8.tgz", - "integrity": "sha512-itaWrbYbqpGXkGhZPGUulwnhVf5Hpy1xiCFsGqyIGglbBxmG5vSjxQen3/WGOjPpNEv1RtBLKxbmVXm8HpJStQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, "license": "MIT", "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.2", - "es-object-atoms": "^1.0.0", - "get-intrinsic": "^1.2.4", - "is-string": "^1.0.7" + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -2455,9 +3125,9 @@ } }, "node_modules/axios": { - "version": "1.12.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.12.2.tgz", - "integrity": "sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==", + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", @@ -2496,6 +3166,15 @@ "node": ">= 0.6.0" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.14", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.14.tgz", + "integrity": "sha512-B0xUquLkiGLgHhpPBqvl7GWegWBUNuujQ6kXd/r1U38ElPT6Ok8KZ8e+FpUGEc2ZoRQUzq/aUnaKFc/svWUGSg==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, "node_modules/brace-expansion": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", @@ -2508,9 +3187,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.5", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.5.tgz", - "integrity": "sha512-FDToo4Wo82hIdgc1CQ+NQD0hEhmpPjrZ3hiUgwgOG6IuTdlpr8jdjyG24P6cNP1yJpTLzS5OcGgSw0xmDU1/Tw==", + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "funding": [ { "type": "opencollective", @@ -2526,12 +3205,12 @@ } ], "license": "MIT", - "peer": true, "dependencies": { - "caniuse-lite": "^1.0.30001716", - "electron-to-chromium": "^1.5.149", - "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.3" + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" @@ -2611,9 +3290,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001718", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001718.tgz", - "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==", + "version": "1.0.30001764", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001764.tgz", + "integrity": "sha512-9JGuzl2M+vPL+pz70gtMF9sHdMFbY9FJaQBi186cHKH3pSzDvzoUJUPV6fqiKIMyXbud9ZLg4F3Yza1vJ1+93g==", "funding": [ { "type": "opencollective", @@ -2630,6 +3309,23 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -2743,9 +3439,9 @@ } }, "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, "node_modules/d3-array": { @@ -2939,13 +3635,12 @@ "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/debug": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", - "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "license": "MIT", "dependencies": { "ms": "^2.1.3" @@ -3083,9 +3778,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.155", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.155.tgz", - "integrity": "sha512-ps5KcGGmwL8VaeJlvlDlu4fORQpv3+GIcF5I3f9tUKUlJ/wsysh6HU8P5L1XWRYeXfA0oJd4PyM8ds8zTFf6Ng==", + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", "license": "ISC" }, "node_modules/entities": { @@ -3101,18 +3796,18 @@ } }, "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } }, "node_modules/es-abstract": { - "version": "1.23.10", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.10.tgz", - "integrity": "sha512-MtUbM072wlJNyeYAe0mhzrD+M6DIJa96CZAOBBrhDbgKnB4MApIKefcyAB1eOdYn8cUNZgvwBvEzdoAYsxgEIw==", + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", "dev": true, "license": "MIT", "dependencies": { @@ -3143,7 +3838,9 @@ "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", + "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", @@ -3158,6 +3855,7 @@ "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", @@ -3194,27 +3892,27 @@ } }, "node_modules/es-iterator-helpers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", - "integrity": "sha512-uDn+FE1yrDzyC0pCo961B2IHbdM8y/ACZsKD4dG6WqrjV53BADjwa7D+1aom2rsNVfLyDgU/eigvlJGJ08OQ4w==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.2.tgz", + "integrity": "sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==", "dev": true, "license": "MIT", "dependencies": { "call-bind": "^1.0.8", - "call-bound": "^1.0.3", + "call-bound": "^1.0.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.6", + "es-abstract": "^1.24.1", "es-errors": "^1.3.0", - "es-set-tostringtag": "^2.0.3", + "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.6", + "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", - "iterator.prototype": "^1.1.4", + "iterator.prototype": "^1.1.5", "safe-array-concat": "^1.1.3" }, "engines": { @@ -3280,9 +3978,9 @@ } }, "node_modules/esbuild": { - "version": "0.25.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", - "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "hasInstallScript": true, "license": "MIT", "bin": { @@ -3292,31 +3990,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.25.5", - "@esbuild/android-arm": "0.25.5", - "@esbuild/android-arm64": "0.25.5", - "@esbuild/android-x64": "0.25.5", - "@esbuild/darwin-arm64": "0.25.5", - "@esbuild/darwin-x64": "0.25.5", - "@esbuild/freebsd-arm64": "0.25.5", - "@esbuild/freebsd-x64": "0.25.5", - "@esbuild/linux-arm": "0.25.5", - "@esbuild/linux-arm64": "0.25.5", - "@esbuild/linux-ia32": "0.25.5", - "@esbuild/linux-loong64": "0.25.5", - "@esbuild/linux-mips64el": "0.25.5", - "@esbuild/linux-ppc64": "0.25.5", - "@esbuild/linux-riscv64": "0.25.5", - "@esbuild/linux-s390x": "0.25.5", - "@esbuild/linux-x64": "0.25.5", - "@esbuild/netbsd-arm64": "0.25.5", - "@esbuild/netbsd-x64": "0.25.5", - "@esbuild/openbsd-arm64": "0.25.5", - "@esbuild/openbsd-x64": "0.25.5", - "@esbuild/sunos-x64": "0.25.5", - "@esbuild/win32-arm64": "0.25.5", - "@esbuild/win32-ia32": "0.25.5", - "@esbuild/win32-x64": "0.25.5" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/escalade": { @@ -3347,7 +4046,6 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -3445,9 +4143,9 @@ } }, "node_modules/eslint-plugin-react-refresh": { - "version": "0.4.20", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.20.tgz", - "integrity": "sha512-XpbHQ2q5gUF8BGOX4dHe+71qoirYMhApEPZ7sfhF/dNnOF1UXnCMGZf79SFTBO7Bz5YEIT4TMieSlJBWhP9WBA==", + "version": "0.4.26", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.26.tgz", + "integrity": "sha512-1RETEylht2O6FM/MvgnyvT+8K21wLqDNg4qD51Zj3guhjt433XbnnkVttHMyaVyAFD03QSV4LPS5iE3VQmO7XQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -3515,39 +4213,6 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/eslint/node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "type-fest": "^0.20.2" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/espree": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", @@ -3567,9 +4232,9 @@ } }, "node_modules/esquery": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", - "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3632,9 +4297,9 @@ "license": "MIT" }, "node_modules/fast-equals": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", - "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", "license": "MIT", "engines": { "node": ">=6.0.0" @@ -3655,9 +4320,9 @@ "license": "MIT" }, "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", "dev": true, "license": "ISC", "dependencies": { @@ -3665,10 +4330,13 @@ } }, "node_modules/fdir": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", - "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, "peerDependencies": { "picomatch": "^3 || ^4" }, @@ -3743,9 +4411,9 @@ "license": "ISC" }, "node_modules/follow-redirects": { - "version": "1.15.9", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", - "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", "funding": [ { "type": "individual", @@ -3779,9 +4447,9 @@ } }, "node_modules/form-data": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", - "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -3855,6 +4523,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -3955,12 +4633,19 @@ } }, "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, "engines": { - "node": ">=4" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalthis": { @@ -4155,7 +4840,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -4179,9 +4863,9 @@ } }, "node_modules/immer": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/immer/-/immer-10.1.1.tgz", - "integrity": "sha512-s2MPrmjovJcoMaHtx6K11Ra7oD05NT97w1IC5zpMkT6Atjr7H8LjaDd81iIxUYpMKSRRNMJE703M1Fhr/TctHw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/immer/-/immer-10.2.0.tgz", + "integrity": "sha512-d/+XTN3zfODyjr89gM3mPq1WNX2B8pYsu7eORitdwyA2sBubnTl3laYlBk4sXY5FUa5qTZGBDPJICVbvqzjlbw==", "license": "MIT", "funding": { "type": "opencollective", @@ -4424,14 +5108,15 @@ } }, "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", "dev": true, "license": "MIT", "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" }, @@ -4468,6 +5153,19 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/is-number-object": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", @@ -4692,9 +5390,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -4966,9 +5664,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.19", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", - "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", "license": "MIT" }, "node_modules/numbered": { @@ -5256,9 +5954,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", - "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "license": "MIT", "engines": { "node": ">=12" @@ -5278,9 +5976,9 @@ } }, "node_modules/postcss": { - "version": "8.5.3", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", - "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", + "version": "8.5.6", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", "funding": [ { "type": "opencollective", @@ -5297,7 +5995,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.8", + "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -5316,9 +6014,9 @@ } }, "node_modules/prettier": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.3.tgz", - "integrity": "sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==", + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.7.4.tgz", + "integrity": "sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==", "dev": true, "license": "MIT", "bin": { @@ -5423,7 +6121,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -5437,7 +6134,6 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz", "integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -5450,16 +6146,16 @@ } }, "node_modules/react-i18next": { - "version": "15.5.1", - "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.5.1.tgz", - "integrity": "sha512-C8RZ7N7H0L+flitiX6ASjq9p5puVJU1Z8VyL3OgM/QOMRf40BMZX+5TkpxzZVcTmOLPX5zlti4InEX5pFyiVeA==", + "version": "15.7.4", + "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-15.7.4.tgz", + "integrity": "sha512-nyU8iKNrI5uDJch0z9+Y5XEr34b0wkyYj3Rp+tfbahxtlswxSCjcUL9H0nqXo9IR3/t5Y5PKIA3fx3MfUyR9Xw==", "license": "MIT", "dependencies": { - "@babel/runtime": "^7.25.0", + "@babel/runtime": "^7.27.6", "html-parse-stringify": "^3.0.1" }, "peerDependencies": { - "i18next": ">= 23.2.3", + "i18next": ">= 23.4.0", "react": ">= 16.8.0", "typescript": "^5" }, @@ -5485,9 +6181,9 @@ } }, "node_modules/react-is": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.1.0.tgz", - "integrity": "sha512-Oe56aUPnkHyyDxxkvqtd7KkdQP5uIUfHxd5XTb3wE9d/kRnZLmKbDB0GWk919tdQ+mxxPtG6EAs6RMT6i1qtHg==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.3.tgz", + "integrity": "sha512-qJNJfu81ByyabuG7hPFEbXqNcWSU3+eVus+KJs+0ncpGfMyYdvSmxiJxbWR65lYi1I+/0HBcliO029gc4F+PnA==", "license": "MIT" }, "node_modules/react-redux": { @@ -5524,12 +6220,12 @@ } }, "node_modules/react-router": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", - "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0" + "@remix-run/router": "1.23.2" }, "engines": { "node": ">=14.0.0" @@ -5539,13 +6235,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", - "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0", - "react-router": "6.30.1" + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" }, "engines": { "node": ">=14.0.0" @@ -5641,8 +6337,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-persist": { "version": "6.0.0", @@ -5713,12 +6408,12 @@ "license": "MIT" }, "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", "license": "MIT", "dependencies": { - "is-core-module": "^2.16.0", + "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, @@ -5776,13 +6471,12 @@ "license": "Unlicense" }, "node_modules/rollup": { - "version": "4.41.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz", - "integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==", + "version": "4.55.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", + "integrity": "sha512-wDv/Ht1BNHB4upNbK74s9usvl7hObDnvVzknxqY/E/O3X6rW1U1rV1aENEfJ54eFZDTNo7zv1f5N4edCluH7+A==", "license": "MIT", - "peer": true, "dependencies": { - "@types/estree": "1.0.7" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -5792,26 +6486,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.41.0", - "@rollup/rollup-android-arm64": "4.41.0", - "@rollup/rollup-darwin-arm64": "4.41.0", - "@rollup/rollup-darwin-x64": "4.41.0", - "@rollup/rollup-freebsd-arm64": "4.41.0", - "@rollup/rollup-freebsd-x64": "4.41.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.41.0", - "@rollup/rollup-linux-arm-musleabihf": "4.41.0", - "@rollup/rollup-linux-arm64-gnu": "4.41.0", - "@rollup/rollup-linux-arm64-musl": "4.41.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.41.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.41.0", - "@rollup/rollup-linux-riscv64-gnu": "4.41.0", - "@rollup/rollup-linux-riscv64-musl": "4.41.0", - "@rollup/rollup-linux-s390x-gnu": "4.41.0", - "@rollup/rollup-linux-x64-gnu": "4.41.0", - "@rollup/rollup-linux-x64-musl": "4.41.0", - "@rollup/rollup-win32-arm64-msvc": "4.41.0", - "@rollup/rollup-win32-ia32-msvc": "4.41.0", - "@rollup/rollup-win32-x64-msvc": "4.41.0", + "@rollup/rollup-android-arm-eabi": "4.55.1", + "@rollup/rollup-android-arm64": "4.55.1", + "@rollup/rollup-darwin-arm64": "4.55.1", + "@rollup/rollup-darwin-x64": "4.55.1", + "@rollup/rollup-freebsd-arm64": "4.55.1", + "@rollup/rollup-freebsd-x64": "4.55.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.1", + "@rollup/rollup-linux-arm-musleabihf": "4.55.1", + "@rollup/rollup-linux-arm64-gnu": "4.55.1", + "@rollup/rollup-linux-arm64-musl": "4.55.1", + "@rollup/rollup-linux-loong64-gnu": "4.55.1", + "@rollup/rollup-linux-loong64-musl": "4.55.1", + "@rollup/rollup-linux-ppc64-gnu": "4.55.1", + "@rollup/rollup-linux-ppc64-musl": "4.55.1", + "@rollup/rollup-linux-riscv64-gnu": "4.55.1", + "@rollup/rollup-linux-riscv64-musl": "4.55.1", + "@rollup/rollup-linux-s390x-gnu": "4.55.1", + "@rollup/rollup-linux-x64-gnu": "4.55.1", + "@rollup/rollup-linux-x64-musl": "4.55.1", + "@rollup/rollup-openbsd-x64": "4.55.1", + "@rollup/rollup-openharmony-arm64": "4.55.1", + "@rollup/rollup-win32-arm64-msvc": "4.55.1", + "@rollup/rollup-win32-ia32-msvc": "4.55.1", + "@rollup/rollup-win32-x64-gnu": "4.55.1", + "@rollup/rollup-win32-x64-msvc": "4.55.1", "fsevents": "~2.3.2" } }, @@ -6088,6 +6787,20 @@ "node": ">=0.10.0" } }, + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/string.prototype.matchall": { "version": "4.0.12", "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", @@ -6285,13 +6998,13 @@ "license": "MIT" }, "node_modules/tinyglobby": { - "version": "0.2.14", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", - "integrity": "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==", + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", "license": "MIT", "dependencies": { - "fdir": "^6.4.4", - "picomatch": "^4.0.2" + "fdir": "^6.5.0", + "picomatch": "^4.0.3" }, "engines": { "node": ">=12.0.0" @@ -6416,7 +7129,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -6452,9 +7164,9 @@ "license": "MIT" }, "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", "funding": [ { "type": "opencollective", @@ -6492,9 +7204,9 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.5.0.tgz", - "integrity": "sha512-Rb46I4cGGVBmjamjphe8L/UnvJD+uPPtTkNvX5mZgqdbavhI4EbgIWJiIHXJ8bc/i9EQGPRh4DwEURJ552Do0A==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" @@ -6536,7 +7248,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", @@ -6607,12 +7318,12 @@ } }, "node_modules/vite-plugin-svgr": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.3.0.tgz", - "integrity": "sha512-Jy9qLB2/PyWklpYy0xk0UU3TlU0t2UMpJXZvf+hWII1lAmRHrOUKi11Uw8N3rxoNk7atZNYO3pR3vI1f7oi+6w==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/vite-plugin-svgr/-/vite-plugin-svgr-4.5.0.tgz", + "integrity": "sha512-W+uoSpmVkSmNOGPSsDCWVW/DDAyv+9fap9AZXBvWiQqrboJ08j2vh0tFxTD/LjwqwAd3yYSVJgm54S/1GhbdnA==", "license": "MIT", "dependencies": { - "@rollup/pluginutils": "^5.1.3", + "@rollup/pluginutils": "^5.2.0", "@svgr/core": "^8.1.0", "@svgr/plugin-jsx": "^8.1.0" }, @@ -6757,6 +7468,23 @@ "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "license": "ISC" }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "license": "ISC", + "optional": true, + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index b988b9e2c..e950dfd41 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -153,29 +153,20 @@ const Settings = () => { return; } - // Validate - const { error } = settingsValidation.validate(newSettingsData.settings, { - abortEarly: false, - }); - if (!error || error.details.length === 0) { - setErrors({}); - } else { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - setErrors(newErrors); - } - setSettingsData(newSettingsData); // Update Redux immediately for UI feedback if (name === "showURL") { - dispatch(setShowURL(newValue)); - } -}; + dispatch(setShowURL(newValue)); + } + }; -const handleSave = () => { + const handleSave = () => { + // Validate + const { error } = settingsValidation.validate(settingsData.settings, { + abortEarly: false, + }); + if (!error || error.details.length === 0) { setErrors({}); } else { const newErrors = {}; From 1c1ac25857eb0a2db2fb8e98b0bc7b19939ea2a9 Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Mon, 12 Jan 2026 08:56:24 +0000 Subject: [PATCH 189/400] fix saveSettings is currently being called even when validation fails. --- client/src/Pages/v1/Settings/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/v1/Settings/index.jsx index e950dfd41..c37a8b1bc 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/v1/Settings/index.jsx @@ -168,6 +168,7 @@ const Settings = () => { }); if (!error || error.details.length === 0) { setErrors({}); + saveSettings(settingsData?.settings); } else { const newErrors = {}; error.details.forEach((err) => { @@ -176,7 +177,6 @@ const Settings = () => { setErrors(newErrors); } - saveSettings(settingsData?.settings); }; return ( From e090d3cfd3e78ebac3a6cc61bb58595330650b63 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 12 Jan 2026 19:32:23 +0000 Subject: [PATCH 190/400] -> timeseries --- server/package-lock.json | 97 ++++ server/package.json | 1 + server/src/app.js | 1 - server/src/config/controllers.js | 47 -- server/src/config/controllers.ts | 35 ++ server/src/config/{routes.js => routes.ts} | 6 +- server/src/config/services.js | 84 +-- server/src/controllers/authController.ts | 233 +++++++++ .../controllers/{v1 => }/checkController.ts | 0 .../controllers/{v1 => }/controllerUtils.ts | 0 .../{v1 => }/diagnosticController.ts | 0 .../{v1 => }/incidentController.ts | 0 .../controllers/{v1 => }/inviteController.ts | 0 .../src/controllers/{v1 => }/logController.ts | 0 .../{v1 => }/maintenanceWindowController.ts | 0 .../controllers/{v1 => }/monitorController.ts | 2 +- .../{v1 => }/notificationController.ts | 0 .../controllers/{v1 => }/queueController.ts | 0 .../{v1 => }/settingsController.ts | 0 .../{v1 => }/statusPageController.ts | 0 .../controllers/v1/announcementsController.js | 77 --- server/src/controllers/v1/authController.js | 478 ------------------ server/src/controllers/v1/baseController.js | 83 --- server/src/db/{v1 => }/MongoDB.js | 0 .../0001_migrateStatusWindowThreshold.ts | 10 + .../0002_convertChecksToTimeSeries.ts | 93 ++++ server/src/db/migration/index.ts | 51 ++ server/src/db/{v1 => }/models/AppSettings.js | 0 server/src/db/models/Check.ts | 214 ++++++++ server/src/db/{v1 => }/models/Incident.js | 0 server/src/db/{v1 => }/models/InviteToken.js | 0 .../db/{v1 => }/models/MaintenanceWindow.js | 0 server/src/db/models/Migration.ts | 13 + server/src/db/{v1 => }/models/Monitor.js | 0 server/src/db/{v1 => }/models/MonitorStats.js | 0 server/src/db/{v1 => }/models/Notification.js | 0 .../src/db/{v1 => }/models/RecoveryToken.js | 0 server/src/db/{v1 => }/models/StatusPage.js | 0 server/src/db/{v1 => }/models/Team.js | 0 server/src/db/{v1 => }/models/User.js | 0 server/src/db/{v1 => }/modules/checkModule.js | 0 .../db/{v1 => }/modules/checkModuleQueries.js | 0 .../src/db/{v1 => }/modules/incidentModule.js | 0 .../src/db/{v1 => }/modules/inviteModule.js | 0 .../modules/maintenanceWindowModule.js | 0 .../src/db/{v1 => }/modules/monitorModule.js | 0 .../{v1 => }/modules/monitorModuleQueries.js | 0 .../db/{v1 => }/modules/notificationModule.js | 0 .../src/db/{v1 => }/modules/recoveryModule.js | 0 .../src/db/{v1 => }/modules/settingsModule.js | 0 .../db/{v1 => }/modules/statusPageModule.js | 0 server/src/db/{v1 => }/modules/userModule.js | 0 server/src/db/{v1 => }/utils/seedDb.js | 0 .../0001_migrateStatusWindowThreshold.ts | 18 - server/src/db/v1/migration/index.ts | 7 - server/src/db/v1/models/Check.js | 178 ------- server/src/index.js | 7 +- server/src/middleware/v1/handleErrors.js | 4 +- server/src/middleware/v1/isAllowed.js | 6 +- server/src/middleware/v1/verifyJWT.js | 6 +- server/src/middleware/v1/verifyOwnership.js | 4 +- .../repositories/checks/IChecksRepository.ts | 0 .../checks/MongoChecksRepistory.ts | 0 .../monitors/IMonitorsRepository.ts | 9 + .../monitors/MongoMonitorsRepository.ts | 5 + server/src/routes/v1/announcementsRoute.js | 33 -- server/src/routes/v1/monitorRoute.js | 2 +- .../service/{v1 => }/business/checkService.js | 0 .../{v1 => }/business/diagnosticService.js | 0 .../{v1 => }/business/incidentService.js | 0 .../{v1 => }/business/inviteService.js | 0 .../business/maintenanceWindowService.js | 0 .../{v1 => }/business/monitorService.js | 2 +- .../service/{v1 => }/business/userService.js | 0 .../SuperSimpleQueue/SuperSimpleQueue.js | 0 .../SuperSimpleQueueHelper.js | 0 .../{v1 => }/infrastructure/bufferService.js | 0 .../{v1 => }/infrastructure/emailService.js | 0 .../{v1 => }/infrastructure/errorService.js | 0 .../{v1 => }/infrastructure/networkService.js | 0 .../notificationProviders/matrix.js | 0 .../infrastructure/notificationService.js | 0 .../infrastructure/notificationUtils.js | 0 .../{v1 => }/infrastructure/statusService.js | 12 +- .../{v1 => }/system/serviceRegistry.js | 0 .../{v1 => }/system/settingsService.js | 0 .../service/{v1 => }/system/stringService.js | 0 .../{v1 => }/system/translationService.js | 0 server/src/types/check.ts | 79 +++ server/src/types/index.ts | 2 + server/src/types/monitor.ts | 29 ++ 91 files changed, 941 insertions(+), 987 deletions(-) delete mode 100644 server/src/config/controllers.js create mode 100644 server/src/config/controllers.ts rename server/src/config/{routes.js => routes.ts} (92%) create mode 100755 server/src/controllers/authController.ts rename server/src/controllers/{v1 => }/checkController.ts (100%) rename server/src/controllers/{v1 => }/controllerUtils.ts (100%) rename server/src/controllers/{v1 => }/diagnosticController.ts (100%) rename server/src/controllers/{v1 => }/incidentController.ts (100%) rename server/src/controllers/{v1 => }/inviteController.ts (100%) rename server/src/controllers/{v1 => }/logController.ts (100%) rename server/src/controllers/{v1 => }/maintenanceWindowController.ts (100%) rename server/src/controllers/{v1 => }/monitorController.ts (99%) rename server/src/controllers/{v1 => }/notificationController.ts (100%) rename server/src/controllers/{v1 => }/queueController.ts (100%) rename server/src/controllers/{v1 => }/settingsController.ts (100%) rename server/src/controllers/{v1 => }/statusPageController.ts (100%) delete mode 100755 server/src/controllers/v1/announcementsController.js delete mode 100755 server/src/controllers/v1/authController.js delete mode 100644 server/src/controllers/v1/baseController.js rename server/src/db/{v1 => }/MongoDB.js (100%) create mode 100644 server/src/db/migration/0001_migrateStatusWindowThreshold.ts create mode 100644 server/src/db/migration/0002_convertChecksToTimeSeries.ts create mode 100644 server/src/db/migration/index.ts rename server/src/db/{v1 => }/models/AppSettings.js (100%) create mode 100755 server/src/db/models/Check.ts rename server/src/db/{v1 => }/models/Incident.js (100%) rename server/src/db/{v1 => }/models/InviteToken.js (100%) rename server/src/db/{v1 => }/models/MaintenanceWindow.js (100%) create mode 100644 server/src/db/models/Migration.ts rename server/src/db/{v1 => }/models/Monitor.js (100%) rename server/src/db/{v1 => }/models/MonitorStats.js (100%) rename server/src/db/{v1 => }/models/Notification.js (100%) rename server/src/db/{v1 => }/models/RecoveryToken.js (100%) rename server/src/db/{v1 => }/models/StatusPage.js (100%) rename server/src/db/{v1 => }/models/Team.js (100%) rename server/src/db/{v1 => }/models/User.js (100%) rename server/src/db/{v1 => }/modules/checkModule.js (100%) rename server/src/db/{v1 => }/modules/checkModuleQueries.js (100%) rename server/src/db/{v1 => }/modules/incidentModule.js (100%) rename server/src/db/{v1 => }/modules/inviteModule.js (100%) rename server/src/db/{v1 => }/modules/maintenanceWindowModule.js (100%) rename server/src/db/{v1 => }/modules/monitorModule.js (100%) rename server/src/db/{v1 => }/modules/monitorModuleQueries.js (100%) rename server/src/db/{v1 => }/modules/notificationModule.js (100%) rename server/src/db/{v1 => }/modules/recoveryModule.js (100%) rename server/src/db/{v1 => }/modules/settingsModule.js (100%) rename server/src/db/{v1 => }/modules/statusPageModule.js (100%) rename server/src/db/{v1 => }/modules/userModule.js (100%) rename server/src/db/{v1 => }/utils/seedDb.js (100%) delete mode 100644 server/src/db/v1/migration/0001_migrateStatusWindowThreshold.ts delete mode 100644 server/src/db/v1/migration/index.ts delete mode 100755 server/src/db/v1/models/Check.js create mode 100644 server/src/repositories/checks/IChecksRepository.ts create mode 100644 server/src/repositories/checks/MongoChecksRepistory.ts create mode 100644 server/src/repositories/monitors/IMonitorsRepository.ts create mode 100644 server/src/repositories/monitors/MongoMonitorsRepository.ts delete mode 100755 server/src/routes/v1/announcementsRoute.js rename server/src/service/{v1 => }/business/checkService.js (100%) rename server/src/service/{v1 => }/business/diagnosticService.js (100%) rename server/src/service/{v1 => }/business/incidentService.js (100%) rename server/src/service/{v1 => }/business/inviteService.js (100%) rename server/src/service/{v1 => }/business/maintenanceWindowService.js (100%) rename server/src/service/{v1 => }/business/monitorService.js (99%) rename server/src/service/{v1 => }/business/userService.js (100%) rename server/src/service/{v1 => }/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js (100%) rename server/src/service/{v1 => }/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js (100%) rename server/src/service/{v1 => }/infrastructure/bufferService.js (100%) rename server/src/service/{v1 => }/infrastructure/emailService.js (100%) rename server/src/service/{v1 => }/infrastructure/errorService.js (100%) rename server/src/service/{v1 => }/infrastructure/networkService.js (100%) rename server/src/service/{v1 => }/infrastructure/notificationProviders/matrix.js (100%) rename server/src/service/{v1 => }/infrastructure/notificationService.js (100%) rename server/src/service/{v1 => }/infrastructure/notificationUtils.js (100%) rename server/src/service/{v1 => }/infrastructure/statusService.js (98%) rename server/src/service/{v1 => }/system/serviceRegistry.js (100%) rename server/src/service/{v1 => }/system/settingsService.js (100%) rename server/src/service/{v1 => }/system/stringService.js (100%) rename server/src/service/{v1 => }/system/translationService.js (100%) create mode 100644 server/src/types/check.ts create mode 100644 server/src/types/index.ts create mode 100644 server/src/types/monitor.ts diff --git a/server/package-lock.json b/server/package-lock.json index e08248989..eaf771a61 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -57,6 +57,7 @@ "esm": "3.2.25", "globals": "^15.14.0", "mocha": "11.1.0", + "nodemon": "^3.1.11", "prettier": "^3.3.3", "sinon": "19.0.2", "tsc-alias": "1.8.16", @@ -6732,6 +6733,13 @@ "node": ">= 4" } }, + "node_modules/ignore-by-default": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz", + "integrity": "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==", + "dev": true, + "license": "ISC" + }, "node_modules/import-fresh": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", @@ -8745,6 +8753,58 @@ "node": ">=6.0.0" } }, + "node_modules/nodemon": { + "version": "3.1.11", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.11.tgz", + "integrity": "sha512-is96t8F/1//UHAjNPHpbsNY46ELPpftGUoSVNXwUfMk/qdjSylYrWSu1XavVTBOn526kFiOR733ATgNBCQyH0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^3.5.2", + "debug": "^4", + "ignore-by-default": "^1.0.1", + "minimatch": "^3.1.2", + "pstree.remy": "^1.1.8", + "semver": "^7.5.3", + "simple-update-notifier": "^2.0.0", + "supports-color": "^5.5.0", + "touch": "^3.1.0", + "undefsafe": "^2.0.5" + }, + "bin": { + "nodemon": "bin/nodemon.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/nodemon" + } + }, + "node_modules/nodemon/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/nodemon/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -9797,6 +9857,13 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "license": "MIT" }, + "node_modules/pstree.remy": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/pstree.remy/-/pstree.remy-1.1.8.tgz", + "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", + "dev": true, + "license": "MIT" + }, "node_modules/pump": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", @@ -10424,6 +10491,19 @@ "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==", "license": "MIT" }, + "node_modules/simple-update-notifier": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-2.0.0.tgz", + "integrity": "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/sinon": { "version": "19.0.2", "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", @@ -11046,6 +11126,16 @@ "node": ">=0.6" } }, + "node_modules/touch": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.1.tgz", + "integrity": "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA==", + "dev": true, + "license": "ISC", + "bin": { + "nodetouch": "bin/nodetouch.js" + } + }, "node_modules/tough-cookie": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", @@ -11225,6 +11315,13 @@ "node": ">=0.8.0" } }, + "node_modules/undefsafe": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", + "integrity": "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA==", + "dev": true, + "license": "MIT" + }, "node_modules/undici": { "version": "6.22.0", "resolved": "https://registry.npmjs.org/undici/-/undici-6.22.0.tgz", diff --git a/server/package.json b/server/package.json index 050dd8e66..5610d32a9 100755 --- a/server/package.json +++ b/server/package.json @@ -69,6 +69,7 @@ "esm": "3.2.25", "globals": "^15.14.0", "mocha": "11.1.0", + "nodemon": "^3.1.11", "prettier": "^3.3.3", "sinon": "19.0.2", "tsc-alias": "1.8.16", diff --git a/server/src/app.js b/server/src/app.js index 4578de896..d782d07eb 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -14,7 +14,6 @@ import { sanitizeBody, sanitizeQuery } from "./middleware/v1/sanitization.js"; export const createApp = ({ services, controllers, envSettings, frontendPath, openApiSpec }) => { const allowedOrigin = envSettings.clientHost; - const app = express(); app.use(generalApiLimiter); // Static files diff --git a/server/src/config/controllers.js b/server/src/config/controllers.js deleted file mode 100644 index afd558930..000000000 --- a/server/src/config/controllers.js +++ /dev/null @@ -1,47 +0,0 @@ -import { createCommonDependencies } from "../controllers/v1/baseController.js"; - -// Services - -// V1 Controllers -import MonitorController from "../controllers/v1/monitorController.js"; -import AuthController from "../controllers/v1/authController.js"; -import SettingsController from "../controllers/v1/settingsController.js"; -import CheckController from "../controllers/v1/checkController.js"; -import InviteController from "../controllers/v1/inviteController.js"; -import MaintenanceWindowController from "../controllers/v1/maintenanceWindowController.js"; -import QueueController from "../controllers/v1/queueController.js"; -import LogController from "../controllers/v1/logController.js"; -import StatusPageController from "../controllers/v1/statusPageController.js"; -import NotificationController from "../controllers/v1/notificationController.js"; -import DiagnosticController from "../controllers/v1/diagnosticController.js"; -import IncidentController from "../controllers/v1/incidentController.js"; - -export const initializeControllers = (services) => { - const controllers = {}; - const commonDependencies = createCommonDependencies(services.db, services.errorService, services.logger, services.stringService); - - // V1 - controllers.authController = new AuthController(commonDependencies, { - settingsService: services.settingsService, - emailService: services.emailService, - jobQueue: services.jobQueue, - userService: services.userService, - }); - - controllers.monitorController = new MonitorController(services.monitorService); - - controllers.settingsController = new SettingsController(services.settingsService, services.emailService, services.db); - controllers.checkController = new CheckController(services.checkService); - controllers.inviteController = new InviteController(services.inviteService); - - controllers.maintenanceWindowController = new MaintenanceWindowController(services.maintenanceWindowService); - controllers.queueController = new QueueController(services.jobQueue); - controllers.logController = new LogController(services.logger); - controllers.statusPageController = new StatusPageController(services.db); - controllers.notificationController = new NotificationController(services.notificationService, services.db); - controllers.diagnosticController = new DiagnosticController(services.diagnosticService); - - controllers.incidentController = new IncidentController(services.incidentService); - - return controllers; -}; diff --git a/server/src/config/controllers.ts b/server/src/config/controllers.ts new file mode 100644 index 000000000..9fe834348 --- /dev/null +++ b/server/src/config/controllers.ts @@ -0,0 +1,35 @@ +import MonitorController from "../controllers/monitorController.js"; +import AuthController from "../controllers/authController.js"; +import SettingsController from "../controllers/settingsController.js"; +import CheckController from "../controllers/checkController.js"; +import InviteController from "../controllers/inviteController.js"; +import MaintenanceWindowController from "../controllers/maintenanceWindowController.js"; +import QueueController from "../controllers/queueController.js"; +import LogController from "../controllers/logController.js"; +import StatusPageController from "../controllers/statusPageController.js"; +import NotificationController from "../controllers/notificationController.js"; +import DiagnosticController from "../controllers/diagnosticController.js"; +import IncidentController from "../controllers/incidentController.js"; + +export const initializeControllers = (services: any) => { + const controllers: Record = {}; + + controllers.authController = new AuthController(services.userService); + + controllers.monitorController = new MonitorController(services.monitorService); + + controllers.settingsController = new SettingsController(services.settingsService, services.emailService, services.db); + controllers.checkController = new CheckController(services.checkService); + controllers.inviteController = new InviteController(services.inviteService); + + controllers.maintenanceWindowController = new MaintenanceWindowController(services.maintenanceWindowService); + controllers.queueController = new QueueController(services.jobQueue); + controllers.logController = new LogController(services.logger); + controllers.statusPageController = new StatusPageController(services.db); + controllers.notificationController = new NotificationController(services.notificationService, services.db); + controllers.diagnosticController = new DiagnosticController(services.diagnosticService); + + controllers.incidentController = new IncidentController(services.incidentService); + + return controllers; +}; diff --git a/server/src/config/routes.js b/server/src/config/routes.ts similarity index 92% rename from server/src/config/routes.js rename to server/src/config/routes.ts index 97b2d6a1d..303dd4eb7 100644 --- a/server/src/config/routes.js +++ b/server/src/config/routes.ts @@ -2,7 +2,7 @@ import { verifyJWT } from "../middleware/v1/verifyJWT.js"; import { authApiLimiter } from "../middleware/v1/rateLimiter.js"; import AuthRoutes from "../routes/v1/authRoute.js"; -import InviteRoutes from "../routes/v1//inviteRoute.js"; +import InviteRoutes from "../routes/v1/inviteRoute.js"; import MonitorRoutes from "../routes/v1/monitorRoute.js"; import CheckRoutes from "../routes/v1/checkRoute.js"; import SettingsRoutes from "../routes/v1/settingsRoute.js"; @@ -10,12 +10,12 @@ import MaintenanceWindowRoutes from "../routes/v1/maintenanceWindowRoute.js"; import StatusPageRoutes from "../routes/v1/statusPageRoute.js"; import QueueRoutes from "../routes/v1/queueRoute.js"; import LogRoutes from "../routes/v1/logRoutes.js"; -import DiagnosticRoutes from "../routes/v1//diagnosticRoute.js"; +import DiagnosticRoutes from "../routes/v1/diagnosticRoute.js"; import NotificationRoutes from "../routes/v1/notificationRoute.js"; import IncidentRoutes from "../routes/v1/incidentRoute.js"; -export const setupRoutes = (app, controllers) => { +export const setupRoutes = (app: any, controllers: Record) => { // V1 const authRoutes = new AuthRoutes(controllers.authController); const monitorRoutes = new MonitorRoutes(controllers.monitorController); diff --git a/server/src/config/services.js b/server/src/config/services.js index 9744c647d..28b7c9a86 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -1,23 +1,23 @@ -import ServiceRegistry from "../service/v1/system/serviceRegistry.js"; -import TranslationService from "../service/v1/system/translationService.js"; -import StringService from "../service/v1/system/stringService.js"; -import MongoDB from "../db/v1/MongoDB.js"; -import NetworkService from "../service/v1/infrastructure/networkService.js"; -import EmailService from "../service/v1/infrastructure/emailService.js"; -import BufferService from "../service/v1/infrastructure/bufferService.js"; -import StatusService from "../service/v1/infrastructure/statusService.js"; -import NotificationUtils from "../service/v1/infrastructure/notificationUtils.js"; -import NotificationService from "../service/v1/infrastructure/notificationService.js"; -import ErrorService from "../service/v1/infrastructure/errorService.js"; -import SuperSimpleQueueHelper from "../service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; -import SuperSimpleQueue from "../service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; -import UserService from "../service/v1/business/userService.js"; -import CheckService from "../service/v1/business/checkService.js"; -import DiagnosticService from "../service/v1/business/diagnosticService.js"; -import InviteService from "../service/v1/business/inviteService.js"; -import MaintenanceWindowService from "../service/v1/business/maintenanceWindowService.js"; -import MonitorService from "../service/v1/business/monitorService.js"; -import IncidentService from "../service/v1/business/incidentService.js"; +import ServiceRegistry from "../service/system/serviceRegistry.js"; +import TranslationService from "../service/system/translationService.js"; +import StringService from "../service/system/stringService.js"; +import MongoDB from "../db/MongoDB.js"; +import NetworkService from "../service/infrastructure/networkService.js"; +import EmailService from "../service/infrastructure/emailService.js"; +import BufferService from "../service/infrastructure/bufferService.js"; +import StatusService from "../service/infrastructure/statusService.js"; +import NotificationUtils from "../service/infrastructure/notificationUtils.js"; +import NotificationService from "../service/infrastructure/notificationService.js"; +import ErrorService from "../service/infrastructure/errorService.js"; +import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; +import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; +import UserService from "../service/business/userService.js"; +import CheckService from "../service/business/checkService.js"; +import DiagnosticService from "../service/business/diagnosticService.js"; +import InviteService from "../service/business/inviteService.js"; +import MaintenanceWindowService from "../service/business/maintenanceWindowService.js"; +import MonitorService from "../service/business/monitorService.js"; +import IncidentService from "../service/business/incidentService.js"; import papaparse from "papaparse"; import axios from "axios"; import got from "got"; @@ -47,29 +47,29 @@ import { GenerateAvatarImage } from "../utils/imageProcessing.js"; import { ParseBoolean } from "../utils/utils.js"; // Models -import Check from "../db/v1/models/Check.js"; -import Monitor from "../db/v1/models/Monitor.js"; -import User from "../db/v1/models/User.js"; -import InviteToken from "../db/v1/models/InviteToken.js"; -import StatusPage from "../db/v1/models/StatusPage.js"; -import Team from "../db/v1/models/Team.js"; -import MaintenanceWindow from "../db/v1/models/MaintenanceWindow.js"; -import MonitorStats from "../db/v1/models/MonitorStats.js"; -import Notification from "../db/v1/models/Notification.js"; -import RecoveryToken from "../db/v1/models/RecoveryToken.js"; -import AppSettings from "../db/v1/models/AppSettings.js"; -import Incident from "../db/v1/models/Incident.js"; +import Check from "../db/models/Check.js"; +import Monitor from "../db/models/Monitor.js"; +import User from "../db/models/User.js"; +import InviteToken from "../db/models/InviteToken.js"; +import StatusPage from "../db/models/StatusPage.js"; +import Team from "../db/models/Team.js"; +import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; +import MonitorStats from "../db/models/MonitorStats.js"; +import Notification from "../db/models/Notification.js"; +import RecoveryToken from "../db/models/RecoveryToken.js"; +import AppSettings from "../db/models/AppSettings.js"; +import Incident from "../db/models/Incident.js"; -import InviteModule from "../db/v1/modules/inviteModule.js"; -import CheckModule from "../db/v1/modules/checkModule.js"; -import StatusPageModule from "../db/v1/modules/statusPageModule.js"; -import UserModule from "../db/v1/modules/userModule.js"; -import MaintenanceWindowModule from "../db/v1/modules/maintenanceWindowModule.js"; -import MonitorModule from "../db/v1/modules/monitorModule.js"; -import NotificationModule from "../db/v1/modules/notificationModule.js"; -import RecoveryModule from "../db/v1/modules/recoveryModule.js"; -import SettingsModule from "../db/v1/modules/settingsModule.js"; -import IncidentModule from "../db/v1/modules/incidentModule.js"; +import InviteModule from "../db/modules/inviteModule.js"; +import CheckModule from "../db/modules/checkModule.js"; +import StatusPageModule from "../db/modules/statusPageModule.js"; +import UserModule from "../db/modules/userModule.js"; +import MaintenanceWindowModule from "../db/modules/maintenanceWindowModule.js"; +import MonitorModule from "../db/modules/monitorModule.js"; +import NotificationModule from "../db/modules/notificationModule.js"; +import RecoveryModule from "../db/modules/recoveryModule.js"; +import SettingsModule from "../db/modules/settingsModule.js"; +import IncidentModule from "../db/modules/incidentModule.js"; export const initializeServices = async ({ logger, envSettings, settingsService }) => { const serviceRegistry = new ServiceRegistry({ logger }); diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts new file mode 100755 index 000000000..d033aa39d --- /dev/null +++ b/server/src/controllers/authController.ts @@ -0,0 +1,233 @@ +import { Request, Response, NextFunction } from "express"; +import { AppError } from "@/utils/AppError.js"; + +import { + registrationBodyValidation, + loginValidation, + editUserBodyValidation, + recoveryValidation, + recoveryTokenBodyValidation, + newPasswordValidation, + getUserByIdParamValidation, + editUserByIdParamValidation, + editUserByIdBodyValidation, + editSuperadminUserByIdBodyValidation, + editUserPasswordByIdBodyValidation, +} from "@/validation/joi.js"; + +const SERVICE_NAME = "authController"; + +class AuthController { + static SERVICE_NAME = SERVICE_NAME; + + private userService: any; + + constructor(userService: any) { + this.userService = userService; + } + + get serviceName() { + return AuthController.SERVICE_NAME; + } + + registerUser = async (req: Request, res: Response, next: NextFunction) => { + try { + if (req.body?.email) { + req.body.email = req.body.email?.toLowerCase(); + } + await registrationBodyValidation.validateAsync(req.body); + const { user, token } = await this.userService.registerUser(req.body, req.file); + res.status(200).json({ + success: true, + msg: "User registered successfully", + data: { user, token }, + }); + } catch (error) { + next(error); + } + }; + + loginUser = async (req: Request, res: Response, next: NextFunction) => { + try { + if (req.body?.email) { + req.body.email = req.body.email?.toLowerCase(); + } + await loginValidation.validateAsync(req.body); + const { user, token } = await this.userService.loginUser(req.body.email, req.body.password); + + return res.status(200).json({ + success: true, + msg: "User logged in successfully", + data: { + user, + token, + }, + }); + } catch (error) { + next(error); + } + }; + + editUser = async (req: Request, res: Response, next: NextFunction) => { + try { + await editUserBodyValidation.validateAsync(req.body); + + const updatedUser = await this.userService.editUser(req.body, req.file, req.user); + + res.status(200).json({ + success: true, + msg: "User updated successfully", + data: updatedUser, + }); + } catch (error) { + next(error); + } + }; + + checkSuperadminExists = async (req: Request, res: Response, next: NextFunction) => { + try { + const superAdminExists = await this.userService.checkSuperadminExists(); + return res.status(200).json({ + success: true, + msg: "Superadmin existence checked successfully", + data: superAdminExists, + }); + } catch (error) { + next(error); + } + }; + + requestRecovery = async (req: Request, res: Response, next: NextFunction) => { + try { + await recoveryValidation.validateAsync(req.body); + const email = req?.body?.email; + const msgId = await this.userService.requestRecovery(email); + return res.status(200).json({ + success: true, + msg: "Password recovery email sent successfully", + data: msgId, + }); + } catch (error) { + next(error); + } + }; + + validateRecovery = async (req: Request, res: Response, next: NextFunction) => { + try { + await recoveryTokenBodyValidation.validateAsync(req.body); + await this.userService.validateRecovery(req.body.recoveryToken); + return res.status(200).json({ + success: true, + msg: "Recovery token is valid", + }); + } catch (error) { + next(error); + } + }; + + resetPassword = async (req: Request, res: Response, next: NextFunction) => { + try { + await newPasswordValidation.validateAsync(req.body); + const { user, token } = await this.userService.resetPassword(req.body.password, req.body.recoveryToken); + return res.status(200).json({ + success: true, + msg: "Password has been reset successfully", + data: { user, token }, + }); + } catch (error) { + next(error); + } + }; + + deleteUser = async (req: Request, res: Response, next: NextFunction) => { + try { + await this.userService.deleteUser(req.user); + return res.status(200).json({ + success: true, + msg: "User deleted successfully", + }); + } catch (error) { + next(error); + } + }; + + getAllUsers = async (req: Request, res: Response, next: NextFunction) => { + try { + const allUsers = await this.userService.getAllUsers(); + return res.status(200).json({ + success: true, + msg: "Users retrieved successfully", + data: allUsers, + }); + } catch (error) { + next(error); + } + }; + + getUserById = async (req: Request, res: Response, next: NextFunction) => { + try { + await getUserByIdParamValidation.validateAsync(req.params); + const userId = req?.params?.userId; + const roles = req?.user?.role; + + if (!userId) { + throw new Error("No user ID in request"); + } + + if (!roles || roles.length === 0) { + throw new Error("No roles in request"); + } + + const user = await this.userService.getUserById(roles, userId); + + return res.status(200).json({ success: true, msg: "ok", data: user }); + } catch (error) { + next(error); + } + }; + + editUserById = async (req: Request, res: Response, next: NextFunction) => { + try { + const roles = req?.user?.role; + if (!roles.includes("superadmin")) { + throw new AppError({ message: "Unauthorized", status: 403 }); + } + + const userId = req.params.userId; + const user = { ...req.body }; + + await editUserByIdParamValidation.validateAsync(req.params); + // If this is superadmin self edit, allow "superadmin" role + if (userId === req.user._id) { + await editSuperadminUserByIdBodyValidation.validateAsync(req.body); + } else { + await editUserByIdBodyValidation.validateAsync(req.body); + } + + await this.userService.editUserById(userId, user); + return res.status(200).json({ success: true, msg: "ok" }); + } catch (error) { + next(error); + } + }; + + editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => { + try { + const roles = req?.user?.role; + if (!roles.includes("superadmin")) { + throw new AppError({ message: "Unauthorized", status: 403 }); + } + + const userId = req.params.userId; + await editUserByIdParamValidation.validateAsync(req.params); + await editUserPasswordByIdBodyValidation.validateAsync(req.body); + const updatedPassword = req.body.password; + await this.userService.setPasswordByUserId(userId, updatedPassword); + return res.status(200).json({ success: true, msg: "Password reset successfully" }); + } catch (error) { + next(error); + } + }; +} + +export default AuthController; diff --git a/server/src/controllers/v1/checkController.ts b/server/src/controllers/checkController.ts similarity index 100% rename from server/src/controllers/v1/checkController.ts rename to server/src/controllers/checkController.ts diff --git a/server/src/controllers/v1/controllerUtils.ts b/server/src/controllers/controllerUtils.ts similarity index 100% rename from server/src/controllers/v1/controllerUtils.ts rename to server/src/controllers/controllerUtils.ts diff --git a/server/src/controllers/v1/diagnosticController.ts b/server/src/controllers/diagnosticController.ts similarity index 100% rename from server/src/controllers/v1/diagnosticController.ts rename to server/src/controllers/diagnosticController.ts diff --git a/server/src/controllers/v1/incidentController.ts b/server/src/controllers/incidentController.ts similarity index 100% rename from server/src/controllers/v1/incidentController.ts rename to server/src/controllers/incidentController.ts diff --git a/server/src/controllers/v1/inviteController.ts b/server/src/controllers/inviteController.ts similarity index 100% rename from server/src/controllers/v1/inviteController.ts rename to server/src/controllers/inviteController.ts diff --git a/server/src/controllers/v1/logController.ts b/server/src/controllers/logController.ts similarity index 100% rename from server/src/controllers/v1/logController.ts rename to server/src/controllers/logController.ts diff --git a/server/src/controllers/v1/maintenanceWindowController.ts b/server/src/controllers/maintenanceWindowController.ts similarity index 100% rename from server/src/controllers/v1/maintenanceWindowController.ts rename to server/src/controllers/maintenanceWindowController.ts diff --git a/server/src/controllers/v1/monitorController.ts b/server/src/controllers/monitorController.ts similarity index 99% rename from server/src/controllers/v1/monitorController.ts rename to server/src/controllers/monitorController.ts index aac59cd27..425979062 100644 --- a/server/src/controllers/v1/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -13,7 +13,7 @@ import { getCertificateParamValidation, getHardwareDetailsByIdParamValidation, getHardwareDetailsByIdQueryValidation, -} from "../../validation/joi.js"; +} from "@/validation/joi.js"; import sslChecker from "ssl-checker"; import { fetchMonitorCertificate } from "./controllerUtils.js"; import { AppError } from "@/utils/AppError.js"; diff --git a/server/src/controllers/v1/notificationController.ts b/server/src/controllers/notificationController.ts similarity index 100% rename from server/src/controllers/v1/notificationController.ts rename to server/src/controllers/notificationController.ts diff --git a/server/src/controllers/v1/queueController.ts b/server/src/controllers/queueController.ts similarity index 100% rename from server/src/controllers/v1/queueController.ts rename to server/src/controllers/queueController.ts diff --git a/server/src/controllers/v1/settingsController.ts b/server/src/controllers/settingsController.ts similarity index 100% rename from server/src/controllers/v1/settingsController.ts rename to server/src/controllers/settingsController.ts diff --git a/server/src/controllers/v1/statusPageController.ts b/server/src/controllers/statusPageController.ts similarity index 100% rename from server/src/controllers/v1/statusPageController.ts rename to server/src/controllers/statusPageController.ts diff --git a/server/src/controllers/v1/announcementsController.js b/server/src/controllers/v1/announcementsController.js deleted file mode 100755 index 0425b44aa..000000000 --- a/server/src/controllers/v1/announcementsController.js +++ /dev/null @@ -1,77 +0,0 @@ -import { createAnnouncementValidation } from "../../validation/joi.js"; -import BaseController from "./baseController.js"; - -const SERVICE_NAME = "announcementController"; - -/** - * Controller for managing announcements in the system. - * This class handles the creation of new announcements. - * - * @class AnnouncementController - */ - -class AnnouncementController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - constructor(commonDependencies) { - super(commonDependencies); - this.createAnnouncement = this.createAnnouncement.bind(this); - this.getAnnouncement = this.getAnnouncement.bind(this); - } - - get serviceName() { - return AnnouncementController.SERVICE_NAME; - } - - /** - * Handles the creation of a new announcement. - * - * @async - * @param {Object} req - The request object, containing the announcement data in the body. - * @param {Object} res - The response object used to send the result back to the client. - * @param {Function} next - The next middleware function in the stack for error handling. - * - * @returns {Promise} A promise that resolves once the response is sent. - */ - createAnnouncement = asyncHandler( - async (req, res, next) => { - await createAnnouncementValidation.validateAsync(req.body); - const { title, message } = req.body; - const announcementData = { - title: title.trim(), - message: message.trim(), - userId: req.user._id, - }; - - const newAnnouncement = await this.db.createAnnouncement(announcementData); - return res.success({ - msg: this.stringService.createAnnouncement, - data: newAnnouncement, - }); - }, - SERVICE_NAME, - "createAnnouncement" - ); - - /** - * Handles retrieving announcements with pagination. - * - * @async - * @param {Object} res - The response object used to send the result back to the client. - * - `data`: The list of announcements to be sent back to the client. - * - `msg`: A message about the success of the request. - * @param {Function} next - The next middleware function in the stack for error handling. - */ - getAnnouncement = asyncHandler( - async (req, res, next) => { - const allAnnouncements = await this.db.getAnnouncements(); - return res.success({ - msg: this.stringService.getAnnouncement, - data: allAnnouncements, - }); - }, - SERVICE_NAME, - "getAnnouncement" - ); -} - -export default AnnouncementController; diff --git a/server/src/controllers/v1/authController.js b/server/src/controllers/v1/authController.js deleted file mode 100755 index e52de3849..000000000 --- a/server/src/controllers/v1/authController.js +++ /dev/null @@ -1,478 +0,0 @@ -import BaseController from "./baseController.js"; -import { - registrationBodyValidation, - loginValidation, - editUserBodyValidation, - recoveryValidation, - recoveryTokenBodyValidation, - newPasswordValidation, - getUserByIdParamValidation, - editUserByIdParamValidation, - editUserByIdBodyValidation, - editSuperadminUserByIdBodyValidation, - editUserPasswordByIdBodyValidation, -} from "../../validation/joi.js"; - -const SERVICE_NAME = "authController"; -/** - * Authentication Controller - * - * Handles all authentication-related HTTP requests including user registration, - * login, password recovery, and user management operations. - * - * @class AuthController - * @description Manages user authentication and authorization operations - */ -class AuthController extends BaseController { - static SERVICE_NAME = SERVICE_NAME; - /** - * Creates an instance of AuthController. - * - * @param {Object} commonDependencies - Common dependencies injected into the controller - * @param {Object} dependencies - The dependencies required by the controller - * @param {Object} dependencies.settingsService - Service for application settings - * @param {Object} dependencies.emailService - Service for email operations - * @param {Object} dependencies.jobQueue - Service for job queue operations - * @param {Object} dependencies.userService - User business logic service - */ - constructor(commonDependencies, { settingsService, emailService, jobQueue, userService }) { - super(commonDependencies); - this.settingsService = settingsService; - this.emailService = emailService; - this.jobQueue = jobQueue; - this.userService = userService; - } - - get serviceName() { - return AuthController.SERVICE_NAME; - } - - /** - * Registers a new user in the system. - * - * @async - * @function registerUser - * @param {Object} req - Express request object - * @param {Object} req.body - Request body containing user registration data - * @param {string} req.body.firstName - User's first name - * @param {string} req.body.lastName - User's last name - * @param {string} req.body.email - User's email address (will be converted to lowercase) - * @param {string} req.body.password - User's password - * @param {string} [req.body.inviteToken] - Invite token for registration (required if superadmin exists) - * @param {string} [req.body.teamId] - Team ID (auto-assigned if superadmin) - * @param {Array} [req.body.role] - User roles (auto-assigned if superadmin) - * @param {Object} [req.file] - Profile image file uploaded via multer - * @param {Object} res - Express response object - * @returns {Promise} Success response with user data and JWT token - * @throws {Error} 422 - Validation error if request body is invalid - * @throws {Error} 409 - Conflict if user already exists - * @example - * // Register first user (becomes superadmin) - * POST /auth/register - * { - * "firstName": "John", - * "lastName": "Doe", - * "email": "john@example.com", - * "password": "SecurePass123!" - * } - * - * // Register subsequent user (requires invite token) - * POST /auth/register - * { - * "firstName": "Jane", - * "lastName": "Smith", - * "email": "jane@example.com", - * "password": "SecurePass123!", - * "inviteToken": "abc123..." - * } - */ - registerUser = this.asyncHandler( - async (req, res) => { - if (req.body?.email) { - req.body.email = req.body.email?.toLowerCase(); - } - await registrationBodyValidation.validateAsync(req.body); - const { user, token } = await this.userService.registerUser(req.body, req.file); - res.success({ - msg: this.stringService.authCreateUser, - data: { user, token }, - }); - }, - SERVICE_NAME, - "registerUser" - ); - - /** - * Authenticates a user and returns a JWT token. - * - * @async - * @function loginUser - * @param {Object} req - Express request object - * @param {Object} req.body - Request body containing login credentials - * @param {string} req.body.email - User's email address (will be converted to lowercase) - * @param {string} req.body.password - User's password - * @param {Object} res - Express response object - * @returns {Promise} Success response with user data and JWT token - * @throws {Error} 422 - Validation error if request body is invalid - * @throws {Error} 401 - Unauthorized if credentials are incorrect - * @example - * POST /auth/login - * { - * "email": "john@example.com", - * "password": "SecurePass123!" - * } - */ - loginUser = this.asyncHandler( - async (req, res) => { - if (req.body?.email) { - req.body.email = req.body.email?.toLowerCase(); - } - await loginValidation.validateAsync(req.body); - const { user, token } = await this.userService.loginUser(req.body.email, req.body.password); - - return res.success({ - msg: this.stringService.authLoginUser, - data: { - user, - token, - }, - }); - }, - SERVICE_NAME, - "loginUser" - ); - - /** - * Updates the current user's profile information. - * - * @async - * @function editUser - * @param {Object} req - Express request object - * @param {Object} req.body - Request body containing user update data - * @param {string} [req.body.firstName] - Updated first name - * @param {string} [req.body.lastName] - Updated last name - * @param {string} [req.body.password] - Current password (required for password change) - * @param {string} [req.body.newPassword] - New password (required for password change) - * @param {boolean} [req.body.deleteProfileImage] - Flag to delete profile image - * @param {Object} [req.file] - New profile image file - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {Object} res - Express response object - * @returns {Promise} Success response with updated user data - * @throws {Error} 422 - Validation error if request body is invalid - * @throws {Error} 403 - Forbidden if current password is incorrect - * @example - * PUT /auth/user - * { - * "firstName": "John Updated", - * "lastName": "Doe Updated" - * } - * - * // Change password - * PUT /auth/user - * { - * "password": "OldPass123!", - * "newPassword": "NewPass123!" - * } - */ - editUser = this.asyncHandler( - async (req, res) => { - await editUserBodyValidation.validateAsync(req.body); - - const updatedUser = await this.userService.editUser(req.body, req.file, req.user); - - res.success({ - msg: this.stringService.authUpdateUser, - data: updatedUser, - }); - }, - SERVICE_NAME, - "editUser" - ); - - /** - * Checks if a superadmin account exists in the system. - * - * @async - * @function checkSuperadminExists - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @returns {Promise} Success response with boolean indicating superadmin existence - * @example - * GET /auth/users/superadmin - * // Response: { "data": true } or { "data": false } - */ - checkSuperadminExists = this.asyncHandler( - async (req, res) => { - const superAdminExists = await this.userService.checkSuperadminExists(); - return res.success({ - msg: this.stringService.authAdminExists, - data: superAdminExists, - }); - }, - SERVICE_NAME, - "checkSuperadminExists" - ); - - /** - * Initiates password recovery process by sending a recovery email. - * - * @async - * @function requestRecovery - * @param {Object} req - Express request object - * @param {Object} req.body - Request body containing email - * @param {string} req.body.email - Email address for password recovery - * @param {Object} res - Express response object - * @returns {Promise} Success response with message ID - * @throws {Error} 422 - Validation error if email is invalid - * @throws {Error} 404 - Not found if user doesn't exist - * @example - * POST /auth/recovery/request - * { - * "email": "john@example.com" - * } - */ - requestRecovery = this.asyncHandler( - async (req, res) => { - await recoveryValidation.validateAsync(req.body); - const email = req?.body?.email; - const msgId = await this.userService.requestRecovery(email); - return res.success({ - msg: this.stringService.authCreateRecoveryToken, - data: msgId, - }); - }, - SERVICE_NAME, - "requestRecovery" - ); - - /** - * Validates a password recovery token. - * - * @async - * @function validateRecovery - * @param {Object} req - Express request object - * @param {Object} req.body - Request body containing recovery token - * @param {string} req.body.recoveryToken - Recovery token to validate - * @param {Object} res - Express response object - * @returns {Promise} Success response if token is valid - * @throws {Error} 422 - Validation error if token format is invalid - * @throws {Error} 400 - Bad request if token is invalid or expired - * @example - * POST /auth/recovery/validate - * { - * "recoveryToken": "abc123..." - * } - */ - validateRecovery = this.asyncHandler( - async (req, res) => { - await recoveryTokenBodyValidation.validateAsync(req.body); - await this.userService.validateRecovery(req.body.recoveryToken); - return res.success({ - msg: this.stringService.authVerifyRecoveryToken, - }); - }, - SERVICE_NAME, - "validateRecovery" - ); - - /** - * Resets user password using a valid recovery token. - * - * @async - * @function resetPassword - * @param {Object} req - Express request object - * @param {Object} req.body - Request body containing new password and recovery token - * @param {string} req.body.password - New password - * @param {string} req.body.recoveryToken - Valid recovery token - * @param {Object} res - Express response object - * @returns {Promise} Success response with user data and JWT token - * @throws {Error} 422 - Validation error if password format is invalid - * @throws {Error} 400 - Bad request if token is invalid or expired - * @example - * POST /auth/recovery/reset - * { - * "password": "NewSecurePass123!", - * "recoveryToken": "abc123..." - * } - */ - resetPassword = this.asyncHandler( - async (req, res) => { - await newPasswordValidation.validateAsync(req.body); - const { user, token } = await this.userService.resetPassword(req.body.password, req.body.recoveryToken); - return res.success({ - msg: this.stringService.authResetPassword, - data: { user, token }, - }); - }, - SERVICE_NAME, - "resetPassword" - ); - - /** - * Deletes the current user's account and associated data. - * - * @async - * @function deleteUser - * @param {Object} req - Express request object - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user._id - User ID - * @param {string} req.user.email - User email - * @param {string} req.user.teamId - User's team ID - * @param {Array} req.user.role - User roles - * @param {Object} res - Express response object - * @returns {Promise} Success response confirming user deletion - * @throws {Error} 400 - Bad request if user is demo user - * @throws {Error} 404 - Not found if user doesn't exist - * @example - * DELETE /auth/user - * // Requires JWT authentication - */ - deleteUser = this.asyncHandler( - async (req, res) => { - await this.userService.deleteUser(req.user); - return res.success({ - msg: this.stringService.authDeleteUser, - }); - }, - SERVICE_NAME, - "deleteUser" - ); - - /** - * Retrieves all users in the system (admin/superadmin only). - * - * @async - * @function getAllUsers - * @param {Object} req - Express request object - * @param {Object} res - Express response object - * @returns {Promise} Success response with array of users - * @throws {Error} 403 - Forbidden if user doesn't have admin/superadmin role - * @example - * GET /auth/users - * // Requires JWT authentication with admin/superadmin role - */ - getAllUsers = this.asyncHandler( - async (req, res) => { - const allUsers = await this.userService.getAllUsers(); - return res.success({ - msg: this.stringService.authGetAllUsers, - data: allUsers, - }); - }, - SERVICE_NAME, - "getAllUsers" - ); - - /** - * Retrieves a specific user by ID (superadmin only). - * - * @async - * @function getUserById - * @param {Object} req - Express request object - * @param {Object} req.params - URL parameters - * @param {string} req.params.userId - ID of the user to retrieve - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {Array} req.user.role - Current user's roles - * @param {Object} res - Express response object - * @returns {Promise} Success response with user data - * @throws {Error} 422 - Validation error if userId is invalid - * @throws {Error} 403 - Forbidden if user doesn't have superadmin role - * @throws {Error} 404 - Not found if user doesn't exist - * @example - * GET /auth/users/507f1f77bcf86cd799439011 - * // Requires JWT authentication with superadmin role - */ - getUserById = this.asyncHandler( - async (req, res) => { - await getUserByIdParamValidation.validateAsync(req.params); - const userId = req?.params?.userId; - const roles = req?.user?.role; - - if (!userId) { - throw new Error("No user ID in request"); - } - - if (!roles || roles.length === 0) { - throw new Error("No roles in request"); - } - - const user = await this.userService.getUserById(roles, userId); - - return res.success({ msg: "ok", data: user }); - }, - SERVICE_NAME, - "getUserById" - ); - - /** - * Updates a specific user by ID (superadmin only). - * - * @async - * @function editUserById - * @param {Object} req - Express request object - * @param {Object} req.params - URL parameters - * @param {string} req.params.userId - ID of the user to update - * @param {Object} req.body - Request body containing user update data - * @param {string} [req.body.firstName] - Updated first name - * @param {string} [req.body.lastName] - Updated last name - * @param {Array} [req.body.role] - Updated user roles - * @param {Object} req.user - Current authenticated user (from JWT) - * @param {string} req.user._id - Current user's ID - * @param {Array} req.user.role - Current user's roles - * @param {Object} res - Express response object - * @returns {Promise} Success response confirming user update - * @throws {Error} 422 - Validation error if parameters or body are invalid - * @throws {Error} 403 - Forbidden if user doesn't have superadmin role - * @throws {Error} 404 - Not found if user doesn't exist - * @example - * PUT /auth/users/507f1f77bcf86cd799439011 - * { - * "firstName": "Updated Name", - * "role": ["admin"] - * } - * // Requires JWT authentication with superadmin role - */ - editUserById = this.asyncHandler( - async (req, res) => { - const roles = req?.user?.role; - if (!roles.includes("superadmin")) { - throw this.errorService.createError("Unauthorized", 403); - } - - const userId = req.params.userId; - const user = { ...req.body }; - - await editUserByIdParamValidation.validateAsync(req.params); - // If this is superadmin self edit, allow "superadmin" role - if (userId === req.user._id) { - await editSuperadminUserByIdBodyValidation.validateAsync(req.body); - } else { - await editUserByIdBodyValidation.validateAsync(req.body); - } - - await this.userService.editUserById(userId, user); - return res.success({ msg: "ok" }); - }, - SERVICE_NAME, - "editUserById" - ); - editUserPasswordById = this.asyncHandler( - async (req, res) => { - const roles = req?.user?.role; - if (!roles.includes("superadmin")) { - throw this.errorService.createError("Unauthorized", 403); - } - - const userId = req.params.userId; - await editUserByIdParamValidation.validateAsync(req.params); - await editUserPasswordByIdBodyValidation.validateAsync(req.body); - const updatedPassword = req.body.password; - await this.userService.setPasswordByUserId(userId, updatedPassword); - return res.success({ msg: "Password reset successfully" }); - }, - SERVICE_NAME, - "editUserPasswordById" - ); -} - -export default AuthController; diff --git a/server/src/controllers/v1/baseController.js b/server/src/controllers/v1/baseController.js deleted file mode 100644 index e8612cec5..000000000 --- a/server/src/controllers/v1/baseController.js +++ /dev/null @@ -1,83 +0,0 @@ -import { AppError } from "../../service/v1/infrastructure/errorService.js"; - -export const createCommonDependencies = (db, errorService, logger, stringService) => { - return { - db, - errorService, - logger, - stringService, - }; -}; - -class BaseController { - constructor({ db, logger, errorService, ...additionalDependencies }) { - this.db = db; - this.logger = logger; - this.errorService = errorService; - Object.assign(this, additionalDependencies); - - this.asyncHandler = (fn, serviceName, methodName) => { - return async (req, res, next) => { - try { - await fn(req, res, next); - } catch (error) { - // Handle validation errors - if (error.isJoi) { - const validationError = this.errorService.createValidationError(error.message, error.details, serviceName, methodName); - return next(validationError); - } - - if (error.name === "ValidationError") { - const validationError = this.errorService.createValidationError("Database validation failed", error.errors, serviceName, methodName); - return next(validationError); - } - - if (error.name === "CastError") { - const notFoundError = this.errorService.createNotFoundError( - "Invalid resource identifier", - { field: error.path, value: error.value }, - serviceName, - methodName - ); - return next(notFoundError); - } - - if (error.code === "11000") { - const conflictError = this.errorService.createConflictError("Resource already exists", { - originalError: error.message, - code: error.code, - }); - conflictError.service = serviceName; - conflictError.method = methodName; - return next(conflictError); - } - - if (error instanceof AppError) { - error.service = error.service || serviceName; - error.method = error.method || methodName; - return next(error); - } - - if (error.status) { - const appError = this.errorService.createError(error.message, error.status, serviceName, methodName, { - originalError: error.message, - stack: error.stack, - }); - return next(appError); - } - - // For unknown errors, create a server error - const appError = this.errorService.createServerError(error.message || "An unexpected error occurred", { - originalError: error.message, - stack: error.stack, - }); - appError.service = serviceName; - appError.method = methodName; - appError.stack = error.stack; // Preserve original stack - return next(appError); - } - }; - }; - } -} -export default BaseController; diff --git a/server/src/db/v1/MongoDB.js b/server/src/db/MongoDB.js similarity index 100% rename from server/src/db/v1/MongoDB.js rename to server/src/db/MongoDB.js diff --git a/server/src/db/migration/0001_migrateStatusWindowThreshold.ts b/server/src/db/migration/0001_migrateStatusWindowThreshold.ts new file mode 100644 index 000000000..d9dd12aa1 --- /dev/null +++ b/server/src/db/migration/0001_migrateStatusWindowThreshold.ts @@ -0,0 +1,10 @@ +import Monitor from "@/db/models/Monitor.js"; +async function migrateStatusWindowThreshold() { + const monitors = await Monitor.find({ statusWindowThreshold: { $lt: 1 } }); + for (const monitor of monitors) { + monitor.statusWindowThreshold = monitor.statusWindowThreshold * 100; + await monitor.save(); + } +} + +export { migrateStatusWindowThreshold }; diff --git a/server/src/db/migration/0002_convertChecksToTimeSeries.ts b/server/src/db/migration/0002_convertChecksToTimeSeries.ts new file mode 100644 index 000000000..46e6fbc97 --- /dev/null +++ b/server/src/db/migration/0002_convertChecksToTimeSeries.ts @@ -0,0 +1,93 @@ +import mongoose from "mongoose"; + +const CHECKS_COLLECTION = "checks"; +const BACKUP_COLLECTION = "checks_backup"; +const BATCH_SIZE = 1000; + +const getDb = () => { + const db = mongoose.connection.db; + if (!db) { + throw new Error("Database connection is not initialized"); + } + return db; +}; + +const backupAndDropExistingCollection = async () => { + const db = getDb(); + const collections = await db.listCollections({ name: CHECKS_COLLECTION }).toArray(); + if (collections.length === 0) { + return false; + } + + await db.collection(CHECKS_COLLECTION).aggregate([ + { $match: {} }, + { $out: BACKUP_COLLECTION }, + ]).toArray(); + await db.collection(CHECKS_COLLECTION).drop(); + return true; +}; + +const createTimeSeriesCollection = async () => { + const db = getDb(); + const existing = await db.listCollections({ name: CHECKS_COLLECTION }).toArray(); + if (existing.length === 0) { + await db.createCollection(CHECKS_COLLECTION, { + timeseries: { + timeField: "createdAt", + metaField: "metadata", + granularity: "seconds", + }, + }); + } + + await db.collection(CHECKS_COLLECTION).createIndexes([ + { key: { updatedAt: 1 } }, + { key: { "metadata.monitorId": 1, updatedAt: 1 } }, + { key: { "metadata.monitorId": 1, updatedAt: -1 } }, + { key: { "metadata.teamId": 1, updatedAt: -1 } }, + ]); +}; + +const migrateBackupData = async () => { + const db = getDb(); + const backupExists = await db.listCollections({ name: BACKUP_COLLECTION }).toArray(); + if (backupExists.length === 0) { + return; + } + + const source = db.collection(BACKUP_COLLECTION); + const target = db.collection(CHECKS_COLLECTION); + const cursor = source.find(); + const operations: any[] = []; + + while (await cursor.hasNext()) { + const doc = await cursor.next(); + if (!doc) { + continue; + } + const metadata = { + monitorId: doc.monitorId, + teamId: doc.teamId, + type: doc.type, + }; + + operations.push({ insertOne: { document: { ...doc, metadata } } }); + + if (operations.length >= BATCH_SIZE) { + await target.bulkWrite(operations); + operations.length = 0; + } + } + + if (operations.length) { + await target.bulkWrite(operations); + } +}; + +export const convertChecksToTimeSeries = async () => { + const backedUp = await backupAndDropExistingCollection(); + await createTimeSeriesCollection(); + if (backedUp) { + await migrateBackupData(); + } +}; diff --git a/server/src/db/migration/index.ts b/server/src/db/migration/index.ts new file mode 100644 index 000000000..de2a1dee7 --- /dev/null +++ b/server/src/db/migration/index.ts @@ -0,0 +1,51 @@ +import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js"; +import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js"; +import MigrationModel from "../models/Migration.js"; + +type MigrationEntry = { + name: string; + execute: () => Promise; +}; + +const migrations: MigrationEntry[] = [ + { name: "0001_migrateStatusWindowThreshold", execute: migrateStatusWindowThreshold }, + { name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries }, +]; + +const runMigrations = async (logger?: { info: Function; error: Function }) => { + try { + logger?.info({ message: "Running migrations", service: "Migrations" }); + for (const migration of migrations) { + const exists = await MigrationModel.findOne({ name: migration.name, status: "completed" }); + if (exists) { + logger?.info({ message: `Skipping ${migration.name}`, service: "Migrations" }); + continue; + } + + try { + await migration.execute(); + await MigrationModel.findOneAndUpdate( + { name: migration.name }, + { status: "completed", completedAt: new Date(), error: undefined }, + { upsert: true } + ); + logger?.info({ message: `Completed ${migration.name}`, service: "Migrations" }); + } catch (error) { + const err = error as Error; + await MigrationModel.findOneAndUpdate( + { name: migration.name }, + { status: "failed", error: err?.message }, + { upsert: true } + ); + throw error; + } + } + logger?.info({ message: "Migrations completed", service: "Migrations" }); + } catch (error) { + const err = error as Error; + logger?.error({ message: "Migration failed", service: "Migrations", details: err?.message, stack: err?.stack }); + throw error; + } +}; + +export { runMigrations }; diff --git a/server/src/db/v1/models/AppSettings.js b/server/src/db/models/AppSettings.js similarity index 100% rename from server/src/db/v1/models/AppSettings.js rename to server/src/db/models/AppSettings.js diff --git a/server/src/db/models/Check.ts b/server/src/db/models/Check.ts new file mode 100755 index 000000000..b13aa9a16 --- /dev/null +++ b/server/src/db/models/Check.ts @@ -0,0 +1,214 @@ +import mongoose from "mongoose"; +import { MonitorTypes } from "@/types/monitor.js"; + +const cpuSchema = new mongoose.Schema( + { + physical_core: { type: Number, default: 0 }, + logical_core: { type: Number, default: 0 }, + frequency: { type: Number, default: 0 }, + temperature: { type: [Number], default: [] }, + free_percent: { type: Number, default: 0 }, + usage_percent: { type: Number, default: 0 }, + }, + { _id: false } +); + +const memorySchema = new mongoose.Schema( + { + total_bytes: { type: Number, default: 0 }, + available_bytes: { type: Number, default: 0 }, + used_bytes: { type: Number, default: 0 }, + usage_percent: { type: Number, default: 0 }, + }, + { _id: false } +); + +const diskSchema = new mongoose.Schema( + { + device: { type: String, default: "" }, + mountpoint: { type: String, default: "" }, + read_speed_bytes: { type: Number, default: 0 }, + write_speed_bytes: { type: Number, default: 0 }, + total_bytes: { type: Number, default: 0 }, + free_bytes: { type: Number, default: 0 }, + usage_percent: { type: Number, default: 0 }, + }, + { _id: false } +); + +const hostSchema = new mongoose.Schema( + { + os: { type: String, default: "" }, + platform: { type: String, default: "" }, + kernel_version: { type: String, default: "" }, + }, + { _id: false } +); + +const errorSchema = new mongoose.Schema( + { + metric: { type: [String], default: [] }, + err: { type: String, default: "" }, + }, + { _id: false } +); + +const captureSchema = new mongoose.Schema( + { + version: { type: String, default: "" }, + mode: { type: String, default: "" }, + }, + { _id: false } +); + +const networkInterfaceSchema = new mongoose.Schema( + { + name: { type: String }, + bytes_sent: { type: Number, default: 0 }, + bytes_recv: { type: Number, default: 0 }, + packets_sent: { type: Number, default: 0 }, + packets_recv: { type: Number, default: 0 }, + err_in: { type: Number, default: 0 }, + err_out: { type: Number, default: 0 }, + drop_in: { type: Number, default: 0 }, + drop_out: { type: Number, default: 0 }, + fifo_in: { type: Number, default: 0 }, + fifo_out: { type: Number, default: 0 }, + }, + { _id: false } +); + +const metadataSchema = new mongoose.Schema( + { + monitorId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Monitor", + immutable: true, + index: true, + }, + teamId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Team", + immutable: true, + index: true, + }, + type: { + type: String, + enum: MonitorTypes, + required: true, + index: true, + }, + }, + { _id: false } +); + +const CheckSchema = new mongoose.Schema( + { + metadata: { + type: metadataSchema, + required: true, + }, + status: { + type: Boolean, + index: true, + }, + + responseTime: { + type: Number, + }, + + timings: { + type: Object, + default: {}, + }, + + statusCode: { + type: Number, + index: true, + }, + + message: { + type: String, + }, + + expiry: { + type: Date, + default: Date.now, + }, + + ack: { + type: Boolean, + default: false, + }, + + ackAt: { + type: Date, + }, + + // Hardware fields + cpu: { + type: cpuSchema, + default: () => ({}), + }, + memory: { + type: memorySchema, + default: () => ({}), + }, + disk: { + type: [diskSchema], + default: () => [], + }, + host: { + type: hostSchema, + default: () => ({}), + }, + + errors: { + type: [errorSchema], + default: () => [], + }, + + capture: { + type: captureSchema, + default: () => ({}), + }, + + net: { + type: [networkInterfaceSchema], + default: () => [], + }, + + // PageSpeed fields + accessibility: { + type: Number, + }, + bestPractices: { + type: Number, + }, + seo: { + type: Number, + }, + performance: { + type: Number, + }, + audits: { + type: Object, + }, + }, + { + timestamps: true, + strict: false, + timeseries: { + timeField: "createdAt", + metaField: "metadata", + granularity: "seconds", + }, + } +); + +CheckSchema.index({ updatedAt: 1 }); +CheckSchema.index({ "metadata.monitorId": 1, updatedAt: 1 }); +CheckSchema.index({ "metadata.monitorId": 1, updatedAt: -1 }); +CheckSchema.index({ "metadata.teamId": 1, updatedAt: -1 }); + +export default mongoose.model("Check", CheckSchema); diff --git a/server/src/db/v1/models/Incident.js b/server/src/db/models/Incident.js similarity index 100% rename from server/src/db/v1/models/Incident.js rename to server/src/db/models/Incident.js diff --git a/server/src/db/v1/models/InviteToken.js b/server/src/db/models/InviteToken.js similarity index 100% rename from server/src/db/v1/models/InviteToken.js rename to server/src/db/models/InviteToken.js diff --git a/server/src/db/v1/models/MaintenanceWindow.js b/server/src/db/models/MaintenanceWindow.js similarity index 100% rename from server/src/db/v1/models/MaintenanceWindow.js rename to server/src/db/models/MaintenanceWindow.js diff --git a/server/src/db/models/Migration.ts b/server/src/db/models/Migration.ts new file mode 100644 index 000000000..ba928d910 --- /dev/null +++ b/server/src/db/models/Migration.ts @@ -0,0 +1,13 @@ +import mongoose from "mongoose"; + +const MigrationSchema = new mongoose.Schema( + { + name: { type: String, required: true, unique: true }, + status: { type: String, enum: ["completed", "failed"], default: "completed" }, + completedAt: { type: Date }, + error: { type: String }, + }, + { timestamps: true } +); + +export default mongoose.model("Migration", MigrationSchema); diff --git a/server/src/db/v1/models/Monitor.js b/server/src/db/models/Monitor.js similarity index 100% rename from server/src/db/v1/models/Monitor.js rename to server/src/db/models/Monitor.js diff --git a/server/src/db/v1/models/MonitorStats.js b/server/src/db/models/MonitorStats.js similarity index 100% rename from server/src/db/v1/models/MonitorStats.js rename to server/src/db/models/MonitorStats.js diff --git a/server/src/db/v1/models/Notification.js b/server/src/db/models/Notification.js similarity index 100% rename from server/src/db/v1/models/Notification.js rename to server/src/db/models/Notification.js diff --git a/server/src/db/v1/models/RecoveryToken.js b/server/src/db/models/RecoveryToken.js similarity index 100% rename from server/src/db/v1/models/RecoveryToken.js rename to server/src/db/models/RecoveryToken.js diff --git a/server/src/db/v1/models/StatusPage.js b/server/src/db/models/StatusPage.js similarity index 100% rename from server/src/db/v1/models/StatusPage.js rename to server/src/db/models/StatusPage.js diff --git a/server/src/db/v1/models/Team.js b/server/src/db/models/Team.js similarity index 100% rename from server/src/db/v1/models/Team.js rename to server/src/db/models/Team.js diff --git a/server/src/db/v1/models/User.js b/server/src/db/models/User.js similarity index 100% rename from server/src/db/v1/models/User.js rename to server/src/db/models/User.js diff --git a/server/src/db/v1/modules/checkModule.js b/server/src/db/modules/checkModule.js similarity index 100% rename from server/src/db/v1/modules/checkModule.js rename to server/src/db/modules/checkModule.js diff --git a/server/src/db/v1/modules/checkModuleQueries.js b/server/src/db/modules/checkModuleQueries.js similarity index 100% rename from server/src/db/v1/modules/checkModuleQueries.js rename to server/src/db/modules/checkModuleQueries.js diff --git a/server/src/db/v1/modules/incidentModule.js b/server/src/db/modules/incidentModule.js similarity index 100% rename from server/src/db/v1/modules/incidentModule.js rename to server/src/db/modules/incidentModule.js diff --git a/server/src/db/v1/modules/inviteModule.js b/server/src/db/modules/inviteModule.js similarity index 100% rename from server/src/db/v1/modules/inviteModule.js rename to server/src/db/modules/inviteModule.js diff --git a/server/src/db/v1/modules/maintenanceWindowModule.js b/server/src/db/modules/maintenanceWindowModule.js similarity index 100% rename from server/src/db/v1/modules/maintenanceWindowModule.js rename to server/src/db/modules/maintenanceWindowModule.js diff --git a/server/src/db/v1/modules/monitorModule.js b/server/src/db/modules/monitorModule.js similarity index 100% rename from server/src/db/v1/modules/monitorModule.js rename to server/src/db/modules/monitorModule.js diff --git a/server/src/db/v1/modules/monitorModuleQueries.js b/server/src/db/modules/monitorModuleQueries.js similarity index 100% rename from server/src/db/v1/modules/monitorModuleQueries.js rename to server/src/db/modules/monitorModuleQueries.js diff --git a/server/src/db/v1/modules/notificationModule.js b/server/src/db/modules/notificationModule.js similarity index 100% rename from server/src/db/v1/modules/notificationModule.js rename to server/src/db/modules/notificationModule.js diff --git a/server/src/db/v1/modules/recoveryModule.js b/server/src/db/modules/recoveryModule.js similarity index 100% rename from server/src/db/v1/modules/recoveryModule.js rename to server/src/db/modules/recoveryModule.js diff --git a/server/src/db/v1/modules/settingsModule.js b/server/src/db/modules/settingsModule.js similarity index 100% rename from server/src/db/v1/modules/settingsModule.js rename to server/src/db/modules/settingsModule.js diff --git a/server/src/db/v1/modules/statusPageModule.js b/server/src/db/modules/statusPageModule.js similarity index 100% rename from server/src/db/v1/modules/statusPageModule.js rename to server/src/db/modules/statusPageModule.js diff --git a/server/src/db/v1/modules/userModule.js b/server/src/db/modules/userModule.js similarity index 100% rename from server/src/db/v1/modules/userModule.js rename to server/src/db/modules/userModule.js diff --git a/server/src/db/v1/utils/seedDb.js b/server/src/db/utils/seedDb.js similarity index 100% rename from server/src/db/v1/utils/seedDb.js rename to server/src/db/utils/seedDb.js diff --git a/server/src/db/v1/migration/0001_migrateStatusWindowThreshold.ts b/server/src/db/v1/migration/0001_migrateStatusWindowThreshold.ts deleted file mode 100644 index 55d26cc27..000000000 --- a/server/src/db/v1/migration/0001_migrateStatusWindowThreshold.ts +++ /dev/null @@ -1,18 +0,0 @@ -// import Monitor from "@/db/v1/models/Monitor.js"; -async function migrateStatusWindowThreshold() { - try { - // const monitors = await Monitor.find({ statusWindowThreshold: { $lt: 1 } }); - // for (const monitor of monitors) { - // monitor.statusWindowThreshold = monitor.statusWindowThreshold * 100; - // await monitor.save(); - // console.log(`Migrated monitor ${monitor._id}: statusWindowThreshold set to ${monitor.statusWindowThreshold}`); - // } - // console.log("StatusWindowThreshold migration complete."); - return true; - } catch (err) { - console.error("Migration error:", err); - return false; - } -} - -export { migrateStatusWindowThreshold }; diff --git a/server/src/db/v1/migration/index.ts b/server/src/db/v1/migration/index.ts deleted file mode 100644 index 4bf56895a..000000000 --- a/server/src/db/v1/migration/index.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js"; - -const runMigrations = async () => { - await migrateStatusWindowThreshold(); -}; - -export { runMigrations }; diff --git a/server/src/db/v1/models/Check.js b/server/src/db/v1/models/Check.js deleted file mode 100755 index 7a5117979..000000000 --- a/server/src/db/v1/models/Check.js +++ /dev/null @@ -1,178 +0,0 @@ -import mongoose from "mongoose"; - -const cpuSchema = mongoose.Schema({ - physical_core: { type: Number, default: 0 }, - logical_core: { type: Number, default: 0 }, - frequency: { type: Number, default: 0 }, - temperature: { type: [Number], default: [] }, - free_percent: { type: Number, default: 0 }, - usage_percent: { type: Number, default: 0 }, -}); - -const memorySchema = mongoose.Schema({ - total_bytes: { type: Number, default: 0 }, - available_bytes: { type: Number, default: 0 }, - used_bytes: { type: Number, default: 0 }, - usage_percent: { type: Number, default: 0 }, -}); - -const diskSchema = mongoose.Schema({ - device: { type: String, default: "" }, - mountpoint: { type: String, default: "" }, - read_speed_bytes: { type: Number, default: 0 }, - write_speed_bytes: { type: Number, default: 0 }, - total_bytes: { type: Number, default: 0 }, - free_bytes: { type: Number, default: 0 }, - usage_percent: { type: Number, default: 0 }, -}); - -const hostSchema = mongoose.Schema({ - os: { type: String, default: "" }, - platform: { type: String, default: "" }, - kernel_version: { type: String, default: "" }, -}); - -const errorSchema = mongoose.Schema({ - metric: { type: [String], default: [] }, - err: { type: String, default: "" }, -}); - -const captureSchema = mongoose.Schema({ - version: { type: String, default: "" }, - mode: { type: String, default: "" }, -}); - -const networkInterfaceSchema = mongoose.Schema({ - name: { type: String }, - bytes_sent: { type: Number, default: 0 }, - bytes_recv: { type: Number, default: 0 }, - packets_sent: { type: Number, default: 0 }, - packets_recv: { type: Number, default: 0 }, - err_in: { type: Number, default: 0 }, - err_out: { type: Number, default: 0 }, - drop_in: { type: Number, default: 0 }, - drop_out: { type: Number, default: 0 }, - fifo_in: { type: Number, default: 0 }, - fifo_out: { type: Number, default: 0 }, -}); - -const CheckSchema = new mongoose.Schema( - { - // Common fields - monitorId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - immutable: true, - index: true, - }, - - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - index: true, - }, - type: { - type: String, - enum: ["http", "ping", "pagespeed", "hardware", "docker", "port", "game"], - required: true, - index: true, - }, - - status: { - type: Boolean, - index: true, - }, - - responseTime: { - type: Number, - }, - - timings: { - type: Object, - default: {}, - }, - - statusCode: { - type: Number, - index: true, - }, - - message: { - type: String, - }, - - expiry: { - type: Date, - default: Date.now, - expires: 60 * 60 * 24 * 30, // 30 days - }, - - ack: { - type: Boolean, - default: false, - }, - - ackAt: { - type: Date, - }, - - // Hardware fields - cpu: { - type: cpuSchema, - default: () => ({}), - }, - memory: { - type: memorySchema, - default: () => ({}), - }, - disk: { - type: [diskSchema], - default: () => [], - }, - host: { - type: hostSchema, - default: () => ({}), - }, - - errors: { - type: [errorSchema], - default: () => [], - }, - - capture: { - type: captureSchema, - default: () => ({}), - }, - - net: { - type: [networkInterfaceSchema], - default: () => [], - }, - - // PageSpeed fields - accessibility: { - type: Number, - }, - bestPractices: { - type: Number, - }, - seo: { - type: Number, - }, - performance: { - type: Number, - }, - audits: { - type: Object, - }, - }, - { timestamps: true } -); - -CheckSchema.index({ updatedAt: 1 }); -CheckSchema.index({ monitorId: 1, updatedAt: 1 }); -CheckSchema.index({ monitorId: 1, updatedAt: -1 }); -CheckSchema.index({ teamId: 1, updatedAt: -1 }); - -export default mongoose.model("Check", CheckSchema); diff --git a/server/src/index.js b/server/src/index.js index e8b756d0e..28dc5e8fd 100755 --- a/server/src/index.js +++ b/server/src/index.js @@ -5,10 +5,11 @@ import { initShutdownListener } from "./shutdown.js"; import { fileURLToPath } from "url"; import path from "path"; import fs from "fs"; +import { runMigrations } from "./db/migration/index.js"; import Logger from "./utils/logger.js"; -import SettingsService from "./service/v1/system/settingsService.js"; -import AppSettings from "./db/v1/models/AppSettings.js"; +import SettingsService from "./service/system/settingsService.js"; +import AppSettings from "./db/models/AppSettings.js"; const SERVICE_NAME = "Server"; let logger; @@ -30,6 +31,8 @@ const startApp = async () => { // Initialize services const services = await initializeServices({ logger, envSettings, settingsService }); + await runMigrations(logger); + // Initialize controllers const controllers = initializeControllers(services); diff --git a/server/src/middleware/v1/handleErrors.js b/server/src/middleware/v1/handleErrors.js index 43671fbe2..0a30b0420 100755 --- a/server/src/middleware/v1/handleErrors.js +++ b/server/src/middleware/v1/handleErrors.js @@ -1,6 +1,6 @@ import { logger } from "../../utils/logger.js"; -import ServiceRegistry from "../../service/v1/system/serviceRegistry.js"; -import StringService from "../../service/v1/system/stringService.js"; +import ServiceRegistry from "../../service/system/serviceRegistry.js"; +import StringService from "../../service/system/stringService.js"; const handleErrors = (error, req, res, next) => { const status = error.status || 500; diff --git a/server/src/middleware/v1/isAllowed.js b/server/src/middleware/v1/isAllowed.js index cc240f531..1d0de42b4 100755 --- a/server/src/middleware/v1/isAllowed.js +++ b/server/src/middleware/v1/isAllowed.js @@ -1,9 +1,9 @@ import jwt from "jsonwebtoken"; const TOKEN_PREFIX = "Bearer "; const SERVICE_NAME = "allowedRoles"; -import ServiceRegistry from "../../service/v1/system/serviceRegistry.js"; -import StringService from "../../service/v1/system/stringService.js"; -import SettingsService from "../../service/v1/system/settingsService.js"; +import ServiceRegistry from "../../service/system/serviceRegistry.js"; +import StringService from "../../service/system/stringService.js"; +import SettingsService from "../../service/system/settingsService.js"; const isAllowed = (allowedRoles) => { return (req, res, next) => { diff --git a/server/src/middleware/v1/verifyJWT.js b/server/src/middleware/v1/verifyJWT.js index b57391dfd..cf983a6c8 100755 --- a/server/src/middleware/v1/verifyJWT.js +++ b/server/src/middleware/v1/verifyJWT.js @@ -1,7 +1,7 @@ import jwt from "jsonwebtoken"; -import ServiceRegistry from "../../service/v1/system/serviceRegistry.js"; -import SettingsService from "../../service/v1/system/settingsService.js"; -import StringService from "../../service/v1/system/stringService.js"; +import ServiceRegistry from "../../service/system/serviceRegistry.js"; +import SettingsService from "../../service/system/settingsService.js"; +import StringService from "../../service/system/stringService.js"; const SERVICE_NAME = "verifyJWT"; const TOKEN_PREFIX = "Bearer "; diff --git a/server/src/middleware/v1/verifyOwnership.js b/server/src/middleware/v1/verifyOwnership.js index 741fb6436..6fcc84f60 100755 --- a/server/src/middleware/v1/verifyOwnership.js +++ b/server/src/middleware/v1/verifyOwnership.js @@ -1,6 +1,6 @@ import { logger } from "../../utils/logger.js"; -import ServiceRegistry from "../../service/v1/system/serviceRegistry.js"; -import StringService from "../../service/v1/system/stringService.js"; +import ServiceRegistry from "../../service/system/serviceRegistry.js"; +import StringService from "../../service/system/stringService.js"; import { ObjectId } from "mongodb"; const SERVICE_NAME = "verifyOwnership"; diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts new file mode 100644 index 000000000..e69de29bb diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts new file mode 100644 index 000000000..e69de29bb diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts new file mode 100644 index 000000000..458ee5bad --- /dev/null +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -0,0 +1,9 @@ +import type { Monitor } from "@/types/index.js"; +export interface IMonitorsRepository { + // create + // single fetch + // collection fetch + findAll(): Promise; + // update + // delete +} diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts new file mode 100644 index 000000000..b4bae06c8 --- /dev/null +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -0,0 +1,5 @@ +class MongoMonitorsRepository implements IMonitorsRepository { + findAll = async () => { + return null; + }; +} diff --git a/server/src/routes/v1/announcementsRoute.js b/server/src/routes/v1/announcementsRoute.js deleted file mode 100755 index 011dfac4a..000000000 --- a/server/src/routes/v1/announcementsRoute.js +++ /dev/null @@ -1,33 +0,0 @@ -import { Router } from "express"; -import { verifyJWT } from "../middleware/verifyJWT.js"; -import { isAllowed } from "../middleware/isAllowed.js"; - -class AnnouncementRoutes { - constructor(controller) { - this.router = Router(); - this.announcementController = controller; - this.initRoutes(); - } - - initRoutes() { - /** - * @route POST / - * @desc Create a new announcement - * @access Private (Requires JWT verification) - */ - this.router.post("/", verifyJWT, isAllowed(["admin", "superadmin"]), this.announcementController.createAnnouncement); - - /** - * @route GET / - * @desc Get announcements - * @access Public - */ - this.router.get("/", this.announcementController.getAnnouncement); - } - - getRouter() { - return this.router; - } -} - -export default AnnouncementRoutes; diff --git a/server/src/routes/v1/monitorRoute.js b/server/src/routes/v1/monitorRoute.js index fe430d46f..86463b6bb 100755 --- a/server/src/routes/v1/monitorRoute.js +++ b/server/src/routes/v1/monitorRoute.js @@ -1,7 +1,7 @@ import { Router } from "express"; import { isAllowed } from "../../middleware/v1/isAllowed.js"; import multer from "multer"; -import { fetchMonitorCertificate } from "../../controllers/v1/controllerUtils.js"; +import { fetchMonitorCertificate } from "../../controllers/controllerUtils.js"; const upload = multer({ storage: multer.memoryStorage(), // Store file in memory as Buffer }); diff --git a/server/src/service/v1/business/checkService.js b/server/src/service/business/checkService.js similarity index 100% rename from server/src/service/v1/business/checkService.js rename to server/src/service/business/checkService.js diff --git a/server/src/service/v1/business/diagnosticService.js b/server/src/service/business/diagnosticService.js similarity index 100% rename from server/src/service/v1/business/diagnosticService.js rename to server/src/service/business/diagnosticService.js diff --git a/server/src/service/v1/business/incidentService.js b/server/src/service/business/incidentService.js similarity index 100% rename from server/src/service/v1/business/incidentService.js rename to server/src/service/business/incidentService.js diff --git a/server/src/service/v1/business/inviteService.js b/server/src/service/business/inviteService.js similarity index 100% rename from server/src/service/v1/business/inviteService.js rename to server/src/service/business/inviteService.js diff --git a/server/src/service/v1/business/maintenanceWindowService.js b/server/src/service/business/maintenanceWindowService.js similarity index 100% rename from server/src/service/v1/business/maintenanceWindowService.js rename to server/src/service/business/maintenanceWindowService.js diff --git a/server/src/service/v1/business/monitorService.js b/server/src/service/business/monitorService.js similarity index 99% rename from server/src/service/v1/business/monitorService.js rename to server/src/service/business/monitorService.js index 2118e6281..4eae0176f 100644 --- a/server/src/service/v1/business/monitorService.js +++ b/server/src/service/business/monitorService.js @@ -1,4 +1,4 @@ -import { createMonitorsBodyValidation } from "../../../validation/joi.js"; +import { createMonitorsBodyValidation } from "@/validation/joi.js"; const SERVICE_NAME = "MonitorService"; class MonitorService { diff --git a/server/src/service/v1/business/userService.js b/server/src/service/business/userService.js similarity index 100% rename from server/src/service/v1/business/userService.js rename to server/src/service/business/userService.js diff --git a/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js similarity index 100% rename from server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js rename to server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js diff --git a/server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js similarity index 100% rename from server/src/service/v1/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js rename to server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js diff --git a/server/src/service/v1/infrastructure/bufferService.js b/server/src/service/infrastructure/bufferService.js similarity index 100% rename from server/src/service/v1/infrastructure/bufferService.js rename to server/src/service/infrastructure/bufferService.js diff --git a/server/src/service/v1/infrastructure/emailService.js b/server/src/service/infrastructure/emailService.js similarity index 100% rename from server/src/service/v1/infrastructure/emailService.js rename to server/src/service/infrastructure/emailService.js diff --git a/server/src/service/v1/infrastructure/errorService.js b/server/src/service/infrastructure/errorService.js similarity index 100% rename from server/src/service/v1/infrastructure/errorService.js rename to server/src/service/infrastructure/errorService.js diff --git a/server/src/service/v1/infrastructure/networkService.js b/server/src/service/infrastructure/networkService.js similarity index 100% rename from server/src/service/v1/infrastructure/networkService.js rename to server/src/service/infrastructure/networkService.js diff --git a/server/src/service/v1/infrastructure/notificationProviders/matrix.js b/server/src/service/infrastructure/notificationProviders/matrix.js similarity index 100% rename from server/src/service/v1/infrastructure/notificationProviders/matrix.js rename to server/src/service/infrastructure/notificationProviders/matrix.js diff --git a/server/src/service/v1/infrastructure/notificationService.js b/server/src/service/infrastructure/notificationService.js similarity index 100% rename from server/src/service/v1/infrastructure/notificationService.js rename to server/src/service/infrastructure/notificationService.js diff --git a/server/src/service/v1/infrastructure/notificationUtils.js b/server/src/service/infrastructure/notificationUtils.js similarity index 100% rename from server/src/service/v1/infrastructure/notificationUtils.js rename to server/src/service/infrastructure/notificationUtils.js diff --git a/server/src/service/v1/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js similarity index 98% rename from server/src/service/v1/infrastructure/statusService.js rename to server/src/service/infrastructure/statusService.js index 829b6d850..3c1682818 100755 --- a/server/src/service/v1/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -1,5 +1,5 @@ -import MonitorStats from "../../../db/v1/models/MonitorStats.js"; -import Check from "../../../db/v1/models/Check.js"; +import MonitorStats from "../../db/models/MonitorStats.js"; +import Check from "../../db/models/Check.js"; const SERVICE_NAME = "StatusService"; class StatusService { @@ -337,9 +337,11 @@ class StatusService { } = networkResponse; const check = { - monitorId, - teamId, - type, + metadata: { + monitorId, + teamId, + type, + }, status, statusCode: code, responseTime, diff --git a/server/src/service/v1/system/serviceRegistry.js b/server/src/service/system/serviceRegistry.js similarity index 100% rename from server/src/service/v1/system/serviceRegistry.js rename to server/src/service/system/serviceRegistry.js diff --git a/server/src/service/v1/system/settingsService.js b/server/src/service/system/settingsService.js similarity index 100% rename from server/src/service/v1/system/settingsService.js rename to server/src/service/system/settingsService.js diff --git a/server/src/service/v1/system/stringService.js b/server/src/service/system/stringService.js similarity index 100% rename from server/src/service/v1/system/stringService.js rename to server/src/service/system/stringService.js diff --git a/server/src/service/v1/system/translationService.js b/server/src/service/system/translationService.js similarity index 100% rename from server/src/service/v1/system/translationService.js rename to server/src/service/system/translationService.js diff --git a/server/src/types/check.ts b/server/src/types/check.ts new file mode 100644 index 000000000..f2ccc0b77 --- /dev/null +++ b/server/src/types/check.ts @@ -0,0 +1,79 @@ +import type { MonitorType } from "@/types/index.js"; + +export interface CheckTimingPhases { + wait: number; + dns: number; + tcp: number; + tls: number; + request: number; + firstByte: number; + download: number; + total: number; +} + +export interface CheckTimings { + start: number; + socket: number; + lookup: number; + connect: number; + secureConnect: number; + upload: number; + response: number; + end: number; + phases: CheckTimingPhases; +} + +export interface CheckCpuInfo { + physical_core: number; + logical_core: number; + frequency: number; + temperature: number[]; + free_percent: number; + usage_percent: number; +} + +export interface CheckMemoryInfo { + total_bytes: number; + available_bytes: number; + used_bytes: number; + usage_percent: number; +} + +export interface CheckHostInfo { + os: string; + platform: string; + kernel_version: string; +} + +export interface CheckCaptureInfo { + version: string; + mode: string; +} + +export interface CheckDiskInfo {} + +export interface CheckNetInfo {} + +export interface Check { + id: string; + monitorId: string; + teamId: string; + type: MonitorType; + status: boolean; + responseTime: number; + timings: CheckTimings; + statusCode: number; + message: string; + ack: boolean; + expiry: string; + cpu: CheckCpuInfo; + memory: CheckMemoryInfo; + disk: CheckDiskInfo[]; + host: CheckHostInfo; + errors: string[]; + capture: CheckCaptureInfo; + net: CheckNetInfo[]; + __v: number; + createdAt: string; + updatedAt: string; +} diff --git a/server/src/types/index.ts b/server/src/types/index.ts new file mode 100644 index 000000000..92bcb69b0 --- /dev/null +++ b/server/src/types/index.ts @@ -0,0 +1,2 @@ +export * from "@/types/check.js"; +export * from "@/types/monitor.js"; diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts new file mode 100644 index 000000000..6a386c2e4 --- /dev/null +++ b/server/src/types/monitor.ts @@ -0,0 +1,29 @@ +export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game"] as const; +export type MonitorType = (typeof MonitorTypes)[number]; + +export interface Monitor { + id: string; + userId: string; + teamId: string; + name: string; + description: string; + statusWindow: boolean[]; + statusWindowSize: number; + statusWindowThreshold: number; + type: MonitorType; + ignoreTlsErrors: boolean; + url: string; + isActive: boolean; + interval: number; + notifications: string[]; + alertThreshold: number; + selectedDisks: string[]; + group: string | null; + cpuAlertThreshold: number; + memoryAlertThreshold: number; + diskAlertThreshold: number; + tempAlertThreshold: number; + createdAt: string; + updatedAt: string; + status: boolean; +} From 7a6b0d5da421ea29acf6c0a2c93d5b78193eee5d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 12 Jan 2026 22:58:46 +0000 Subject: [PATCH 191/400] refactoring --- server/package-lock.json | 11 + server/package.json | 1 + server/src/config/services.js | 14 +- server/src/db/models/Check.ts | 140 +++++++++--- .../src/db/models/{Monitor.js => Monitor.ts} | 72 ++++--- server/src/db/models/index.ts | 5 + server/src/db/modules/checkModule.js | 43 ++-- server/src/db/modules/monitorModule.js | 23 +- server/src/db/modules/monitorModuleQueries.js | 21 +- .../repositories/checks/IChecksRepository.ts | 10 + .../checks/MongoChecksRepistory.ts | 203 ++++++++++++++++++ server/src/repositories/index.ts | 5 + .../monitors/IMonitorsRepository.ts | 17 +- .../monitors/MongoMonitorsRepository.ts | 124 ++++++++++- .../v1/{monitorRoute.js => monitorRoute.ts} | 4 +- server/src/service/business/monitorService.js | 36 +++- .../service/infrastructure/statusService.js | 4 +- server/src/types/check.ts | 57 ++++- server/src/types/monitor.ts | 25 ++- 19 files changed, 699 insertions(+), 116 deletions(-) mode change 100755 => 100644 server/src/db/models/Check.ts rename server/src/db/models/{Monitor.js => Monitor.ts} (70%) mode change 100755 => 100644 create mode 100644 server/src/db/models/index.ts create mode 100644 server/src/repositories/index.ts rename server/src/routes/v1/{monitorRoute.js => monitorRoute.ts} (96%) diff --git a/server/package-lock.json b/server/package-lock.json index eaf771a61..d254da407 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -48,6 +48,7 @@ "@eslint/js": "^9.17.0", "@types/express": "5.0.3", "@types/jsonwebtoken": "9.0.10", + "@types/multer": "^2.0.0", "@types/nodemailer": "7.0.1", "@types/ping": "0.4.4", "c8": "10.1.3", @@ -3301,6 +3302,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/multer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@types/multer/-/multer-2.0.0.tgz", + "integrity": "sha512-C3Z9v9Evij2yST3RSBktxP9STm6OdMc5uR1xF1SGr98uv8dUlAL2hqwrZ3GVB3uyMyiegnscEK6PGtYvNrjTjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*" + } + }, "node_modules/@types/node": { "version": "24.9.1", "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", diff --git a/server/package.json b/server/package.json index 5610d32a9..3547810ce 100755 --- a/server/package.json +++ b/server/package.json @@ -60,6 +60,7 @@ "@eslint/js": "^9.17.0", "@types/express": "5.0.3", "@types/jsonwebtoken": "9.0.10", + "@types/multer": "^2.0.0", "@types/nodemailer": "7.0.1", "@types/ping": "0.4.4", "c8": "10.1.3", diff --git a/server/src/config/services.js b/server/src/config/services.js index 28b7c9a86..0916752ab 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -47,7 +47,7 @@ import { GenerateAvatarImage } from "../utils/imageProcessing.js"; import { ParseBoolean } from "../utils/utils.js"; // Models -import Check from "../db/models/Check.js"; +import { CheckModel } from "@/db/models/index.js"; import Monitor from "../db/models/Monitor.js"; import User from "../db/models/User.js"; import InviteToken from "../db/models/InviteToken.js"; @@ -71,6 +71,9 @@ import RecoveryModule from "../db/modules/recoveryModule.js"; import SettingsModule from "../db/modules/settingsModule.js"; import IncidentModule from "../db/modules/incidentModule.js"; +// repositories +import { MongoMonitorsRepository, MongoChecksRepository } from "@/repositories/index.js"; + export const initializeServices = async ({ logger, envSettings, settingsService }) => { const serviceRegistry = new ServiceRegistry({ logger }); ServiceRegistry.instance = serviceRegistry; @@ -81,7 +84,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService const stringService = new StringService(translationService); // Create DB - const checkModule = new CheckModule({ logger, Check, Monitor, User }); + const checkModule = new CheckModule({ logger, CheckModel, Monitor, User }); const inviteModule = new InviteModule({ InviteToken, crypto, stringService }); const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService }); const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); @@ -89,7 +92,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService const monitorModule = new MonitorModule({ Monitor, MonitorStats, - Check, stringService, fs, path, @@ -120,6 +122,10 @@ export const initializeServices = async ({ logger, envSettings, settingsService await db.connect(); + // Repositories + const monitorsRepository = new MongoMonitorsRepository(); + const checksRepository = new MongoChecksRepository(); + const networkService = new NetworkService({ axios, got, @@ -220,6 +226,8 @@ export const initializeServices = async ({ logger, envSettings, settingsService logger, errorService, games, + monitorsRepository, + checksRepository, }); const services = { diff --git a/server/src/db/models/Check.ts b/server/src/db/models/Check.ts old mode 100755 new mode 100644 index b13aa9a16..b4a31f8f7 --- a/server/src/db/models/Check.ts +++ b/server/src/db/models/Check.ts @@ -1,7 +1,78 @@ -import mongoose from "mongoose"; -import { MonitorTypes } from "@/types/monitor.js"; +import { Schema, model, Types } from "mongoose"; +import { + MonitorTypes, + type MonitorType, +} from "@/types/monitor.js"; +import type { + Check, + CheckAudits, + CheckCaptureInfo, + CheckCpuInfo, + CheckDiskInfo, + CheckErrorInfo, + CheckHostInfo, + CheckMemoryInfo, + CheckMetadata, + CheckNetworkInterfaceInfo, + CheckTimings, + CheckTimingPhases, +} from "@/types/check.js"; -const cpuSchema = new mongoose.Schema( +type CheckMetadataDocument = Omit & { + monitorId: Types.ObjectId; + teamId: Types.ObjectId; + type: MonitorType; +}; + +type CheckDocumentBase = Omit< + Check, + "id" | "metadata" | "expiry" | "ackAt" | "createdAt" | "updatedAt" +> & { + metadata: CheckMetadataDocument; + expiry: Date; + ackAt?: Date | null; + createdAt: Date; + updatedAt: Date; + __v: number; +}; + +interface CheckDocument extends CheckDocumentBase { + _id: Types.ObjectId; +} + +const timingPhasesSchema = new Schema( + { + wait: { type: Number, default: 0 }, + dns: { type: Number, default: 0 }, + tcp: { type: Number, default: 0 }, + tls: { type: Number, default: 0 }, + request: { type: Number, default: 0 }, + firstByte: { type: Number, default: 0 }, + download: { type: Number, default: 0 }, + total: { type: Number, default: 0 }, + }, + { _id: false } +); + +const timingsSchema = new Schema( + { + start: { type: Number, default: 0 }, + socket: { type: Number, default: 0 }, + lookup: { type: Number, default: 0 }, + connect: { type: Number, default: 0 }, + secureConnect: { type: Number, default: 0 }, + upload: { type: Number, default: 0 }, + response: { type: Number, default: 0 }, + end: { type: Number, default: 0 }, + phases: { + type: timingPhasesSchema, + default: () => ({}), + }, + }, + { _id: false } +); + +const cpuSchema = new Schema( { physical_core: { type: Number, default: 0 }, logical_core: { type: Number, default: 0 }, @@ -13,7 +84,7 @@ const cpuSchema = new mongoose.Schema( { _id: false } ); -const memorySchema = new mongoose.Schema( +const memorySchema = new Schema( { total_bytes: { type: Number, default: 0 }, available_bytes: { type: Number, default: 0 }, @@ -23,7 +94,7 @@ const memorySchema = new mongoose.Schema( { _id: false } ); -const diskSchema = new mongoose.Schema( +const diskSchema = new Schema( { device: { type: String, default: "" }, mountpoint: { type: String, default: "" }, @@ -36,7 +107,7 @@ const diskSchema = new mongoose.Schema( { _id: false } ); -const hostSchema = new mongoose.Schema( +const hostSchema = new Schema( { os: { type: String, default: "" }, platform: { type: String, default: "" }, @@ -45,7 +116,7 @@ const hostSchema = new mongoose.Schema( { _id: false } ); -const errorSchema = new mongoose.Schema( +const errorSchema = new Schema( { metric: { type: [String], default: [] }, err: { type: String, default: "" }, @@ -53,7 +124,7 @@ const errorSchema = new mongoose.Schema( { _id: false } ); -const captureSchema = new mongoose.Schema( +const captureSchema = new Schema( { version: { type: String, default: "" }, mode: { type: String, default: "" }, @@ -61,9 +132,9 @@ const captureSchema = new mongoose.Schema( { _id: false } ); -const networkInterfaceSchema = new mongoose.Schema( +const networkInterfaceSchema = new Schema( { - name: { type: String }, + name: { type: String, default: "" }, bytes_sent: { type: Number, default: 0 }, bytes_recv: { type: Number, default: 0 }, packets_sent: { type: Number, default: 0 }, @@ -78,17 +149,30 @@ const networkInterfaceSchema = new mongoose.Schema( { _id: false } ); -const metadataSchema = new mongoose.Schema( +const auditsSchema = new Schema( + { + cls: { type: Number, default: 0 }, + si: { type: Number, default: 0 }, + fcp: { type: Number, default: 0 }, + lcp: { type: Number, default: 0 }, + tbt: { type: Number, default: 0 }, + }, + { _id: false } +); + +const metadataSchema = new Schema( { monitorId: { - type: mongoose.Schema.Types.ObjectId, + type: Schema.Types.ObjectId, ref: "Monitor", + required: true, immutable: true, index: true, }, teamId: { - type: mongoose.Schema.Types.ObjectId, + type: Schema.Types.ObjectId, ref: "Team", + required: true, immutable: true, index: true, }, @@ -102,7 +186,7 @@ const metadataSchema = new mongoose.Schema( { _id: false } ); -const CheckSchema = new mongoose.Schema( +const CheckSchema = new Schema( { metadata: { type: metadataSchema, @@ -112,40 +196,32 @@ const CheckSchema = new mongoose.Schema( type: Boolean, index: true, }, - responseTime: { type: Number, }, - timings: { - type: Object, - default: {}, + type: timingsSchema, + default: undefined, }, - statusCode: { type: Number, index: true, }, - message: { type: String, }, - expiry: { type: Date, default: Date.now, }, - ack: { type: Boolean, default: false, }, - ackAt: { type: Date, + default: undefined, }, - - // Hardware fields cpu: { type: cpuSchema, default: () => ({}), @@ -162,23 +238,18 @@ const CheckSchema = new mongoose.Schema( type: hostSchema, default: () => ({}), }, - errors: { type: [errorSchema], default: () => [], }, - capture: { type: captureSchema, default: () => ({}), }, - net: { type: [networkInterfaceSchema], default: () => [], }, - - // PageSpeed fields accessibility: { type: Number, }, @@ -192,7 +263,8 @@ const CheckSchema = new mongoose.Schema( type: Number, }, audits: { - type: Object, + type: auditsSchema, + default: undefined, }, }, { @@ -211,4 +283,8 @@ CheckSchema.index({ "metadata.monitorId": 1, updatedAt: 1 }); CheckSchema.index({ "metadata.monitorId": 1, updatedAt: -1 }); CheckSchema.index({ "metadata.teamId": 1, updatedAt: -1 }); -export default mongoose.model("Check", CheckSchema); +const CheckModel = model("Check", CheckSchema); + +export type { CheckDocument, CheckMetadataDocument }; +export { CheckModel }; +export default CheckModel; diff --git a/server/src/db/models/Monitor.js b/server/src/db/models/Monitor.ts old mode 100755 new mode 100644 similarity index 70% rename from server/src/db/models/Monitor.js rename to server/src/db/models/Monitor.ts index 901dc39d1..f6f9d4371 --- a/server/src/db/models/Monitor.js +++ b/server/src/db/models/Monitor.ts @@ -1,18 +1,49 @@ -import mongoose from "mongoose"; +import { Schema, model, Types, type UpdateQuery } from "mongoose"; +import type { Monitor, MonitorMatchMethod, MonitorThresholds } from "@/types/monitor.js"; +import { MonitorTypes } from "@/types/monitor.js"; import Check from "./Check.js"; import MonitorStats from "./MonitorStats.js"; import StatusPage from "./StatusPage.js"; -const MonitorSchema = mongoose.Schema( +type MonitorDocumentBase = Omit< + Monitor, + "id" | "userId" | "teamId" | "notifications" | "selectedDisks" | "statusWindow" | "createdAt" | "updatedAt" +> & { + statusWindow: boolean[]; + notifications: Types.ObjectId[]; + selectedDisks: string[]; + matchMethod?: MonitorMatchMethod; + thresholds?: MonitorThresholds; +}; + +interface MonitorDocument extends MonitorDocumentBase { + _id: Types.ObjectId; + userId: Types.ObjectId; + teamId: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const thresholdsSchema = new Schema( + { + usage_cpu: { type: Number }, + usage_memory: { type: Number }, + usage_disk: { type: Number }, + usage_temperature: { type: Number }, + }, + { _id: false } +); + +const MonitorSchema = new Schema( { userId: { - type: mongoose.Schema.Types.ObjectId, + type: Schema.Types.ObjectId, ref: "User", immutable: true, required: true, }, teamId: { - type: mongoose.Schema.Types.ObjectId, + type: Schema.Types.ObjectId, ref: "Team", immutable: true, required: true, @@ -43,7 +74,7 @@ const MonitorSchema = mongoose.Schema( type: { type: String, required: true, - enum: ["http", "ping", "pagespeed", "hardware", "docker", "port", "game"], + enum: MonitorTypes, }, ignoreTlsErrors: { type: Boolean, @@ -71,7 +102,6 @@ const MonitorSchema = mongoose.Schema( default: true, }, interval: { - // in milliseconds type: Number, default: 60000, }, @@ -81,7 +111,7 @@ const MonitorSchema = mongoose.Schema( }, notifications: [ { - type: mongoose.Schema.Types.ObjectId, + type: Schema.Types.ObjectId, ref: "Notification", }, ], @@ -89,13 +119,7 @@ const MonitorSchema = mongoose.Schema( type: String, }, thresholds: { - type: { - usage_cpu: { type: Number }, - usage_memory: { type: Number }, - usage_disk: { type: Number }, - usage_temperature: { type: Number }, - }, - _id: false, + type: thresholdsSchema, }, alertThreshold: { type: Number, @@ -137,7 +161,7 @@ const MonitorSchema = mongoose.Schema( trim: true, maxLength: 50, default: null, - set: function (value) { + set(value: string | null) { return value && value.trim() ? value.trim() : null; }, }, @@ -148,7 +172,6 @@ const MonitorSchema = mongoose.Schema( ); MonitorSchema.pre("findOneAndDelete", async function (next) { - // Delete checks and stats try { const doc = await this.model.findOne(this.getFilter()); @@ -157,20 +180,17 @@ MonitorSchema.pre("findOneAndDelete", async function (next) { } await Check.deleteMany({ monitorId: doc._id }); - - // Deal with status pages await StatusPage.updateMany({ monitors: doc?._id }, { $pull: { monitors: doc?._id } }); - await MonitorStats.deleteMany({ monitorId: doc?._id.toString() }); next(); } catch (error) { - next(error); + next(error as Error); } }); MonitorSchema.pre("deleteMany", async function (next) { const filter = this.getFilter(); - const monitors = await this.model.find(filter).select(["_id", "type"]).lean(); + const monitors = (await this.model.find(filter).select(["_id", "type"]).lean()) as { _id: Types.ObjectId }[]; for (const monitor of monitors) { await Check.deleteMany({ monitorId: monitor._id }); @@ -197,8 +217,8 @@ MonitorSchema.pre("save", function (next) { }); MonitorSchema.pre("findOneAndUpdate", function (next) { - const update = this.getUpdate(); - if (update.alertThreshold) { + const update = this.getUpdate() as UpdateQuery | null; + if (update && !Array.isArray(update) && update.alertThreshold !== undefined) { update.cpuAlertThreshold = update.alertThreshold; update.memoryAlertThreshold = update.alertThreshold; update.diskAlertThreshold = update.alertThreshold; @@ -209,4 +229,8 @@ MonitorSchema.pre("findOneAndUpdate", function (next) { MonitorSchema.index({ teamId: 1, type: 1 }); -export default mongoose.model("Monitor", MonitorSchema); +const MonitorModel = model("Monitor", MonitorSchema); + +export type { MonitorDocument }; +export { MonitorModel }; +export default MonitorModel; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts new file mode 100644 index 000000000..473940874 --- /dev/null +++ b/server/src/db/models/index.ts @@ -0,0 +1,5 @@ +export * from "@/db/models/Monitor.js"; +export { default as MonitorModel } from "@/db/models/Monitor.js"; + +export * from "@/db/models/Check.js"; +export { default as CheckModel } from "@/db/models/Check.js"; diff --git a/server/src/db/modules/checkModule.js b/server/src/db/modules/checkModule.js index dfca02bed..871761290 100755 --- a/server/src/db/modules/checkModule.js +++ b/server/src/db/modules/checkModule.js @@ -1,4 +1,6 @@ import { ObjectId } from "mongodb"; +import mongoose from "mongoose"; +import { CheckModel } from "@/db/models/index.js"; import { buildChecksSummaryByTeamIdPipeline } from "./checkModuleQueries.js"; const SERVICE_NAME = "checkModule"; @@ -12,18 +14,15 @@ const dateRangeLookup = { }; class CheckModule { - constructor({ logger, Check, HardwareCheck, PageSpeedCheck, Monitor, User }) { + constructor({ logger, Monitor, User }) { this.logger = logger; - this.Check = Check; - this.HardwareCheck = HardwareCheck; - this.PageSpeedCheck = PageSpeedCheck; this.Monitor = Monitor; this.User = User; } createChecks = async (checks) => { try { - await this.Check.insertMany(checks, { ordered: false }); + await CheckModel.insertMany(checks, { ordered: false }); } catch (error) { error.service = SERVICE_NAME; error.method = "createCheck"; @@ -41,7 +40,7 @@ class CheckModule { // Match const matchStage = { - monitorId: new ObjectId(monitorId), + "metadata.monitorId": new ObjectId(monitorId), ...(typeof status !== "undefined" && { status }), ...(typeof ack !== "undefined" && ackStage), ...(dateRangeLookup[dateRange] && { @@ -79,7 +78,7 @@ class CheckModule { skip = page * rowsPerPage; } - const checks = await this.Check.aggregate([ + const checks = await CheckModel.aggregate([ { $match: matchStage }, { $sort: { createdAt: sortOrder } }, { @@ -115,7 +114,7 @@ class CheckModule { const ackStage = ack === "true" ? { ack: true } : { $or: [{ ack: false }, { ack: { $exists: false } }] }; const matchStage = { - teamId: new ObjectId(teamId), + "metadata.teamId": new ObjectId(teamId), status: false, ...(typeof ack !== "undefined" && ackStage), ...(dateRangeLookup[dateRange] && { @@ -170,7 +169,7 @@ class CheckModule { }, ]; - const checks = await this.Check.aggregate(aggregatePipeline); + const checks = await CheckModel.aggregate(aggregatePipeline); return checks[0]; } catch (error) { error.service = SERVICE_NAME; @@ -181,7 +180,11 @@ class CheckModule { ackCheck = async (checkId, teamId, ack) => { try { - const updatedCheck = await this.Check.findOneAndUpdate({ _id: checkId, teamId: teamId }, { $set: { ack, ackAt: new Date() } }, { new: true }); + const updatedCheck = await CheckModel.findOneAndUpdate( + { _id: checkId, "metadata.teamId": teamId }, + { $set: { ack, ackAt: new Date() } }, + { new: true } + ); if (!updatedCheck) { throw new Error("Check not found"); @@ -197,7 +200,11 @@ class CheckModule { ackAllChecks = async (monitorId, teamId, ack, path) => { try { - const updatedChecks = await this.Check.updateMany(path === "monitor" ? { monitorId } : { teamId }, { $set: { ack, ackAt: new Date() } }); + const filter = + path === "monitor" + ? { "metadata.monitorId": new mongoose.Types.ObjectId(monitorId) } + : { "metadata.teamId": new mongoose.Types.ObjectId(teamId) }; + const updatedChecks = await CheckModel.updateMany(filter, { $set: { ack, ackAt: new Date() } }); return updatedChecks.modifiedCount; } catch (error) { error.service = SERVICE_NAME; @@ -209,9 +216,9 @@ class CheckModule { getChecksSummaryByTeamId = async ({ teamId }) => { try { const matchStage = { - teamId: new ObjectId(teamId), + "metadata.teamId": new ObjectId(teamId), }; - const checks = await this.Check.aggregate(buildChecksSummaryByTeamIdPipeline({ matchStage })); + const checks = await CheckModel.aggregate(buildChecksSummaryByTeamIdPipeline({ matchStage })); return checks[0].summary; } catch (error) { error.service = SERVICE_NAME; @@ -221,7 +228,7 @@ class CheckModule { }; deleteChecks = async (monitorId) => { try { - const result = await this.Check.deleteMany({ monitorId }); + const result = await CheckModel.deleteMany({ "metadata.monitorId": monitorId }); return result.deletedCount; } catch (error) { error.service = SERVICE_NAME; @@ -237,7 +244,7 @@ class CheckModule { const monitorIds = teamMonitors.map((monitor) => monitor._id); // Delete all checks for these monitors in one operation - const deleteResult = await this.Check.deleteMany({ monitorId: { $in: monitorIds } }); + const deleteResult = await CheckModel.deleteMany({ "metadata.monitorId": { $in: monitorIds } }); return deleteResult.deletedCount; } catch (error) { @@ -249,7 +256,7 @@ class CheckModule { updateChecksTTL = async (teamId, ttl) => { try { - await this.Check.collection.dropIndex("expiry_1"); + await CheckModel.collection.dropIndex("expiry_1"); } catch (error) { this.logger.error({ message: error.message, @@ -260,9 +267,9 @@ class CheckModule { } try { - await this.Check.collection.createIndex( + await CheckModel.collection.createIndex( { expiry: 1 }, - { expireAfterSeconds: ttl } // TTL in seconds, adjust as necessary + { expireAfterSeconds: ttl, partialFilterExpression: { "metadata.mode": { $exists: true } } } ); } catch (error) { error.service = SERVICE_NAME; diff --git a/server/src/db/modules/monitorModule.js b/server/src/db/modules/monitorModule.js index 39168f0b2..316caa2ea 100755 --- a/server/src/db/modules/monitorModule.js +++ b/server/src/db/modules/monitorModule.js @@ -8,13 +8,14 @@ import { buildFilteredMonitorsByTeamIdPipeline, } from "./monitorModuleQueries.js"; +import { CheckModel } from "@/db/models/index.js"; + const SERVICE_NAME = "monitorModule"; class MonitorModule { - constructor({ Monitor, MonitorStats, Check, stringService, fs, path, fileURLToPath, ObjectId, NormalizeData, NormalizeDataUptimeDetails }) { + constructor({ Monitor, MonitorStats, stringService, fs, path, fileURLToPath, ObjectId, NormalizeData, NormalizeDataUptimeDetails }) { this.Monitor = Monitor; this.MonitorStats = MonitorStats; - this.Check = Check; this.stringService = stringService; this.fs = fs; this.path = path; @@ -123,15 +124,18 @@ class MonitorModule { //Helper getMonitorChecks = async (monitorId, dateRange, sortOrder) => { + const objectId = new this.ObjectId(monitorId); const indexSpec = { - monitorId: 1, - updatedAt: sortOrder, // This will be 1 or -1 + "metadata.monitorId": 1, + updatedAt: sortOrder, }; + const matchBase = { "metadata.monitorId": objectId }; + const [checksAll, checksForDateRange] = await Promise.all([ - this.Check.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(), - this.Check.find({ - monitorId, + CheckModel.find(matchBase).sort({ createdAt: sortOrder }).hint(indexSpec).lean(), + CheckModel.find({ + ...matchBase, createdAt: { $gte: dateRange.start, $lte: dateRange.end }, }) .hint(indexSpec) @@ -238,7 +242,8 @@ class MonitorModule { const dateString = formatLookup[dateRange]; - const results = await this.Check.aggregate(buildUptimeDetailsPipeline(monitorId, dates, dateString)); + console.log("WTTTTFFF"); + const results = await CheckModel.aggregate(buildUptimeDetailsPipeline(monitorId, dates, dateString)); const monitorData = results[0]; @@ -313,7 +318,7 @@ class MonitorModule { }; const dateString = formatLookup[dateRange]; - const hardwareStats = await this.Check.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString)); + const hardwareStats = await CheckModel.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString)); const stats = hardwareStats[0]; diff --git a/server/src/db/modules/monitorModuleQueries.js b/server/src/db/modules/monitorModuleQueries.js index 32aed1529..e6c6b4ec6 100755 --- a/server/src/db/modules/monitorModuleQueries.js +++ b/server/src/db/modules/monitorModuleQueries.js @@ -4,7 +4,7 @@ const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => { return [ { $match: { - monitorId: new ObjectId(monitorId), + "metadata.monitorId": new ObjectId(monitorId), updatedAt: { $gte: dates.start, $lte: dates.end }, }, }, @@ -172,8 +172,8 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { return [ { $match: { - monitorId: monitor._id, - type: "hardware", + "metadata.monitorId": monitor._id, + "metadata.type": "hardware", createdAt: { $gte: dates.start, $lte: dates.end }, }, }, @@ -233,11 +233,16 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, pipeline: [ { - $match: { - $expr: { - $and: [{ $eq: ["$monitorId", monitor._id] }, { $gte: ["$createdAt", dates.start] }, { $lte: ["$createdAt", dates.end] }], - }, - }, + $match: { + $expr: { + $and: [ + { $eq: ["$metadata.monitorId", monitor._id] }, + { $eq: ["$metadata.type", "hardware"] }, + { $gte: ["$createdAt", dates.start] }, + { $lte: ["$createdAt", dates.end] }, + ], + }, + }, }, { $group: { diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index e69de29bb..df69cf18a 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -0,0 +1,10 @@ +import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js"; + +export interface IChecksRepository { + // create + // single fetch + // collection fetch + findLatestChecksByMonitorIds(monitorIds: string[]): Promise; + // update + // delete +} diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index e69de29bb..d04118da0 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -0,0 +1,203 @@ +import { IChecksRepository } from "@/repositories/index.js"; +import type { + Check, + CheckAudits, + CheckCaptureInfo, + CheckCpuInfo, + CheckDiskInfo, + CheckErrorInfo, + CheckHostInfo, + CheckMemoryInfo, + CheckMetadata, + CheckNetworkInterfaceInfo, + CheckTimings, +} from "@/types/index.js"; +import { CheckModel, type CheckDocument } from "@/db/models/index.js"; +import mongoose from "mongoose"; + +export type LatestChecksMap = Record; + +class MongoChecksRepistory implements IChecksRepository { + private toEntity = (doc: CheckDocument): Check => { + const toStringId = (value: mongoose.Types.ObjectId | string | undefined | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + const toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + const toOptionalDateString = (value?: Date | string | null): string | undefined => { + if (!value) { + return undefined; + } + return toDateString(value); + }; + + const mapTimings = (timings?: CheckTimings): CheckTimings => { + const phases = timings?.phases ?? { + wait: 0, + dns: 0, + tcp: 0, + tls: 0, + request: 0, + firstByte: 0, + download: 0, + total: 0, + }; + + return { + start: timings?.start ?? 0, + socket: timings?.socket ?? 0, + lookup: timings?.lookup ?? 0, + connect: timings?.connect ?? 0, + secureConnect: timings?.secureConnect ?? 0, + upload: timings?.upload ?? 0, + response: timings?.response ?? 0, + end: timings?.end ?? 0, + phases, + }; + }; + + const mapCpu = (cpu?: CheckCpuInfo): CheckCpuInfo => ({ + physical_core: cpu?.physical_core ?? 0, + logical_core: cpu?.logical_core ?? 0, + frequency: cpu?.frequency ?? 0, + temperature: cpu?.temperature ?? [], + free_percent: cpu?.free_percent ?? 0, + usage_percent: cpu?.usage_percent ?? 0, + }); + + const mapMemory = (memory?: CheckMemoryInfo): CheckMemoryInfo => ({ + total_bytes: memory?.total_bytes ?? 0, + available_bytes: memory?.available_bytes ?? 0, + used_bytes: memory?.used_bytes ?? 0, + usage_percent: memory?.usage_percent ?? 0, + }); + + const mapHost = (host?: CheckHostInfo): CheckHostInfo => ({ + os: host?.os ?? "", + platform: host?.platform ?? "", + kernel_version: host?.kernel_version ?? "", + }); + + const mapCapture = (capture?: CheckCaptureInfo): CheckCaptureInfo => ({ + version: capture?.version ?? "", + mode: capture?.mode ?? "", + }); + + const mapDisks = (disks?: CheckDiskInfo[]): CheckDiskInfo[] => + (disks ?? []).map((disk) => ({ + device: disk?.device ?? "", + mountpoint: disk?.mountpoint ?? "", + read_speed_bytes: disk?.read_speed_bytes ?? 0, + write_speed_bytes: disk?.write_speed_bytes ?? 0, + total_bytes: disk?.total_bytes ?? 0, + free_bytes: disk?.free_bytes ?? 0, + usage_percent: disk?.usage_percent ?? 0, + })); + + const mapErrors = (errors?: CheckErrorInfo[]): CheckErrorInfo[] => + (errors ?? []).map((error) => ({ + metric: error?.metric ?? [], + err: error?.err ?? "", + })); + + const mapNet = (net?: CheckNetworkInterfaceInfo[]): CheckNetworkInterfaceInfo[] => + (net ?? []).map((iface) => ({ + name: iface?.name ?? "", + bytes_sent: iface?.bytes_sent ?? 0, + bytes_recv: iface?.bytes_recv ?? 0, + packets_sent: iface?.packets_sent ?? 0, + packets_recv: iface?.packets_recv ?? 0, + err_in: iface?.err_in ?? 0, + err_out: iface?.err_out ?? 0, + drop_in: iface?.drop_in ?? 0, + drop_out: iface?.drop_out ?? 0, + fifo_in: iface?.fifo_in ?? 0, + fifo_out: iface?.fifo_out ?? 0, + })); + + const mapAudits = (audits?: CheckAudits): CheckAudits | undefined => { + if (!audits) { + return undefined; + } + return { + cls: audits.cls ?? 0, + si: audits.si ?? 0, + fcp: audits.fcp ?? 0, + lcp: audits.lcp ?? 0, + tbt: audits.tbt ?? 0, + }; + }; + + const mapMetadata = (metadata: CheckDocument["metadata"]): CheckMetadata => ({ + monitorId: toStringId(metadata.monitorId), + teamId: toStringId(metadata.teamId), + type: metadata.type, + }); + + return { + id: toStringId(doc._id), + metadata: mapMetadata(doc.metadata), + status: doc.status ?? false, + responseTime: doc.responseTime ?? 0, + timings: mapTimings(doc.timings), + statusCode: doc.statusCode ?? 0, + message: doc.message ?? "", + ack: doc.ack ?? false, + ackAt: toOptionalDateString(doc.ackAt), + expiry: toDateString(doc.expiry), + cpu: mapCpu(doc.cpu), + memory: mapMemory(doc.memory), + disk: mapDisks(doc.disk), + host: mapHost(doc.host), + errors: mapErrors(doc.errors), + capture: mapCapture(doc.capture), + net: mapNet(doc.net), + accessibility: doc.accessibility, + bestPractices: doc.bestPractices, + seo: doc.seo, + performance: doc.performance, + audits: mapAudits(doc.audits), + __v: doc.__v ?? 0, + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), + }; + }; + + findLatestChecksByMonitorIds = async (monitorIds: string[]): Promise => { + const mongoIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); + const checkMap = await CheckModel.aggregate([ + { + $match: { + "metadata.monitorId": { $in: mongoIds }, + }, + }, + { $sort: { createdAt: -1 } }, + { + $group: { + _id: "$metadata.monitorId", + latestChecks: { $push: "$$ROOT" }, + }, + }, + { + $project: { + latestChecks: { $slice: [{ $ifNull: ["$latestChecks", []] }, 25] }, + }, + }, + ]); + return checkMap.reduce((acc, cm) => { + acc[cm._id.toString()] = cm.latestChecks.map((c: CheckDocument) => this.toEntity(c)); + return acc; + }, {}); + }; +} + +export default MongoChecksRepistory; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts new file mode 100644 index 000000000..c2bae6939 --- /dev/null +++ b/server/src/repositories/index.ts @@ -0,0 +1,5 @@ +export * from "@/repositories/monitors/IMonitorsRepository.js"; +export { default as MongoMonitorsRepository } from "@/repositories/monitors/MongoMonitorsRepository.js"; + +export * from "@/repositories/checks/IChecksRepository.js"; +export { default as MongoChecksRepository } from "@/repositories/checks/MongoChecksRepistory.js"; diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 458ee5bad..865d7f8e4 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -1,9 +1,24 @@ -import type { Monitor } from "@/types/index.js"; +import { type MonitorType, type Monitor } from "@/types/index.js"; + +export interface TeamQueryConfig { + limit?: number; + type?: MonitorType | MonitorType[]; + page?: number; + rowsPerPage?: number; + filter?: string; + field?: string; + order?: "asc" | "desc"; +} + export interface IMonitorsRepository { // create // single fetch // collection fetch findAll(): Promise; + findByTeamId(teamId: string, config: TeamQueryConfig): Promise; // update // delete + + // counts + findMonitorCountByTeamIdAndType(teamId: string, config: TeamQueryConfig): Promise; } diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index b4bae06c8..dd85f9a3d 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -1,5 +1,125 @@ +import { MonitorModel } from "@/db/models/index.js"; +import type { MonitorDocument } from "@/db/models/Monitor.js"; +import type { Monitor, MonitorType } from "@/types/monitor.js"; +import mongoose, { type FilterQuery } from "mongoose"; +import type { IMonitorsRepository, TeamQueryConfig } from "./IMonitorsRepository.js"; + class MongoMonitorsRepository implements IMonitorsRepository { - findAll = async () => { - return null; + findAll = async (): Promise => { + const documents = await MonitorModel.find().exec(); + return this.mapDocuments(documents); + }; + + findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise => { + const { page = 0, rowsPerPage = 25, filter, field = "createdAt", order = "desc", type, limit } = config ?? {}; + + const query: Record = { + teamId: new mongoose.Types.ObjectId(teamId), + }; + + if (type !== undefined) { + query.type = Array.isArray(type) ? { $in: type } : type; + } + + if (filter !== undefined) { + switch (field) { + case "name": + query.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }]; + break; + case "isActive": + query.isActive = filter === "true"; + break; + case "status": + query.status = filter === "true"; + break; + case "type": + query.type = filter; + break; + default: + break; + } + } + + const sort = { [field]: order === "asc" ? 1 : -1 } as const; + const skip = Math.max(page, 0) * rowsPerPage; + const limitValue = limit ?? rowsPerPage; + + const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(limitValue).exec(); + + return this.mapDocuments(documents); + }; + + findMonitorCountByTeamIdAndType = async (teamId: string, config?: TeamQueryConfig): Promise => { + const { type } = config ?? {}; + + const query: FilterQuery = { + teamId: new mongoose.Types.ObjectId(teamId), + }; + + if (type !== undefined) { + query.type = Array.isArray(type) ? { $in: type } : type; + } + + const count = await MonitorModel.countDocuments(query); + return count; + }; + + private mapDocuments = (documents: MonitorDocument[]): Monitor[] | null => { + if (!documents?.length) { + return null; + } + return documents.map((doc) => this.toEntity(doc)); + }; + + private toEntity = (doc: MonitorDocument): Monitor => { + const toStringId = (value: unknown): string => { + if (value instanceof mongoose.Types.ObjectId) { + return value.toString(); + } + return value?.toString() ?? ""; + }; + + const toDateString = (value: Date | string): string => { + return value instanceof Date ? value.toISOString() : value; + }; + + const notificationIds = (doc.notifications ?? []).map((notification) => toStringId(notification)); + + return { + id: toStringId(doc._id), + userId: toStringId(doc.userId), + teamId: toStringId(doc.teamId), + name: doc.name, + description: doc.description ?? undefined, + status: doc.status ?? undefined, + statusWindow: doc.statusWindow ?? [], + statusWindowSize: doc.statusWindowSize, + statusWindowThreshold: doc.statusWindowThreshold, + type: doc.type, + ignoreTlsErrors: doc.ignoreTlsErrors, + jsonPath: doc.jsonPath ?? undefined, + expectedValue: doc.expectedValue ?? undefined, + matchMethod: doc.matchMethod ?? undefined, + url: doc.url, + port: doc.port ?? undefined, + isActive: doc.isActive, + interval: doc.interval, + uptimePercentage: doc.uptimePercentage ?? undefined, + notifications: notificationIds, + secret: doc.secret ?? undefined, + thresholds: doc.thresholds ?? undefined, + alertThreshold: doc.alertThreshold, + cpuAlertThreshold: doc.cpuAlertThreshold, + memoryAlertThreshold: doc.memoryAlertThreshold, + diskAlertThreshold: doc.diskAlertThreshold, + tempAlertThreshold: doc.tempAlertThreshold, + selectedDisks: doc.selectedDisks ?? [], + gameId: doc.gameId ?? undefined, + group: doc.group ?? null, + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), + }; }; } + +export default MongoMonitorsRepository; diff --git a/server/src/routes/v1/monitorRoute.js b/server/src/routes/v1/monitorRoute.ts similarity index 96% rename from server/src/routes/v1/monitorRoute.js rename to server/src/routes/v1/monitorRoute.ts index 86463b6bb..e68e0019f 100755 --- a/server/src/routes/v1/monitorRoute.js +++ b/server/src/routes/v1/monitorRoute.ts @@ -7,7 +7,9 @@ const upload = multer({ }); class MonitorRoutes { - constructor(monitorController) { + private router: Router; + private monitorController: any; + constructor(monitorController: any) { this.router = Router(); this.monitorController = monitorController; this.initRoutes(); diff --git a/server/src/service/business/monitorService.js b/server/src/service/business/monitorService.js index 4eae0176f..f457e5c7a 100644 --- a/server/src/service/business/monitorService.js +++ b/server/src/service/business/monitorService.js @@ -1,10 +1,23 @@ import { createMonitorsBodyValidation } from "@/validation/joi.js"; +import { NormalizeData } from "@/utils/dataUtils.js"; const SERVICE_NAME = "MonitorService"; class MonitorService { static SERVICE_NAME = SERVICE_NAME; - constructor({ db, settingsService, jobQueue, stringService, emailService, papaparse, logger, errorService, games }) { + constructor({ + db, + settingsService, + jobQueue, + stringService, + emailService, + papaparse, + logger, + errorService, + games, + monitorsRepository, + checksRepository, + }) { this.db = db; this.settingsService = settingsService; this.jobQueue = jobQueue; @@ -14,6 +27,8 @@ class MonitorService { this.logger = logger; this.errorService = errorService; this.games = games; + this.monitorsRepository = monitorsRepository; + this.checksRepository = checksRepository; } get serviceName() { @@ -205,6 +220,7 @@ class MonitorService { }; getMonitorsByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order }) => { + console.log("que"); const monitors = await this.db.monitorModule.getMonitorsByTeamId({ limit, type, @@ -228,7 +244,8 @@ class MonitorService { }; getMonitorsWithChecksByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order, explain }) => { - const result = await this.db.monitorModule.getMonitorsWithChecksByTeamId({ + const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter }); + const monitors = await this.monitorsRepository.findByTeamId(teamId, { limit, type, page, @@ -236,10 +253,19 @@ class MonitorService { filter, field, order, - teamId, - explain, }); - return result; + + const monitorIds = monitors?.map((m) => m.id) ?? []; + const checksMap = await this.checksRepository.findLatestChecksByMonitorIds(monitorIds); + const monitorsWithChecks = (monitors ?? []).map((monitor) => { + const checks = NormalizeData(checksMap[monitor.id] ?? [], 10, 100); + return { + ...monitor, + checks, + }; + }); + + return { count, monitors: monitorsWithChecks }; }; exportMonitorsToCSV = async ({ teamId }) => { diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index 3c1682818..843c356ed 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -1,5 +1,5 @@ import MonitorStats from "../../db/models/MonitorStats.js"; -import Check from "../../db/models/Check.js"; +import { CheckModel } from "@/db/models/index.js"; const SERVICE_NAME = "StatusService"; class StatusService { @@ -115,7 +115,7 @@ class StatusService { if (!check._id) { try { - const checkModel = new Check(check); + const checkModel = new CheckModel(check); savedCheck = await checkModel.save(); this.buffer.removeCheckFromBuffer(check); diff --git a/server/src/types/check.ts b/server/src/types/check.ts index f2ccc0b77..2120d5de1 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -1,5 +1,11 @@ import type { MonitorType } from "@/types/index.js"; +export interface CheckMetadata { + monitorId: string; + teamId: string; + type: MonitorType; +} + export interface CheckTimingPhases { wait: number; dns: number; @@ -50,29 +56,66 @@ export interface CheckCaptureInfo { mode: string; } -export interface CheckDiskInfo {} +export interface CheckDiskInfo { + device: string; + mountpoint: string; + read_speed_bytes: number; + write_speed_bytes: number; + total_bytes: number; + free_bytes: number; + usage_percent: number; +} -export interface CheckNetInfo {} +export interface CheckErrorInfo { + metric: string[]; + err: string; +} + +export interface CheckNetworkInterfaceInfo { + name: string; + bytes_sent: number; + bytes_recv: number; + packets_sent: number; + packets_recv: number; + err_in: number; + err_out: number; + drop_in: number; + drop_out: number; + fifo_in: number; + fifo_out: number; +} + +export interface CheckAudits { + cls: number; + si: number; + fcp: number; + lcp: number; + tbt: number; +} export interface Check { id: string; - monitorId: string; - teamId: string; - type: MonitorType; + metadata: CheckMetadata; status: boolean; responseTime: number; timings: CheckTimings; statusCode: number; message: string; ack: boolean; + ackAt?: string | null; expiry: string; cpu: CheckCpuInfo; memory: CheckMemoryInfo; disk: CheckDiskInfo[]; host: CheckHostInfo; - errors: string[]; + errors: CheckErrorInfo[]; capture: CheckCaptureInfo; - net: CheckNetInfo[]; + net: CheckNetworkInterfaceInfo[]; + accessibility?: number; + bestPractices?: number; + seo?: number; + performance?: number; + audits?: CheckAudits; __v: number; createdAt: string; updatedAt: string; diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index 6a386c2e4..4c8dc7528 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -1,29 +1,46 @@ export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game"] as const; export type MonitorType = (typeof MonitorTypes)[number]; +export interface MonitorThresholds { + usage_cpu?: number; + usage_memory?: number; + usage_disk?: number; + usage_temperature?: number; +} + +export type MonitorMatchMethod = "equal" | "include" | "regex" | ""; + export interface Monitor { id: string; userId: string; teamId: string; name: string; - description: string; + description?: string; + status?: boolean; statusWindow: boolean[]; statusWindowSize: number; statusWindowThreshold: number; type: MonitorType; ignoreTlsErrors: boolean; + jsonPath?: string; + expectedValue?: string; + matchMethod?: MonitorMatchMethod; url: string; + port?: number; isActive: boolean; interval: number; + uptimePercentage?: number; notifications: string[]; + secret?: string; + thresholds?: MonitorThresholds; alertThreshold: number; - selectedDisks: string[]; - group: string | null; cpuAlertThreshold: number; memoryAlertThreshold: number; diskAlertThreshold: number; tempAlertThreshold: number; + selectedDisks: string[]; + gameId?: string; + group: string | null; createdAt: string; updatedAt: string; - status: boolean; } From 25d1039376bdadb05854af79f70fbb16cca87362 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 12 Jan 2026 23:57:39 +0000 Subject: [PATCH 192/400] update pagespeed --- client/package-lock.json | 605 ++++++++++++++++++ .../src/Components/v1/ActionsMenu/index.jsx | 12 +- .../Components/v1/Charts/BarChart/index.jsx | 4 +- .../Components/v1/MonitorActions/index.jsx | 2 +- .../v1/MonitorDetailsControlHeader/index.jsx | 6 +- .../v1/MonitorDetailsControlHeader/status.jsx | 2 +- .../Components/v1/PageStateWrapper/index.jsx | 2 +- client/src/Components/v1/StatBox/index.jsx | 2 +- client/src/Hooks/{v1 => }/checkHooks.js | 4 +- client/src/Hooks/{v1 => }/inviteHooks.js | 2 +- client/src/Hooks/{v1 => }/logHooks.js | 4 +- client/src/Hooks/{v1 => }/monitorHooks.js | 10 +- client/src/Hooks/{v1 => }/settingsHooks.js | 14 +- client/src/Hooks/{v1 => }/useIsAdmin.js | 0 client/src/Hooks/{v1 => }/useMonitorUtils.js | 0 client/src/Hooks/{v1 => }/useNotifications.js | 6 +- client/src/Hooks/{v1 => }/useSendTestEmail.js | 4 +- client/src/Hooks/{v1 => }/userHooks.js | 4 +- client/src/Pages/{v1 => }/About/index.jsx | 0 .../Account/EditUser/hooks/editUser.js | 6 +- .../Pages/{v1 => }/Account/EditUser/index.jsx | 4 +- .../components/AddMemberMenu/index.jsx | 0 .../AddTeamMember/hooks/useAddTeamMember.jsx | 2 +- .../components/AddTeamMember/index.jsx | 4 +- .../components/ChangePasswordModal/index.jsx | 4 +- .../Account/components/PasswordPanel.jsx | 8 +- .../Account/components/ProfilePanel.jsx | 12 +- .../Account/components/RoleTable/index.jsx | 2 +- .../{v1 => }/Account/components/TeamPanel.jsx | 10 +- client/src/Pages/{v1 => }/Account/index.jsx | 0 client/src/Pages/{v1 => }/Auth/CheckEmail.jsx | 10 +- .../Pages/{v1 => }/Auth/ForgotPassword.jsx | 12 +- .../Auth/Login/hooks/useLoadingSubmit.jsx | 0 .../Auth/Login/hooks/useLoginForm.jsx | 0 .../Auth/Login/hooks/useLoginSubmit.jsx | 4 +- .../Auth/Login/hooks/useValidateLoginForm.jsx | 2 +- .../src/Pages/{v1 => }/Auth/Login/index.jsx | 0 .../{v1 => }/Auth/NewPasswordConfirmed.jsx | 8 +- .../Pages/{v1 => }/Auth/Register/index.jsx | 8 +- .../Pages/{v1 => }/Auth/SetNewPassword.jsx | 12 +- .../{v1 => }/Auth/components/AuthHeader.jsx | 4 +- .../Auth/components/AuthPageWrapper.jsx | 4 +- .../Auth/components/PasswordTooltip.jsx | 0 .../Auth/hooks/usePasswordFeedback.jsx | 2 +- .../Auth/hooks/useValidatePassword.jsx | 2 +- client/src/Pages/{v1 => }/Auth/index.css | 0 .../Components/IncidentTable/index.jsx | 6 +- .../Components/OptionsHeader/index.jsx | 0 .../Components/OptionsHeader/skeleton.jsx | 0 .../Components/StatusBoxes/StatusBox.jsx | 10 +- .../Components/StatusBoxes/index.jsx | 0 .../Components/StatusBoxes/skeleton.jsx | 0 client/src/Pages/{v1 => }/Incidents/index.jsx | 5 +- .../Components/ActiveIncidentsPanel/index.jsx | 0 .../Components/IncidentTable/index.jsx | 2 +- .../IncidentsSummaryPanel/index.jsx | 0 .../IncidentsSummaryPanel/skeleton.jsx | 0 .../LatestIncidentsPanel/IncidentItem.jsx | 0 .../Components/LatestIncidentsPanel/index.jsx | 0 .../Components/OptionsHeader/index.jsx | 0 .../Components/StatisticsPanel/index.jsx | 0 .../Components/SummaryCard/index.jsx | 0 .../Incidents2/hooks/useFetchIncidents.js | 4 +- .../src/Pages/{v1 => }/Incidents2/index.jsx | 2 +- .../Create/Components/CustomAlertsSection.jsx | 2 +- .../Components/CustomThreshold/index.jsx | 0 .../Create/Components/DiskSelection.jsx | 0 .../Components/MonitorActionButtons.jsx | 0 .../Create/Components/MonitorStatusHeader.jsx | 2 +- .../hooks/useInfrastructureMonitorForm.jsx | 0 .../Create/hooks/useInfrastructureSubmit.jsx | 5 +- .../hooks/useValidateInfrastructureForm.jsx | 4 +- .../{v1 => }/Infrastructure/Create/index.jsx | 9 +- .../AreaChartBoxes/InfraAreaChart.jsx | 0 .../Components/AreaChartBoxes/index.jsx | 0 .../Components/AreaChartBoxes/skeleton.jsx | 0 .../Components/BaseContainer/index.jsx | 0 .../Details/Components/GaugeBoxes/Gauge.jsx | 0 .../Details/Components/GaugeBoxes/index.jsx | 0 .../Components/GaugeBoxes/skeleton.jsx | 0 .../Components/NetworkStats/NetworkCharts.jsx | 0 .../NetworkStats/NetworkStatBoxes.jsx | 0 .../Details/Components/NetworkStats/index.jsx | 0 .../Components/NetworkStats/skeleton.jsx | 0 .../Details/Components/StatusBoxes/index.jsx | 2 +- .../Details/Hooks/useHardwareUtils.jsx | 0 .../{v1 => }/Infrastructure/Details/index.jsx | 4 +- .../Monitors/Components/Filters/index.jsx | 0 .../Components/MonitorsTable/index.jsx | 4 +- .../Components/MonitorsTableMenu/index.jsx | 8 +- .../Infrastructure/Monitors/index.jsx | 6 +- .../src/Pages/{v1 => }/Integrations/index.css | 0 .../src/Pages/{v1 => }/Integrations/index.jsx | 0 .../Diagnostics/components/gauges/index.jsx | 0 .../Diagnostics/components/stats/index.jsx | 2 +- .../Pages/{v1 => }/Logs/Diagnostics/index.jsx | 12 +- .../{v1 => }/Logs/Diagnostics/utils/utils.js | 0 client/src/Pages/{v1 => }/Logs/Logs/index.jsx | 3 +- .../Queue/components/FailedJobTable/index.jsx | 2 +- .../Logs/Queue/components/JobTable/index.jsx | 2 +- .../Logs/Queue/components/Metrics/index.jsx | 0 .../Queue/components/MetricsTable/index.jsx | 0 .../src/Pages/{v1 => }/Logs/Queue/index.jsx | 2 +- client/src/Pages/{v1 => }/Logs/index.jsx | 0 .../Components/ConfigSelect/index.jsx | 0 .../Components/MonitorList/index.jsx | 0 .../Components/MonitorsConfig/index.jsx | 0 .../hooks/useMaintenanceActions.jsx | 4 +- .../hooks/useMaintenanceData.jsx | 4 +- .../Maintenance/CreateMaintenance/index.css | 0 .../Maintenance/CreateMaintenance/index.jsx | 10 +- .../MaintenanceTable/ActionsMenu/index.jsx | 8 +- .../Maintenance/MaintenanceTable/index.jsx | 4 +- .../src/Pages/{v1 => }/Maintenance/index.css | 0 .../src/Pages/{v1 => }/Maintenance/index.jsx | 2 +- client/src/Pages/{v1 => }/NotFound/index.jsx | 2 +- .../Notifications/components/ActionMenu.jsx | 0 .../{v1 => }/Notifications/create/index.jsx | 6 +- .../Pages/{v1 => }/Notifications/index.jsx | 2 +- .../src/Pages/{v1 => }/Notifications/utils.js | 0 .../Pages/{v1 => }/PageSpeed/Create/index.css | 0 .../Pages/{v1 => }/PageSpeed/Create/index.jsx | 15 +- .../{v1 => }/PageSpeed/Create/skeleton.jsx | 0 .../Details/Components/Charts/AreaChart.jsx | 2 +- .../Components/Charts/AreaChartLegend.jsx | 2 +- .../Details/Components/Charts/PieChart.jsx | 0 .../Components/Charts/PieChartLegend.jsx | 2 +- .../Components/PageSpeedAreaChart/index.jsx | 2 +- .../PageSpeedAreaChart/skeleton.jsx | 0 .../Components/PageSpeedStatusBoxes/index.jsx | 2 +- .../Components/PerformanceReport/index.jsx | 2 +- .../Components/PerformanceReport/skeleton.jsx | 0 .../{v1 => }/PageSpeed/Details/index.jsx | 4 +- .../Monitors/Components/Card/index.jsx | 11 +- .../Monitors/Components/MonitorGrid/index.jsx | 0 .../{v1 => }/PageSpeed/Monitors/index.jsx | 45 +- .../src/Pages/{v1 => }/ServerUnreachable.jsx | 10 +- .../Pages/{v1 => }/Settings/SettingsAbout.jsx | 0 .../Settings/SettingsDemoMonitors.jsx | 0 .../Pages/{v1 => }/Settings/SettingsEmail.jsx | 4 +- .../{v1 => }/Settings/SettingsExport.jsx | 0 .../Settings/SettingsGlobalThresholds.jsx | 0 .../{v1 => }/Settings/SettingsPagespeed.jsx | 0 .../Pages/{v1 => }/Settings/SettingsStats.jsx | 10 - .../{v1 => }/Settings/SettingsTimeZone.jsx | 2 +- .../Pages/{v1 => }/Settings/SettingsUI.jsx | 0 .../Pages/{v1 => }/Settings/SettingsURL.jsx | 0 client/src/Pages/{v1 => }/Settings/index.jsx | 15 +- .../Create/Components/MonitorList/index.jsx | 0 .../Create/Components/Progress/index.jsx | 2 +- .../Create/Components/Skeleton/index.jsx | 0 .../Create/Components/Tabs/ConfigStack.jsx | 0 .../Create/Components/Tabs/Content.jsx | 0 .../Create/Components/Tabs/Settings.jsx | 2 +- .../Create/Components/Tabs/index.jsx | 0 .../Create/Hooks/useCreateStatusPage.jsx | 4 +- .../Create/Hooks/useMonitorsFetch.jsx | 4 +- .../{v1 => }/StatusPage/Create/index.jsx | 6 +- .../Status/Components/AdminLink/index.jsx | 0 .../Components/ControlsHeader/index.jsx | 2 +- .../Status/Components/MonitorsList/index.jsx | 2 +- .../Status/Components/Skeleton/index.jsx | 0 .../Status/Components/StatusBar/index.jsx | 0 .../Status/Hooks/useStatusPageDelete.jsx | 4 +- .../Status/Hooks/useStatusPageFetch.jsx | 6 +- .../{v1 => }/StatusPage/Status/index.jsx | 2 +- .../Components/StatusPagesTable/index.jsx | 0 .../StatusPages/Hooks/useStatusPagesFetch.jsx | 4 +- .../{v1 => }/StatusPage/StatusPages/index.jsx | 2 +- .../{v1 => }/Uptime/BulkImport/Upload.jsx | 0 .../{v1 => }/Uptime/BulkImport/index.jsx | 4 +- .../Pages/{v1 => }/Uptime/Create/index.css | 0 .../Pages/{v1 => }/Uptime/Create/index.jsx | 13 +- .../Pages/{v1 => }/Uptime/Create/skeleton.jsx | 0 .../Details/Components/ChartBoxes/index.jsx | 8 +- .../Components/ChartBoxes/skeleton.jsx | 0 .../Components/Charts/CustomLabels.jsx | 2 +- .../Components/Charts/DownBarChart.jsx | 0 .../Components/Charts/ResponseGaugeChart.jsx | 0 .../Components/Charts/ResponseTimeChart.jsx | 2 +- .../Charts/ResponseTimeChartSkeleton.jsx | 0 .../Details/Components/Charts/UpBarChart.jsx | 0 .../Components/ResponseTable/index.jsx | 4 +- .../Components/ResponseTable/skeleton.jsx | 0 .../Components/UptimeStatusBoxes/index.jsx | 4 +- .../Details/Hooks/useCertificateFetch.jsx | 6 +- .../Pages/{v1 => }/Uptime/Details/index.jsx | 6 +- .../Monitors/Components/Filter/index.jsx | 0 .../Components/LoadingSpinner/index.jsx | 0 .../Components/SearchComponent/index.jsx | 0 .../Monitors/Components/Skeleton/index.jsx | 0 .../Monitors/Components/StatusBoxes/index.jsx | 0 .../Components/StatusBoxes/skeleton.jsx | 0 .../Components/StatusBoxes/statusBox.jsx | 6 +- .../Components/UptimeDataTable/index.jsx | 10 +- .../Uptime/Monitors/Hooks/useDebounce.jsx | 0 .../Pages/{v1 => }/Uptime/Monitors/index.jsx | 8 +- client/src/Routes/index.jsx | 62 +- server/src/db/models/Check.ts | 23 +- server/src/db/modules/checkModule.js | 2 +- server/src/db/modules/monitorModule.js | 1 - server/src/service/business/monitorService.js | 1 - .../service/infrastructure/statusService.js | 28 +- server/src/types/check.ts | 19 +- 204 files changed, 977 insertions(+), 343 deletions(-) rename client/src/Hooks/{v1 => }/checkHooks.js (97%) rename client/src/Hooks/{v1 => }/inviteHooks.js (97%) rename client/src/Hooks/{v1 => }/logHooks.js (96%) rename client/src/Hooks/{v1 => }/monitorHooks.js (98%) rename client/src/Hooks/{v1 => }/settingsHooks.js (89%) rename client/src/Hooks/{v1 => }/useIsAdmin.js (100%) rename client/src/Hooks/{v1 => }/useMonitorUtils.js (100%) rename client/src/Hooks/{v1 => }/useNotifications.js (96%) rename client/src/Hooks/{v1 => }/useSendTestEmail.js (93%) rename client/src/Hooks/{v1 => }/userHooks.js (94%) rename client/src/Pages/{v1 => }/About/index.jsx (100%) rename client/src/Pages/{v1 => }/Account/EditUser/hooks/editUser.js (91%) rename client/src/Pages/{v1 => }/Account/EditUser/index.jsx (96%) rename client/src/Pages/{v1 => }/Account/components/AddMemberMenu/index.jsx (100%) rename client/src/Pages/{v1 => }/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx (93%) rename client/src/Pages/{v1 => }/Account/components/AddTeamMember/index.jsx (97%) rename client/src/Pages/{v1 => }/Account/components/PasswordPanel.jsx (95%) rename client/src/Pages/{v1 => }/Account/components/ProfilePanel.jsx (97%) rename client/src/Pages/{v1 => }/Account/components/RoleTable/index.jsx (93%) rename client/src/Pages/{v1 => }/Account/components/TeamPanel.jsx (96%) rename client/src/Pages/{v1 => }/Account/index.jsx (100%) rename client/src/Pages/{v1 => }/Auth/CheckEmail.jsx (94%) rename client/src/Pages/{v1 => }/Auth/ForgotPassword.jsx (93%) rename client/src/Pages/{v1 => }/Auth/Login/hooks/useLoadingSubmit.jsx (100%) rename client/src/Pages/{v1 => }/Auth/Login/hooks/useLoginForm.jsx (100%) rename client/src/Pages/{v1 => }/Auth/Login/hooks/useLoginSubmit.jsx (87%) rename client/src/Pages/{v1 => }/Auth/Login/hooks/useValidateLoginForm.jsx (90%) rename client/src/Pages/{v1 => }/Auth/Login/index.jsx (100%) rename client/src/Pages/{v1 => }/Auth/NewPasswordConfirmed.jsx (92%) rename client/src/Pages/{v1 => }/Auth/Register/index.jsx (97%) rename client/src/Pages/{v1 => }/Auth/SetNewPassword.jsx (95%) rename client/src/Pages/{v1 => }/Auth/components/AuthHeader.jsx (87%) rename client/src/Pages/{v1 => }/Auth/components/AuthPageWrapper.jsx (93%) rename client/src/Pages/{v1 => }/Auth/components/PasswordTooltip.jsx (100%) rename client/src/Pages/{v1 => }/Auth/hooks/usePasswordFeedback.jsx (96%) rename client/src/Pages/{v1 => }/Auth/hooks/useValidatePassword.jsx (96%) rename client/src/Pages/{v1 => }/Auth/index.css (100%) rename client/src/Pages/{v1 => }/Incidents/Components/IncidentTable/index.jsx (95%) rename client/src/Pages/{v1 => }/Incidents/Components/OptionsHeader/index.jsx (100%) rename client/src/Pages/{v1 => }/Incidents/Components/OptionsHeader/skeleton.jsx (100%) rename client/src/Pages/{v1 => }/Incidents/Components/StatusBoxes/StatusBox.jsx (87%) rename client/src/Pages/{v1 => }/Incidents/Components/StatusBoxes/index.jsx (100%) rename client/src/Pages/{v1 => }/Incidents/Components/StatusBoxes/skeleton.jsx (100%) rename client/src/Pages/{v1 => }/Incidents/index.jsx (93%) rename client/src/Pages/{v1 => }/Incidents2/Components/ActiveIncidentsPanel/index.jsx (100%) rename client/src/Pages/{v1 => }/Incidents2/Components/IncidentTable/index.jsx (98%) rename client/src/Pages/{v1 => }/Incidents2/Components/IncidentsSummaryPanel/index.jsx (100%) rename client/src/Pages/{v1 => }/Incidents2/Components/IncidentsSummaryPanel/skeleton.jsx (100%) rename client/src/Pages/{v1 => }/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx (100%) rename client/src/Pages/{v1 => }/Incidents2/Components/LatestIncidentsPanel/index.jsx (100%) rename client/src/Pages/{v1 => }/Incidents2/Components/OptionsHeader/index.jsx (100%) rename client/src/Pages/{v1 => }/Incidents2/Components/StatisticsPanel/index.jsx (100%) rename client/src/Pages/{v1 => }/Incidents2/Components/SummaryCard/index.jsx (100%) rename client/src/Pages/{v1 => }/Incidents2/hooks/useFetchIncidents.js (97%) rename client/src/Pages/{v1 => }/Incidents2/index.jsx (97%) rename client/src/Pages/{v1 => }/Infrastructure/Create/Components/CustomAlertsSection.jsx (96%) rename client/src/Pages/{v1 => }/Infrastructure/Create/Components/CustomThreshold/index.jsx (100%) rename client/src/Pages/{v1 => }/Infrastructure/Create/Components/DiskSelection.jsx (100%) rename client/src/Pages/{v1 => }/Infrastructure/Create/Components/MonitorActionButtons.jsx (100%) rename client/src/Pages/{v1 => }/Infrastructure/Create/Components/MonitorStatusHeader.jsx (95%) rename client/src/Pages/{v1 => }/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx (100%) rename client/src/Pages/{v1 => }/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx (96%) rename client/src/Pages/{v1 => }/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx (85%) rename client/src/Pages/{v1 => }/Infrastructure/Create/index.jsx (97%) rename client/src/Pages/{v1 => }/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx (100%) rename client/src/Pages/{v1 => }/Infrastructure/Details/Components/AreaChartBoxes/index.jsx (100%) rename client/src/Pages/{v1 => }/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx (100%) rename client/src/Pages/{v1 => }/Infrastructure/Details/Components/BaseContainer/index.jsx (100%) rename client/src/Pages/{v1 => }/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx (100%) rename client/src/Pages/{v1 => }/Infrastructure/Details/Components/GaugeBoxes/index.jsx (100%) rename client/src/Pages/{v1 => }/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx (100%) rename client/src/Pages/{v1 => }/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx (100%) rename client/src/Pages/{v1 => }/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx (100%) rename client/src/Pages/{v1 => }/Infrastructure/Details/Components/NetworkStats/index.jsx (100%) rename client/src/Pages/{v1 => }/Infrastructure/Details/Components/NetworkStats/skeleton.jsx (100%) rename client/src/Pages/{v1 => }/Infrastructure/Details/Components/StatusBoxes/index.jsx (97%) rename client/src/Pages/{v1 => }/Infrastructure/Details/Hooks/useHardwareUtils.jsx (100%) rename client/src/Pages/{v1 => }/Infrastructure/Details/index.jsx (96%) rename client/src/Pages/{v1 => }/Infrastructure/Monitors/Components/Filters/index.jsx (100%) rename client/src/Pages/{v1 => }/Infrastructure/Monitors/Components/MonitorsTable/index.jsx (95%) rename client/src/Pages/{v1 => }/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx (94%) rename client/src/Pages/{v1 => }/Infrastructure/Monitors/index.jsx (94%) rename client/src/Pages/{v1 => }/Integrations/index.css (100%) rename client/src/Pages/{v1 => }/Integrations/index.jsx (100%) rename client/src/Pages/{v1 => }/Logs/Diagnostics/components/gauges/index.jsx (100%) rename client/src/Pages/{v1 => }/Logs/Diagnostics/components/stats/index.jsx (96%) rename client/src/Pages/{v1 => }/Logs/Diagnostics/index.jsx (88%) rename client/src/Pages/{v1 => }/Logs/Diagnostics/utils/utils.js (100%) rename client/src/Pages/{v1 => }/Logs/Logs/index.jsx (97%) rename client/src/Pages/{v1 => }/Logs/Queue/components/FailedJobTable/index.jsx (96%) rename client/src/Pages/{v1 => }/Logs/Queue/components/JobTable/index.jsx (97%) rename client/src/Pages/{v1 => }/Logs/Queue/components/Metrics/index.jsx (100%) rename client/src/Pages/{v1 => }/Logs/Queue/components/MetricsTable/index.jsx (100%) rename client/src/Pages/{v1 => }/Logs/Queue/index.jsx (95%) rename client/src/Pages/{v1 => }/Logs/index.jsx (100%) rename client/src/Pages/{v1 => }/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx (100%) rename client/src/Pages/{v1 => }/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx (100%) rename client/src/Pages/{v1 => }/Maintenance/CreateMaintenance/Components/MonitorsConfig/index.jsx (100%) rename client/src/Pages/{v1 => }/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx (93%) rename client/src/Pages/{v1 => }/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx (95%) rename client/src/Pages/{v1 => }/Maintenance/CreateMaintenance/index.css (100%) rename client/src/Pages/{v1 => }/Maintenance/CreateMaintenance/index.jsx (97%) rename client/src/Pages/{v1 => }/Maintenance/MaintenanceTable/ActionsMenu/index.jsx (93%) rename client/src/Pages/{v1 => }/Maintenance/MaintenanceTable/index.jsx (97%) rename client/src/Pages/{v1 => }/Maintenance/index.css (100%) rename client/src/Pages/{v1 => }/Maintenance/index.jsx (98%) rename client/src/Pages/{v1 => }/NotFound/index.jsx (96%) rename client/src/Pages/{v1 => }/Notifications/components/ActionMenu.jsx (100%) rename client/src/Pages/{v1 => }/Notifications/create/index.jsx (97%) rename client/src/Pages/{v1 => }/Notifications/index.jsx (98%) rename client/src/Pages/{v1 => }/Notifications/utils.js (100%) rename client/src/Pages/{v1 => }/PageSpeed/Create/index.css (100%) rename client/src/Pages/{v1 => }/PageSpeed/Create/index.jsx (96%) rename client/src/Pages/{v1 => }/PageSpeed/Create/skeleton.jsx (100%) rename client/src/Pages/{v1 => }/PageSpeed/Details/Components/Charts/AreaChart.jsx (99%) rename client/src/Pages/{v1 => }/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx (94%) rename client/src/Pages/{v1 => }/PageSpeed/Details/Components/Charts/PieChart.jsx (100%) rename client/src/Pages/{v1 => }/PageSpeed/Details/Components/Charts/PieChartLegend.jsx (96%) rename client/src/Pages/{v1 => }/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx (93%) rename client/src/Pages/{v1 => }/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx (100%) rename client/src/Pages/{v1 => }/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx (91%) rename client/src/Pages/{v1 => }/PageSpeed/Details/Components/PerformanceReport/index.jsx (93%) rename client/src/Pages/{v1 => }/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx (100%) rename client/src/Pages/{v1 => }/PageSpeed/Details/index.jsx (95%) rename client/src/Pages/{v1 => }/PageSpeed/Monitors/Components/Card/index.jsx (96%) rename client/src/Pages/{v1 => }/PageSpeed/Monitors/Components/MonitorGrid/index.jsx (100%) rename client/src/Pages/{v1 => }/PageSpeed/Monitors/index.jsx (64%) rename client/src/Pages/{v1 => }/ServerUnreachable.jsx (92%) rename client/src/Pages/{v1 => }/Settings/SettingsAbout.jsx (100%) rename client/src/Pages/{v1 => }/Settings/SettingsDemoMonitors.jsx (100%) rename client/src/Pages/{v1 => }/Settings/SettingsEmail.jsx (98%) rename client/src/Pages/{v1 => }/Settings/SettingsExport.jsx (100%) rename client/src/Pages/{v1 => }/Settings/SettingsGlobalThresholds.jsx (100%) rename client/src/Pages/{v1 => }/Settings/SettingsPagespeed.jsx (100%) rename client/src/Pages/{v1 => }/Settings/SettingsStats.jsx (86%) rename client/src/Pages/{v1 => }/Settings/SettingsTimeZone.jsx (97%) rename client/src/Pages/{v1 => }/Settings/SettingsUI.jsx (100%) rename client/src/Pages/{v1 => }/Settings/SettingsURL.jsx (100%) rename client/src/Pages/{v1 => }/Settings/index.jsx (95%) rename client/src/Pages/{v1 => }/StatusPage/Create/Components/MonitorList/index.jsx (100%) rename client/src/Pages/{v1 => }/StatusPage/Create/Components/Progress/index.jsx (92%) rename client/src/Pages/{v1 => }/StatusPage/Create/Components/Skeleton/index.jsx (100%) rename client/src/Pages/{v1 => }/StatusPage/Create/Components/Tabs/ConfigStack.jsx (100%) rename client/src/Pages/{v1 => }/StatusPage/Create/Components/Tabs/Content.jsx (100%) rename client/src/Pages/{v1 => }/StatusPage/Create/Components/Tabs/Settings.jsx (98%) rename client/src/Pages/{v1 => }/StatusPage/Create/Components/Tabs/index.jsx (100%) rename client/src/Pages/{v1 => }/StatusPage/Create/Hooks/useCreateStatusPage.jsx (83%) rename client/src/Pages/{v1 => }/StatusPage/Create/Hooks/useMonitorsFetch.jsx (88%) rename client/src/Pages/{v1 => }/StatusPage/Create/index.jsx (97%) rename client/src/Pages/{v1 => }/StatusPage/Status/Components/AdminLink/index.jsx (100%) rename client/src/Pages/{v1 => }/StatusPage/Status/Components/ControlsHeader/index.jsx (97%) rename client/src/Pages/{v1 => }/StatusPage/Status/Components/MonitorsList/index.jsx (95%) rename client/src/Pages/{v1 => }/StatusPage/Status/Components/Skeleton/index.jsx (100%) rename client/src/Pages/{v1 => }/StatusPage/Status/Components/StatusBar/index.jsx (100%) rename client/src/Pages/{v1 => }/StatusPage/Status/Hooks/useStatusPageDelete.jsx (89%) rename client/src/Pages/{v1 => }/StatusPage/Status/Hooks/useStatusPageFetch.jsx (87%) rename client/src/Pages/{v1 => }/StatusPage/Status/index.jsx (98%) rename client/src/Pages/{v1 => }/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx (100%) rename client/src/Pages/{v1 => }/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx (86%) rename client/src/Pages/{v1 => }/StatusPage/StatusPages/index.jsx (95%) rename client/src/Pages/{v1 => }/Uptime/BulkImport/Upload.jsx (100%) rename client/src/Pages/{v1 => }/Uptime/BulkImport/index.jsx (95%) rename client/src/Pages/{v1 => }/Uptime/Create/index.css (100%) rename client/src/Pages/{v1 => }/Uptime/Create/index.jsx (98%) rename client/src/Pages/{v1 => }/Uptime/Create/skeleton.jsx (100%) rename client/src/Pages/{v1 => }/Uptime/Details/Components/ChartBoxes/index.jsx (92%) rename client/src/Pages/{v1 => }/Uptime/Details/Components/ChartBoxes/skeleton.jsx (100%) rename client/src/Pages/{v1 => }/Uptime/Details/Components/Charts/CustomLabels.jsx (93%) rename client/src/Pages/{v1 => }/Uptime/Details/Components/Charts/DownBarChart.jsx (100%) rename client/src/Pages/{v1 => }/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx (100%) rename client/src/Pages/{v1 => }/Uptime/Details/Components/Charts/ResponseTimeChart.jsx (89%) rename client/src/Pages/{v1 => }/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx (100%) rename client/src/Pages/{v1 => }/Uptime/Details/Components/Charts/UpBarChart.jsx (100%) rename client/src/Pages/{v1 => }/Uptime/Details/Components/ResponseTable/index.jsx (97%) rename client/src/Pages/{v1 => }/Uptime/Details/Components/ResponseTable/skeleton.jsx (100%) rename client/src/Pages/{v1 => }/Uptime/Details/Components/UptimeStatusBoxes/index.jsx (92%) rename client/src/Pages/{v1 => }/Uptime/Details/Hooks/useCertificateFetch.jsx (84%) rename client/src/Pages/{v1 => }/Uptime/Details/index.jsx (95%) rename client/src/Pages/{v1 => }/Uptime/Monitors/Components/Filter/index.jsx (100%) rename client/src/Pages/{v1 => }/Uptime/Monitors/Components/LoadingSpinner/index.jsx (100%) rename client/src/Pages/{v1 => }/Uptime/Monitors/Components/SearchComponent/index.jsx (100%) rename client/src/Pages/{v1 => }/Uptime/Monitors/Components/Skeleton/index.jsx (100%) rename client/src/Pages/{v1 => }/Uptime/Monitors/Components/StatusBoxes/index.jsx (100%) rename client/src/Pages/{v1 => }/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx (100%) rename client/src/Pages/{v1 => }/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx (90%) rename client/src/Pages/{v1 => }/Uptime/Monitors/Components/UptimeDataTable/index.jsx (97%) rename client/src/Pages/{v1 => }/Uptime/Monitors/Hooks/useDebounce.jsx (100%) rename client/src/Pages/{v1 => }/Uptime/Monitors/index.jsx (96%) diff --git a/client/package-lock.json b/client/package-lock.json index 75dbe43da..e917619ae 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -499,6 +499,262 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==", "license": "MIT" }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/linux-x64": { "version": "0.25.5", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", @@ -515,6 +771,134 @@ "node": ">=18" } }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -1696,6 +2080,123 @@ } } }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz", + "integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz", + "integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz", + "integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz", + "integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz", + "integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz", + "integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz", + "integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz", + "integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz", + "integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-arm64-musl": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz", @@ -1709,6 +2210,71 @@ "linux" ] }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz", + "integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz", + "integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz", + "integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz", + "integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz", + "integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@rollup/rollup-linux-x64-gnu": { "version": "4.41.0", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz", @@ -1735,6 +2301,45 @@ "linux" ] }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz", + "integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz", + "integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz", + "integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", diff --git a/client/src/Components/v1/ActionsMenu/index.jsx b/client/src/Components/v1/ActionsMenu/index.jsx index 229c7c875..5cd0dfa5b 100644 --- a/client/src/Components/v1/ActionsMenu/index.jsx +++ b/client/src/Components/v1/ActionsMenu/index.jsx @@ -12,7 +12,7 @@ import { useNavigate } from "react-router-dom"; import { createToast } from "../../../Utils/toastUtils.jsx"; import PropTypes from "prop-types"; -import { usePauseMonitor, useDeleteMonitor } from "../../../Hooks/v1/monitorHooks.js"; +import { usePauseMonitor, useDeleteMonitor } from "../../../Hooks/monitorHooks.js"; const ActionsMenu = ({ monitor, @@ -31,7 +31,8 @@ const ActionsMenu = ({ const handleRemove = async (event) => { event.preventDefault(); event.stopPropagation(); - let monitor = { _id: actions.id }; + let monitor = { id: actions.id }; + console.log(monitor); await deleteMonitor({ monitor }); updateRowCallback(); }; @@ -39,7 +40,7 @@ const ActionsMenu = ({ const handlePause = async () => { try { setIsLoading(true); - await pauseMonitor({ monitorId: monitor._id }); + await pauseMonitor({ monitorId: monitor.id }); pauseCallback(); } catch (error) { createToast({ body: "Failed to pause monitor." }); @@ -72,7 +73,7 @@ const ActionsMenu = ({ aria-label="monitor actions" onClick={(event) => { event.stopPropagation(); - openMenu(event, monitor._id, monitor.type === "ping" ? null : monitor.url); + openMenu(event, monitor.id, monitor.type === "ping" ? null : monitor.url); }} sx={{ "&:focus": { @@ -197,6 +198,7 @@ const ActionsMenu = ({ confirmationButtonLabel="Delete" /* Do we need stop propagation? */ onConfirm={(e) => { + console.log(e); e.stopPropagation(); handleRemove(e); }} @@ -210,7 +212,7 @@ const ActionsMenu = ({ ActionsMenu.propTypes = { monitor: PropTypes.shape({ - _id: PropTypes.string, + id: PropTypes.string, url: PropTypes.string, type: PropTypes.string, isActive: PropTypes.bool, diff --git a/client/src/Components/v1/Charts/BarChart/index.jsx b/client/src/Components/v1/Charts/BarChart/index.jsx index 2bf8e1798..9f0ed8aa6 100644 --- a/client/src/Components/v1/Charts/BarChart/index.jsx +++ b/client/src/Components/v1/Charts/BarChart/index.jsx @@ -103,7 +103,7 @@ const BarChart = ({ checks = [] }) => { } placement="top" - key={`check-${check?._id}`} + key={`check-${check?.id}`} slotProps={{ popper: { className: "bar-tooltip", @@ -145,7 +145,7 @@ const BarChart = ({ checks = [] }) => { position="relative" width="9px" height="100%" - backgroundColor={theme.palette.primary.lowContrast} // CAIO_REVIEW + backgroundColor={theme.palette.primary.lowContrast} sx={{ borderRadius: theme.spacing(1.5), /* diff --git a/client/src/Components/v1/MonitorActions/index.jsx b/client/src/Components/v1/MonitorActions/index.jsx index 23cebc972..624d34b9a 100644 --- a/client/src/Components/v1/MonitorActions/index.jsx +++ b/client/src/Components/v1/MonitorActions/index.jsx @@ -11,7 +11,7 @@ import MenuList from "@mui/material/MenuList"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { createToast } from "../../../Utils/toastUtils.jsx"; -import { useExportMonitors } from "../../../Hooks/v1/monitorHooks.js"; +import { useExportMonitors } from "../../../Hooks/monitorHooks.js"; const MonitorActions = ({ isLoading }) => { const [open, setOpen] = React.useState(false); diff --git a/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx b/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx index ae565f3bb..bb4399b85 100644 --- a/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx +++ b/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx @@ -12,10 +12,10 @@ import EmailIcon from "@mui/icons-material/Email"; import PropTypes from "prop-types"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@mui/material/styles"; -import { usePauseMonitor } from "../../../Hooks/v1/monitorHooks.js"; -import { useSendTestEmail } from "../../../Hooks/v1/useSendTestEmail.js"; +import { usePauseMonitor } from "../../../Hooks/monitorHooks.js"; +import { useSendTestEmail } from "../../../Hooks/useSendTestEmail.js"; import { useTranslation } from "react-i18next"; -import { useTestAllNotifications } from "../../../Hooks/v1/useNotifications.js"; +import { useTestAllNotifications } from "../../../Hooks/useNotifications.js"; /** * MonitorDetailsControlHeader component displays the control header for monitor details. * It includes status display, pause/resume button, and a configure button for admins. diff --git a/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx b/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx index d9b7afc40..febf449d4 100644 --- a/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx +++ b/client/src/Components/v1/MonitorDetailsControlHeader/status.jsx @@ -7,7 +7,7 @@ import Dot from "../Dot/index.jsx"; import { formatDurationRounded } from "../../../Utils/timeUtils.js"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "../../../Hooks/v1/useMonitorUtils.js"; +import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js"; import { formatMonitorUrl } from "../../../Utils/utils.js"; /** * Status component displays the status information of a monitor. diff --git a/client/src/Components/v1/PageStateWrapper/index.jsx b/client/src/Components/v1/PageStateWrapper/index.jsx index ac97704df..08adbe514 100644 --- a/client/src/Components/v1/PageStateWrapper/index.jsx +++ b/client/src/Components/v1/PageStateWrapper/index.jsx @@ -3,7 +3,7 @@ import Fallback from "../Fallback/index.jsx"; import { Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import { useIsAdmin } from "../../../Hooks/v1/useIsAdmin.js"; +import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; import PropTypes from "prop-types"; const PageStateWrapper = ({ networkError, diff --git a/client/src/Components/v1/StatBox/index.jsx b/client/src/Components/v1/StatBox/index.jsx index 1b5f1cf6f..e72f947b3 100644 --- a/client/src/Components/v1/StatBox/index.jsx +++ b/client/src/Components/v1/StatBox/index.jsx @@ -2,7 +2,7 @@ import { Stack, Typography } from "@mui/material"; import Image from "../Image/index.jsx"; import { useTheme } from "@mui/material/styles"; import PropTypes from "prop-types"; -import { useMonitorUtils } from "../../../Hooks/v1/useMonitorUtils.js"; +import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js"; /** * StatBox Component diff --git a/client/src/Hooks/v1/checkHooks.js b/client/src/Hooks/checkHooks.js similarity index 97% rename from client/src/Hooks/v1/checkHooks.js rename to client/src/Hooks/checkHooks.js index 96fd1fe1c..b8af32073 100644 --- a/client/src/Hooks/v1/checkHooks.js +++ b/client/src/Hooks/checkHooks.js @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; -import { networkService } from "../../main.jsx"; -import { createToast } from "../../Utils/toastUtils.jsx"; +import { networkService } from "../main.jsx"; +import { createToast } from "../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; const useFetchChecksTeam = ({ diff --git a/client/src/Hooks/v1/inviteHooks.js b/client/src/Hooks/inviteHooks.js similarity index 97% rename from client/src/Hooks/v1/inviteHooks.js rename to client/src/Hooks/inviteHooks.js index b6a0851af..c862eec31 100644 --- a/client/src/Hooks/v1/inviteHooks.js +++ b/client/src/Hooks/inviteHooks.js @@ -1,5 +1,5 @@ import { useState } from "react"; -import { networkService } from "../../main.jsx"; +import { networkService } from "../main.jsx"; import { useTranslation } from "react-i18next"; const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST; diff --git a/client/src/Hooks/v1/logHooks.js b/client/src/Hooks/logHooks.js similarity index 96% rename from client/src/Hooks/v1/logHooks.js rename to client/src/Hooks/logHooks.js index ff4d13872..d5162de5a 100644 --- a/client/src/Hooks/v1/logHooks.js +++ b/client/src/Hooks/logHooks.js @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; -import { networkService } from "../../main.jsx"; -import { createToast } from "../../Utils/toastUtils.jsx"; +import { networkService } from "../main.jsx"; +import { createToast } from "../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; const useFetchLogs = () => { diff --git a/client/src/Hooks/v1/monitorHooks.js b/client/src/Hooks/monitorHooks.js similarity index 98% rename from client/src/Hooks/v1/monitorHooks.js rename to client/src/Hooks/monitorHooks.js index 8acbea608..ffc356320 100644 --- a/client/src/Hooks/v1/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; -import { networkService } from "../../main.jsx"; -import { createToast } from "../../Utils/toastUtils.jsx"; +import { networkService } from "../main.jsx"; +import { createToast } from "../Utils/toastUtils.jsx"; import { useTheme } from "@emotion/react"; import { useMonitorUtils } from "./useMonitorUtils.js"; import { useNavigate } from "react-router-dom"; @@ -67,12 +67,13 @@ const useFetchMonitorsWithChecks = ({ field, order, }); + const { count, monitors } = res?.data?.data ?? {}; const mappedMonitors = monitors.map((monitor) => getMonitorWithPercentage(monitor, theme) ); setMonitors(mappedMonitors); - setCount(count?.monitorsCount ?? 0); + setCount(count || 0); } catch (error) { console.error(error); setNetworkError(true); @@ -347,7 +348,8 @@ const useDeleteMonitor = () => { const deleteMonitor = async ({ monitor, redirect }) => { try { setIsLoading(true); - await networkService.deleteMonitorById({ monitorId: monitor._id }); + console.log(monitor); + await networkService.deleteMonitorById({ monitorId: monitor.id }); createToast({ body: "Monitor deleted successfully!" }); if (redirect) { navigate(redirect); diff --git a/client/src/Hooks/v1/settingsHooks.js b/client/src/Hooks/settingsHooks.js similarity index 89% rename from client/src/Hooks/v1/settingsHooks.js rename to client/src/Hooks/settingsHooks.js index 787e46ff9..8c4bef385 100644 --- a/client/src/Hooks/v1/settingsHooks.js +++ b/client/src/Hooks/settingsHooks.js @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; -import { networkService } from "../../main.jsx"; -import { createToast } from "../../Utils/toastUtils.jsx"; +import { networkService } from "../main.jsx"; +import { createToast } from "../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; const useFetchSettings = ({ setSettingsData, setIsApiKeySet, setIsEmailPasswordSet }) => { @@ -42,11 +42,11 @@ const useSaveSettings = ({ setIsLoading(true); try { const settingsResponse = await networkService.updateAppSettings({ settings }); - if (settings.checkTTL) { - await networkService.updateChecksTTL({ - ttl: settings.checkTTL, - }); - } + // if (settings.checkTTL) { + // await networkService.updateChecksTTL({ + // ttl: settings.checkTTL, + // }); + // } setIsApiKeySet(settingsResponse.data.data.pagespeedKeySet); setIsEmailPasswordSet(settingsResponse.data.data.emailPasswordSet); if (settingsResponse.data.data.pagespeedKeySet === true) { diff --git a/client/src/Hooks/v1/useIsAdmin.js b/client/src/Hooks/useIsAdmin.js similarity index 100% rename from client/src/Hooks/v1/useIsAdmin.js rename to client/src/Hooks/useIsAdmin.js diff --git a/client/src/Hooks/v1/useMonitorUtils.js b/client/src/Hooks/useMonitorUtils.js similarity index 100% rename from client/src/Hooks/v1/useMonitorUtils.js rename to client/src/Hooks/useMonitorUtils.js diff --git a/client/src/Hooks/v1/useNotifications.js b/client/src/Hooks/useNotifications.js similarity index 96% rename from client/src/Hooks/v1/useNotifications.js rename to client/src/Hooks/useNotifications.js index d1b4a6652..5f2058b6a 100644 --- a/client/src/Hooks/v1/useNotifications.js +++ b/client/src/Hooks/useNotifications.js @@ -1,9 +1,9 @@ import { useState, useEffect, useCallback } from "react"; -import { createToast } from "../../Utils/toastUtils.jsx"; -import { networkService } from "../../main.jsx"; +import { createToast } from "../Utils/toastUtils.jsx"; +import { networkService } from "../main.jsx"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; -import { NOTIFICATION_TYPES } from "../../Pages/v1/Notifications/utils.js"; +import { NOTIFICATION_TYPES } from "../Pages/Notifications/utils.js"; const useCreateNotification = () => { const navigate = useNavigate(); diff --git a/client/src/Hooks/v1/useSendTestEmail.js b/client/src/Hooks/useSendTestEmail.js similarity index 93% rename from client/src/Hooks/v1/useSendTestEmail.js rename to client/src/Hooks/useSendTestEmail.js index 0be77954a..7223a489a 100644 --- a/client/src/Hooks/v1/useSendTestEmail.js +++ b/client/src/Hooks/useSendTestEmail.js @@ -1,7 +1,7 @@ import { useState } from "react"; -import { networkService } from "../../main.jsx"; +import { networkService } from "../main.jsx"; import { useSelector } from "react-redux"; -import { createToast } from "../../Utils/toastUtils.jsx"; +import { createToast } from "../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; const useSendTestEmail = () => { diff --git a/client/src/Hooks/v1/userHooks.js b/client/src/Hooks/userHooks.js similarity index 94% rename from client/src/Hooks/v1/userHooks.js rename to client/src/Hooks/userHooks.js index f9c454450..3d822f979 100644 --- a/client/src/Hooks/v1/userHooks.js +++ b/client/src/Hooks/userHooks.js @@ -1,6 +1,6 @@ import { useEffect, useState, useCallback } from "react"; -import { networkService } from "../../main.jsx"; -import { createToast } from "../../Utils/toastUtils.jsx"; +import { networkService } from "../main.jsx"; +import { createToast } from "../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; export const useGetUser = (userId) => { diff --git a/client/src/Pages/v1/About/index.jsx b/client/src/Pages/About/index.jsx similarity index 100% rename from client/src/Pages/v1/About/index.jsx rename to client/src/Pages/About/index.jsx diff --git a/client/src/Pages/v1/Account/EditUser/hooks/editUser.js b/client/src/Pages/Account/EditUser/hooks/editUser.js similarity index 91% rename from client/src/Pages/v1/Account/EditUser/hooks/editUser.js rename to client/src/Pages/Account/EditUser/hooks/editUser.js index 00660aead..aeb50ffff 100644 --- a/client/src/Pages/v1/Account/EditUser/hooks/editUser.js +++ b/client/src/Pages/Account/EditUser/hooks/editUser.js @@ -1,8 +1,8 @@ import { useState, useEffect } from "react"; -import { ROLES } from "../../../../../Utils/roleUtils.js"; -import { editUserValidation } from "../../../../../Validation/validation.js"; +import { ROLES } from "../../../../Utils/roleUtils.js"; +import { editUserValidation } from "../../../../Validation/validation.js"; import Joi from "joi"; -import { createToast } from "../../../../../Utils/toastUtils.jsx"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; export const useEditUserForm = (user) => { const [searchInput, setSearchInput] = useState(""); diff --git a/client/src/Pages/v1/Account/EditUser/index.jsx b/client/src/Pages/Account/EditUser/index.jsx similarity index 96% rename from client/src/Pages/v1/Account/EditUser/index.jsx rename to client/src/Pages/Account/EditUser/index.jsx index 674a522b8..2b2ac5128 100644 --- a/client/src/Pages/v1/Account/EditUser/index.jsx +++ b/client/src/Pages/Account/EditUser/index.jsx @@ -11,8 +11,8 @@ import ChangePasswordModal from "@/Pages/Account/components/ChangePasswordModal/ import { useParams } from "react-router-dom"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import { useGetUser, useEditUser } from "../../../../Hooks/v1/userHooks.js"; -import { EDITABLE_ROLES, ROLES } from "../../../../Utils/roleUtils.js"; +import { useGetUser, useEditUser } from "../../../Hooks/userHooks.js"; +import { EDITABLE_ROLES, ROLES } from "../../../Utils/roleUtils.js"; import { useEditUserForm, useValidateEditUserForm } from "./hooks/editUser.js"; import { useSelector } from "react-redux"; diff --git a/client/src/Pages/v1/Account/components/AddMemberMenu/index.jsx b/client/src/Pages/Account/components/AddMemberMenu/index.jsx similarity index 100% rename from client/src/Pages/v1/Account/components/AddMemberMenu/index.jsx rename to client/src/Pages/Account/components/AddMemberMenu/index.jsx diff --git a/client/src/Pages/v1/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx b/client/src/Pages/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx similarity index 93% rename from client/src/Pages/v1/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx rename to client/src/Pages/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx index 58db019a3..1237777ff 100644 --- a/client/src/Pages/v1/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx +++ b/client/src/Pages/Account/components/AddTeamMember/hooks/useAddTeamMember.jsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { newOrChangedCredentials } from "../../../../../../Validation/validation.js"; +import { newOrChangedCredentials } from "../../../../../Validation/validation.js"; import { useTranslation } from "react-i18next"; const useAddTeamMember = () => { const { t } = useTranslation(); diff --git a/client/src/Pages/v1/Account/components/AddTeamMember/index.jsx b/client/src/Pages/Account/components/AddTeamMember/index.jsx similarity index 97% rename from client/src/Pages/v1/Account/components/AddTeamMember/index.jsx rename to client/src/Pages/Account/components/AddTeamMember/index.jsx index 219658b63..e5280af16 100644 --- a/client/src/Pages/v1/Account/components/AddTeamMember/index.jsx +++ b/client/src/Pages/Account/components/AddTeamMember/index.jsx @@ -2,10 +2,10 @@ import { Button, Stack } from "@mui/material"; import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; import Select from "@/Components/v1/Inputs/Select/index.jsx"; -import { useGetInviteToken } from "../../../../../Hooks/v1/inviteHooks.js"; +import { useGetInviteToken } from "../../../../Hooks/inviteHooks.js"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import { createToast } from "../../../../../Utils/toastUtils.jsx"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; import { useState } from "react"; import PasswordTooltip from "../../../Auth/components/PasswordTooltip.jsx"; import useAddTeamMember from "./hooks/useAddTeamMember.jsx"; diff --git a/client/src/Pages/Account/components/ChangePasswordModal/index.jsx b/client/src/Pages/Account/components/ChangePasswordModal/index.jsx index cc284a675..725a9b00e 100644 --- a/client/src/Pages/Account/components/ChangePasswordModal/index.jsx +++ b/client/src/Pages/Account/components/ChangePasswordModal/index.jsx @@ -2,12 +2,12 @@ import { useState } from "react"; import { Button, Stack } from "@mui/material"; import { GenericDialog } from "@/Components/v1/Dialog/genericDialog"; import TextInput from "@/Components/v1/Inputs/TextInput"; -import PasswordTooltip from "@/Pages/v1/Auth/components/PasswordTooltip"; +import PasswordTooltip from "@/Pages/Auth/components/PasswordTooltip"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; import { createToast } from "../../../../Utils/toastUtils"; import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments"; -import usePasswordFeedback from "@/Pages/v1/Auth/hooks/usePasswordFeedback"; +import usePasswordFeedback from "@/Pages/Auth/hooks/usePasswordFeedback"; import PropTypes from "prop-types"; const ChangePasswordModal = ({ isSaving, isLoading, changePassword }) => { diff --git a/client/src/Pages/v1/Account/components/PasswordPanel.jsx b/client/src/Pages/Account/components/PasswordPanel.jsx similarity index 95% rename from client/src/Pages/v1/Account/components/PasswordPanel.jsx rename to client/src/Pages/Account/components/PasswordPanel.jsx index ca2af9205..f54c61a16 100644 --- a/client/src/Pages/v1/Account/components/PasswordPanel.jsx +++ b/client/src/Pages/Account/components/PasswordPanel.jsx @@ -4,12 +4,12 @@ import { useTheme } from "@emotion/react"; import { Box, Stack, Typography, Button } from "@mui/material"; import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import { newOrChangedCredentials } from "../../../../Validation/validation.js"; +import { newOrChangedCredentials } from "../../../Validation/validation.js"; import Alert from "@/Components/v1/Alert/index.jsx"; -import { update } from "../../../../Features/Auth/authSlice.js"; +import { update } from "../../../Features/Auth/authSlice.js"; import { useDispatch, useSelector } from "react-redux"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; -import { getTouchedFieldErrors } from "../../../../Validation/error.js"; +import { createToast } from "../../../Utils/toastUtils.jsx"; +import { getTouchedFieldErrors } from "../../../Validation/error.js"; import { useTranslation } from "react-i18next"; const defaultPasswordsState = { diff --git a/client/src/Pages/v1/Account/components/ProfilePanel.jsx b/client/src/Pages/Account/components/ProfilePanel.jsx similarity index 97% rename from client/src/Pages/v1/Account/components/ProfilePanel.jsx rename to client/src/Pages/Account/components/ProfilePanel.jsx index 2035861a2..d59923ae4 100644 --- a/client/src/Pages/v1/Account/components/ProfilePanel.jsx +++ b/client/src/Pages/Account/components/ProfilePanel.jsx @@ -5,15 +5,11 @@ import { Box, Button, Divider, Stack, Typography } from "@mui/material"; import Avatar from "@/Components/v1/Avatar/index.jsx"; import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; import ImageUpload from "@/Components/v1/Inputs/ImageUpload/index.jsx"; -import { newOrChangedCredentials } from "../../../../Validation/validation.js"; +import { newOrChangedCredentials } from "../../../Validation/validation.js"; import { useDispatch, useSelector } from "react-redux"; -import { - clearAuthState, - deleteUser, - update, -} from "../../../../Features/Auth/authSlice.js"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; -import { logger } from "../../../../Utils/Logger.js"; +import { clearAuthState, deleteUser, update } from "../../../Features/Auth/authSlice.js"; +import { createToast } from "../../../Utils/toastUtils.jsx"; +import { logger } from "../../../Utils/Logger.js"; import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; import Dialog from "@/Components/v1/Dialog/index.jsx"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/v1/Account/components/RoleTable/index.jsx b/client/src/Pages/Account/components/RoleTable/index.jsx similarity index 93% rename from client/src/Pages/v1/Account/components/RoleTable/index.jsx rename to client/src/Pages/Account/components/RoleTable/index.jsx index 02dcaaf0d..5eea33685 100644 --- a/client/src/Pages/v1/Account/components/RoleTable/index.jsx +++ b/client/src/Pages/Account/components/RoleTable/index.jsx @@ -3,7 +3,7 @@ import DataTable from "@/Components/v1/Table/index.jsx"; import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded"; import { useTranslation } from "react-i18next"; -import { ROLES } from "../../../../../Utils/roleUtils.js"; +import { ROLES } from "../../../../Utils/roleUtils.js"; const RoleTable = ({ roles, handleDeleteRole }) => { const { t } = useTranslation(); diff --git a/client/src/Pages/v1/Account/components/TeamPanel.jsx b/client/src/Pages/Account/components/TeamPanel.jsx similarity index 96% rename from client/src/Pages/v1/Account/components/TeamPanel.jsx rename to client/src/Pages/Account/components/TeamPanel.jsx index 02918eb30..03a6145a5 100644 --- a/client/src/Pages/v1/Account/components/TeamPanel.jsx +++ b/client/src/Pages/Account/components/TeamPanel.jsx @@ -4,16 +4,16 @@ import { Button, ButtonGroup, Stack, Typography } from "@mui/material"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import { newOrChangedCredentials } from "../../../../Validation/validation.js"; -import { networkService } from "../../../../main.jsx"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; +import { newOrChangedCredentials } from "../../../Validation/validation.js"; +import { networkService } from "../../../main.jsx"; +import { createToast } from "../../../Utils/toastUtils.jsx"; import Select from "@/Components/v1/Inputs/Select/index.jsx"; import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; import AddTeamMember from "./AddTeamMember/index.jsx"; import DataTable from "@/Components/v1/Table/index.jsx"; -import { useGetInviteToken } from "../../../../Hooks/v1/inviteHooks.js"; +import { useGetInviteToken } from "../../../Hooks/inviteHooks.js"; import { useNavigate } from "react-router-dom"; -import { useIsSuperAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; +import { useIsSuperAdmin } from "@/Hooks/useIsAdmin.js"; import AddMemberMenu from "./AddMemberMenu/index.jsx"; /** * TeamPanel component manages the organization and team members, diff --git a/client/src/Pages/v1/Account/index.jsx b/client/src/Pages/Account/index.jsx similarity index 100% rename from client/src/Pages/v1/Account/index.jsx rename to client/src/Pages/Account/index.jsx diff --git a/client/src/Pages/v1/Auth/CheckEmail.jsx b/client/src/Pages/Auth/CheckEmail.jsx similarity index 94% rename from client/src/Pages/v1/Auth/CheckEmail.jsx rename to client/src/Pages/Auth/CheckEmail.jsx index c12a84804..6aee7a470 100644 --- a/client/src/Pages/v1/Auth/CheckEmail.jsx +++ b/client/src/Pages/Auth/CheckEmail.jsx @@ -3,12 +3,12 @@ import { useEffect, useState } from "react"; import { useTheme } from "@emotion/react"; import { useDispatch } from "react-redux"; import { useNavigate } from "react-router-dom"; -import { createToast } from "../../../Utils/toastUtils.jsx"; -import { forgotPassword } from "../../../Features/Auth/authSlice.js"; +import { createToast } from "../../Utils/toastUtils.jsx"; +import { forgotPassword } from "../../Features/Auth/authSlice.js"; import { Trans, useTranslation } from "react-i18next"; -import Background from "../../../assets/Images/background-grid.svg?react"; -import EmailIcon from "../../../assets/icons/email.svg?react"; -import Logo from "../../../assets/icons/checkmate-icon.svg?react"; +import Background from "@/assets/Images/background-grid.svg?react"; +import EmailIcon from "@/assets/icons/email.svg?react"; +import Logo from "@/assets/icons/checkmate-icon.svg?react"; import IconBox from "@/Components/v1/IconBox/index.jsx"; import "./index.css"; diff --git a/client/src/Pages/v1/Auth/ForgotPassword.jsx b/client/src/Pages/Auth/ForgotPassword.jsx similarity index 93% rename from client/src/Pages/v1/Auth/ForgotPassword.jsx rename to client/src/Pages/Auth/ForgotPassword.jsx index 8cbd85d8a..0a89b2987 100644 --- a/client/src/Pages/v1/Auth/ForgotPassword.jsx +++ b/client/src/Pages/Auth/ForgotPassword.jsx @@ -1,15 +1,15 @@ import { Box, Stack, Typography, Button } from "@mui/material"; import { useTheme } from "@emotion/react"; -import { createToast } from "../../../Utils/toastUtils.jsx"; +import { createToast } from "../../Utils/toastUtils.jsx"; import { useDispatch, useSelector } from "react-redux"; -import { forgotPassword } from "../../../Features/Auth/authSlice.js"; +import { forgotPassword } from "../../Features/Auth/authSlice.js"; import { useEffect, useState } from "react"; -import { newOrChangedCredentials } from "../../../Validation/validation.js"; +import { newOrChangedCredentials } from "../../Validation/validation.js"; import { useNavigate } from "react-router-dom"; import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import Logo from "../../../assets/icons/checkmate-icon.svg?react"; -import Key from "../../../assets/icons/key.svg?react"; -import Background from "../../../assets/Images/background-grid.svg?react"; +import Logo from "@/assets/icons/checkmate-icon.svg?react"; +import Key from "@/assets/icons/key.svg?react"; +import Background from "@/assets/Images/background-grid.svg?react"; import IconBox from "@/Components/v1/IconBox/index.jsx"; import { Trans, useTranslation } from "react-i18next"; import "./index.css"; diff --git a/client/src/Pages/v1/Auth/Login/hooks/useLoadingSubmit.jsx b/client/src/Pages/Auth/Login/hooks/useLoadingSubmit.jsx similarity index 100% rename from client/src/Pages/v1/Auth/Login/hooks/useLoadingSubmit.jsx rename to client/src/Pages/Auth/Login/hooks/useLoadingSubmit.jsx diff --git a/client/src/Pages/v1/Auth/Login/hooks/useLoginForm.jsx b/client/src/Pages/Auth/Login/hooks/useLoginForm.jsx similarity index 100% rename from client/src/Pages/v1/Auth/Login/hooks/useLoginForm.jsx rename to client/src/Pages/Auth/Login/hooks/useLoginForm.jsx diff --git a/client/src/Pages/v1/Auth/Login/hooks/useLoginSubmit.jsx b/client/src/Pages/Auth/Login/hooks/useLoginSubmit.jsx similarity index 87% rename from client/src/Pages/v1/Auth/Login/hooks/useLoginSubmit.jsx rename to client/src/Pages/Auth/Login/hooks/useLoginSubmit.jsx index 0eed9bd07..b009793d5 100644 --- a/client/src/Pages/v1/Auth/Login/hooks/useLoginSubmit.jsx +++ b/client/src/Pages/Auth/Login/hooks/useLoginSubmit.jsx @@ -1,7 +1,7 @@ import { useDispatch } from "react-redux"; -import { login } from "../../../../../Features/Auth/authSlice.js"; +import { login } from "../../../../Features/Auth/authSlice.js"; import { useNavigate } from "react-router-dom"; -import { createToast } from "../../../../../Utils/toastUtils.jsx"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; const useLoginSubmit = () => { diff --git a/client/src/Pages/v1/Auth/Login/hooks/useValidateLoginForm.jsx b/client/src/Pages/Auth/Login/hooks/useValidateLoginForm.jsx similarity index 90% rename from client/src/Pages/v1/Auth/Login/hooks/useValidateLoginForm.jsx rename to client/src/Pages/Auth/Login/hooks/useValidateLoginForm.jsx index ca9bef879..0872213ad 100644 --- a/client/src/Pages/v1/Auth/Login/hooks/useValidateLoginForm.jsx +++ b/client/src/Pages/Auth/Login/hooks/useValidateLoginForm.jsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { loginCredentials } from "../../../../../Validation/validation.js"; +import { loginCredentials } from "../../../../Validation/validation.js"; const useValidateLoginForm = () => { const [errors, setErrors] = useState({ diff --git a/client/src/Pages/v1/Auth/Login/index.jsx b/client/src/Pages/Auth/Login/index.jsx similarity index 100% rename from client/src/Pages/v1/Auth/Login/index.jsx rename to client/src/Pages/Auth/Login/index.jsx diff --git a/client/src/Pages/v1/Auth/NewPasswordConfirmed.jsx b/client/src/Pages/Auth/NewPasswordConfirmed.jsx similarity index 92% rename from client/src/Pages/v1/Auth/NewPasswordConfirmed.jsx rename to client/src/Pages/Auth/NewPasswordConfirmed.jsx index 629494098..53b352853 100644 --- a/client/src/Pages/v1/Auth/NewPasswordConfirmed.jsx +++ b/client/src/Pages/Auth/NewPasswordConfirmed.jsx @@ -2,10 +2,10 @@ import { Box, Button, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; import { useDispatch } from "react-redux"; -import { clearAuthState } from "../../../Features/Auth/authSlice.js"; -import Background from "../../../assets/Images/background-grid.svg?react"; -import ConfirmIcon from "../../../assets/icons/check-outlined.svg?react"; -import Logo from "../../../assets/icons/checkmate-icon.svg?react"; +import { clearAuthState } from "../../Features/Auth/authSlice.js"; +import Background from "@/assets/Images/background-grid.svg?react"; +import ConfirmIcon from "@/assets/icons/check-outlined.svg?react"; +import Logo from "@/assets/icons/checkmate-icon.svg?react"; import IconBox from "@/Components/v1/IconBox/index.jsx"; import { Trans, useTranslation } from "react-i18next"; import "./index.css"; diff --git a/client/src/Pages/v1/Auth/Register/index.jsx b/client/src/Pages/Auth/Register/index.jsx similarity index 97% rename from client/src/Pages/v1/Auth/Register/index.jsx rename to client/src/Pages/Auth/Register/index.jsx index d6515b85c..3345d8aa0 100644 --- a/client/src/Pages/v1/Auth/Register/index.jsx +++ b/client/src/Pages/Auth/Register/index.jsx @@ -13,11 +13,11 @@ import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { useDispatch, useSelector } from "react-redux"; import { useParams } from "react-router-dom"; -import { networkService } from "../../../../main.jsx"; -import { newOrChangedCredentials } from "../../../../Validation/validation.js"; -import { register } from "../../../../Features/Auth/authSlice.js"; +import { networkService } from "../../../main.jsx"; +import { newOrChangedCredentials } from "../../../Validation/validation.js"; +import { register } from "../../../Features/Auth/authSlice.js"; import AuthPageWrapper from "../components/AuthPageWrapper.jsx"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; +import { createToast } from "../../../Utils/toastUtils.jsx"; import PropTypes from "prop-types"; const getFeedbackStatus = (form, errors, field, criteria) => { diff --git a/client/src/Pages/v1/Auth/SetNewPassword.jsx b/client/src/Pages/Auth/SetNewPassword.jsx similarity index 95% rename from client/src/Pages/v1/Auth/SetNewPassword.jsx rename to client/src/Pages/Auth/SetNewPassword.jsx index 45f3351f3..6525c3f27 100644 --- a/client/src/Pages/v1/Auth/SetNewPassword.jsx +++ b/client/src/Pages/Auth/SetNewPassword.jsx @@ -4,16 +4,16 @@ import { useNavigate } from "react-router-dom"; import { useParams } from "react-router-dom"; import { useTheme } from "@emotion/react"; import { Box, Stack, Typography, Button } from "@mui/material"; -import { setNewPassword } from "../../../Features/Auth/authSlice.js"; -import { createToast } from "../../../Utils/toastUtils.jsx"; -import { newOrChangedCredentials } from "../../../Validation/validation.js"; +import { setNewPassword } from "../../Features/Auth/authSlice.js"; +import { createToast } from "../../Utils/toastUtils.jsx"; +import { newOrChangedCredentials } from "../../Validation/validation.js"; import Check from "@/Components/v1/Check/Check.jsx"; import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; import IconBox from "@/Components/v1/IconBox/index.jsx"; -import LockIcon from "../../../assets/icons/lock.svg?react"; -import Logo from "../../../assets/icons/checkmate-icon.svg?react"; -import Background from "../../../assets/Images/background-grid.svg?react"; +import LockIcon from "@/assets/icons/lock.svg?react"; +import Logo from "@/assets/icons/checkmate-icon.svg?react"; +import Background from "@/assets/Images/background-grid.svg?react"; import "./index.css"; import { useValidatePassword } from "./hooks/useValidatePassword.jsx"; import { Trans, useTranslation } from "react-i18next"; diff --git a/client/src/Pages/v1/Auth/components/AuthHeader.jsx b/client/src/Pages/Auth/components/AuthHeader.jsx similarity index 87% rename from client/src/Pages/v1/Auth/components/AuthHeader.jsx rename to client/src/Pages/Auth/components/AuthHeader.jsx index a34e83d6a..56b69ef14 100644 --- a/client/src/Pages/v1/Auth/components/AuthHeader.jsx +++ b/client/src/Pages/Auth/components/AuthHeader.jsx @@ -1,8 +1,8 @@ // Components import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; -import Logo from "../../../../assets/icons/checkmate-icon.svg?react"; -import LanguageSelector from "../../../../Components/LanguageSelector.jsx"; +import Logo from "@/assets/icons/checkmate-icon.svg?react"; +import LanguageSelector from "@/Components/LanguageSelector.jsx"; import ThemeSwitch from "@/Components/v1/ThemeSwitch/index.jsx"; // Utils diff --git a/client/src/Pages/v1/Auth/components/AuthPageWrapper.jsx b/client/src/Pages/Auth/components/AuthPageWrapper.jsx similarity index 93% rename from client/src/Pages/v1/Auth/components/AuthPageWrapper.jsx rename to client/src/Pages/Auth/components/AuthPageWrapper.jsx index 7e358e6ab..a5fbe311b 100644 --- a/client/src/Pages/v1/Auth/components/AuthPageWrapper.jsx +++ b/client/src/Pages/Auth/components/AuthPageWrapper.jsx @@ -1,10 +1,10 @@ -import Background from "../../../../assets/Images/background-grid.svg?react"; +import Background from "@/assets/Images/background-grid.svg?react"; import Stack from "@mui/material/Stack"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import AuthHeader from "./AuthHeader.jsx"; import { useTheme } from "@mui/material/styles"; -import Logo from "../../../../assets/icons/checkmate-icon.svg?react"; +import Logo from "@/assets/icons/checkmate-icon.svg?react"; import PropTypes from "prop-types"; const AuthPageWrapper = ({ children, heading, welcome }) => { diff --git a/client/src/Pages/v1/Auth/components/PasswordTooltip.jsx b/client/src/Pages/Auth/components/PasswordTooltip.jsx similarity index 100% rename from client/src/Pages/v1/Auth/components/PasswordTooltip.jsx rename to client/src/Pages/Auth/components/PasswordTooltip.jsx diff --git a/client/src/Pages/v1/Auth/hooks/usePasswordFeedback.jsx b/client/src/Pages/Auth/hooks/usePasswordFeedback.jsx similarity index 96% rename from client/src/Pages/v1/Auth/hooks/usePasswordFeedback.jsx rename to client/src/Pages/Auth/hooks/usePasswordFeedback.jsx index 3c199e25f..73e8fb738 100644 --- a/client/src/Pages/v1/Auth/hooks/usePasswordFeedback.jsx +++ b/client/src/Pages/Auth/hooks/usePasswordFeedback.jsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { newOrChangedCredentials } from "../../../../Validation/validation.js"; +import { newOrChangedCredentials } from "../../../Validation/validation.js"; const usePasswordFeedback = () => { const [feedback, setFeedback] = useState({}); diff --git a/client/src/Pages/v1/Auth/hooks/useValidatePassword.jsx b/client/src/Pages/Auth/hooks/useValidatePassword.jsx similarity index 96% rename from client/src/Pages/v1/Auth/hooks/useValidatePassword.jsx rename to client/src/Pages/Auth/hooks/useValidatePassword.jsx index 231b8eeb8..014804d35 100644 --- a/client/src/Pages/v1/Auth/hooks/useValidatePassword.jsx +++ b/client/src/Pages/Auth/hooks/useValidatePassword.jsx @@ -1,5 +1,5 @@ import { useMemo, useState } from "react"; -import { newOrChangedCredentials } from "../../../../Validation/validation.js"; +import { newOrChangedCredentials } from "../../../Validation/validation.js"; const getFeedbackStatus = (form, errors, field, criteria) => { const fieldErrors = errors[field]; diff --git a/client/src/Pages/v1/Auth/index.css b/client/src/Pages/Auth/index.css similarity index 100% rename from client/src/Pages/v1/Auth/index.css rename to client/src/Pages/Auth/index.css diff --git a/client/src/Pages/v1/Incidents/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx similarity index 95% rename from client/src/Pages/v1/Incidents/Components/IncidentTable/index.jsx rename to client/src/Pages/Incidents/Components/IncidentTable/index.jsx index df457c0e2..78d6d26be 100644 --- a/client/src/Pages/v1/Incidents/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx @@ -13,14 +13,12 @@ import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; //Utils -import { formatDateWithTz } from "../../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "@/Utils/timeUtils.js"; import { useSelector } from "react-redux"; import { useState } from "react"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; -import { useFetchChecksTeam } from "../../../../../Hooks/v1/checkHooks.js"; -import { useFetchChecksByMonitor } from "../../../../../Hooks/v1/checkHooks.js"; -import { useResolveIncident } from "../../../../../Hooks/v1/checkHooks.js"; +import { useFetchChecksTeam, useFetchChecksByMonitor, useResolveIncident } from "@/Hooks/checkHooks.js"; import { Button, Typography, useTheme } from "@mui/material"; import { lighten } from "@mui/material/styles"; diff --git a/client/src/Pages/v1/Incidents/Components/OptionsHeader/index.jsx b/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx similarity index 100% rename from client/src/Pages/v1/Incidents/Components/OptionsHeader/index.jsx rename to client/src/Pages/Incidents/Components/OptionsHeader/index.jsx diff --git a/client/src/Pages/v1/Incidents/Components/OptionsHeader/skeleton.jsx b/client/src/Pages/Incidents/Components/OptionsHeader/skeleton.jsx similarity index 100% rename from client/src/Pages/v1/Incidents/Components/OptionsHeader/skeleton.jsx rename to client/src/Pages/Incidents/Components/OptionsHeader/skeleton.jsx diff --git a/client/src/Pages/v1/Incidents/Components/StatusBoxes/StatusBox.jsx b/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx similarity index 87% rename from client/src/Pages/v1/Incidents/Components/StatusBoxes/StatusBox.jsx rename to client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx index cd7078e6c..c27c5948c 100644 --- a/client/src/Pages/v1/Incidents/Components/StatusBoxes/StatusBox.jsx +++ b/client/src/Pages/Incidents/Components/StatusBoxes/StatusBox.jsx @@ -1,11 +1,11 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { Box, Stack, Typography } from "@mui/material"; -import Background from "../../../../../assets/Images/background-grid.svg?react"; -import AlertIcon from "../../../../../assets/icons/alert-icon.svg?react"; -import CheckIcon from "../../../../../assets/icons/check-icon.svg?react"; -import CloseIcon from "../../../../../assets/icons/close-icon.svg?react"; -import WarningIcon from "../../../../../assets/icons/warning-icon.svg?react"; +import Background from "@/assets/Images/background-grid.svg?react"; +import AlertIcon from "@/assets/icons/alert-icon.svg?react"; +import CheckIcon from "@/assets/icons/check-icon.svg?react"; +import CloseIcon from "@/assets/icons/close-icon.svg?react"; +import WarningIcon from "@/assets/icons/warning-icon.svg?react"; const StatusBox = ({ title, value, status }) => { const theme = useTheme(); diff --git a/client/src/Pages/v1/Incidents/Components/StatusBoxes/index.jsx b/client/src/Pages/Incidents/Components/StatusBoxes/index.jsx similarity index 100% rename from client/src/Pages/v1/Incidents/Components/StatusBoxes/index.jsx rename to client/src/Pages/Incidents/Components/StatusBoxes/index.jsx diff --git a/client/src/Pages/v1/Incidents/Components/StatusBoxes/skeleton.jsx b/client/src/Pages/Incidents/Components/StatusBoxes/skeleton.jsx similarity index 100% rename from client/src/Pages/v1/Incidents/Components/StatusBoxes/skeleton.jsx rename to client/src/Pages/Incidents/Components/StatusBoxes/skeleton.jsx diff --git a/client/src/Pages/v1/Incidents/index.jsx b/client/src/Pages/Incidents/index.jsx similarity index 93% rename from client/src/Pages/v1/Incidents/index.jsx rename to client/src/Pages/Incidents/index.jsx index 91cab2a40..c8462f649 100644 --- a/client/src/Pages/v1/Incidents/index.jsx +++ b/client/src/Pages/Incidents/index.jsx @@ -9,9 +9,8 @@ import { Box, Button } from "@mui/material"; //Utils import { useTheme } from "@emotion/react"; -import { useFetchMonitorsByTeamId } from "../../../Hooks/v1/monitorHooks.js"; -import { useFetchChecksSummaryByTeamId } from "../../../Hooks/v1/checkHooks.js"; -import { useAcknowledgeChecks } from "../../../Hooks/v1/checkHooks.js"; +import { useFetchMonitorsByTeamId } from "@/Hooks/monitorHooks.js"; +import { useFetchChecksSummaryByTeamId, useAcknowledgeChecks } from "@/Hooks/checkHooks.js"; import { useState, useEffect } from "react"; import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/v1/Incidents2/Components/ActiveIncidentsPanel/index.jsx b/client/src/Pages/Incidents2/Components/ActiveIncidentsPanel/index.jsx similarity index 100% rename from client/src/Pages/v1/Incidents2/Components/ActiveIncidentsPanel/index.jsx rename to client/src/Pages/Incidents2/Components/ActiveIncidentsPanel/index.jsx diff --git a/client/src/Pages/v1/Incidents2/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx similarity index 98% rename from client/src/Pages/v1/Incidents2/Components/IncidentTable/index.jsx rename to client/src/Pages/Incidents2/Components/IncidentTable/index.jsx index 8cac4e07b..d613e15c7 100644 --- a/client/src/Pages/v1/Incidents2/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx @@ -8,7 +8,7 @@ import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; //Utils -import { formatDateWithTz } from "../../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; import { useSelector } from "react-redux"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/v1/Incidents2/Components/IncidentsSummaryPanel/index.jsx b/client/src/Pages/Incidents2/Components/IncidentsSummaryPanel/index.jsx similarity index 100% rename from client/src/Pages/v1/Incidents2/Components/IncidentsSummaryPanel/index.jsx rename to client/src/Pages/Incidents2/Components/IncidentsSummaryPanel/index.jsx diff --git a/client/src/Pages/v1/Incidents2/Components/IncidentsSummaryPanel/skeleton.jsx b/client/src/Pages/Incidents2/Components/IncidentsSummaryPanel/skeleton.jsx similarity index 100% rename from client/src/Pages/v1/Incidents2/Components/IncidentsSummaryPanel/skeleton.jsx rename to client/src/Pages/Incidents2/Components/IncidentsSummaryPanel/skeleton.jsx diff --git a/client/src/Pages/v1/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx b/client/src/Pages/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx similarity index 100% rename from client/src/Pages/v1/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx rename to client/src/Pages/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx diff --git a/client/src/Pages/v1/Incidents2/Components/LatestIncidentsPanel/index.jsx b/client/src/Pages/Incidents2/Components/LatestIncidentsPanel/index.jsx similarity index 100% rename from client/src/Pages/v1/Incidents2/Components/LatestIncidentsPanel/index.jsx rename to client/src/Pages/Incidents2/Components/LatestIncidentsPanel/index.jsx diff --git a/client/src/Pages/v1/Incidents2/Components/OptionsHeader/index.jsx b/client/src/Pages/Incidents2/Components/OptionsHeader/index.jsx similarity index 100% rename from client/src/Pages/v1/Incidents2/Components/OptionsHeader/index.jsx rename to client/src/Pages/Incidents2/Components/OptionsHeader/index.jsx diff --git a/client/src/Pages/v1/Incidents2/Components/StatisticsPanel/index.jsx b/client/src/Pages/Incidents2/Components/StatisticsPanel/index.jsx similarity index 100% rename from client/src/Pages/v1/Incidents2/Components/StatisticsPanel/index.jsx rename to client/src/Pages/Incidents2/Components/StatisticsPanel/index.jsx diff --git a/client/src/Pages/v1/Incidents2/Components/SummaryCard/index.jsx b/client/src/Pages/Incidents2/Components/SummaryCard/index.jsx similarity index 100% rename from client/src/Pages/v1/Incidents2/Components/SummaryCard/index.jsx rename to client/src/Pages/Incidents2/Components/SummaryCard/index.jsx diff --git a/client/src/Pages/v1/Incidents2/hooks/useFetchIncidents.js b/client/src/Pages/Incidents2/hooks/useFetchIncidents.js similarity index 97% rename from client/src/Pages/v1/Incidents2/hooks/useFetchIncidents.js rename to client/src/Pages/Incidents2/hooks/useFetchIncidents.js index a67f6b7ee..ab22c42e0 100644 --- a/client/src/Pages/v1/Incidents2/hooks/useFetchIncidents.js +++ b/client/src/Pages/Incidents2/hooks/useFetchIncidents.js @@ -1,6 +1,6 @@ import { useState, useCallback } from "react"; -import { networkService } from "../../../../main.jsx"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; +import { networkService } from "../../../main.jsx"; +import { createToast } from "../../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; /** * Hook to fetch and manage incidents diff --git a/client/src/Pages/v1/Incidents2/index.jsx b/client/src/Pages/Incidents2/index.jsx similarity index 97% rename from client/src/Pages/v1/Incidents2/index.jsx rename to client/src/Pages/Incidents2/index.jsx index 2983a38e8..eda68d976 100644 --- a/client/src/Pages/v1/Incidents2/index.jsx +++ b/client/src/Pages/Incidents2/index.jsx @@ -14,7 +14,7 @@ import { useTranslation } from "react-i18next"; // Hooks import useFetchIncidents from "./hooks/useFetchIncidents.js"; -import { useFetchMonitorsByTeamId } from "../../../Hooks/v1/monitorHooks.js"; +import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks.js"; const Incidents2 = () => { const { t } = useTranslation(); diff --git a/client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx b/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx similarity index 96% rename from client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx rename to client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx index 5fe1d8984..64f82cec2 100644 --- a/client/src/Pages/v1/Infrastructure/Create/Components/CustomAlertsSection.jsx +++ b/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx @@ -1,7 +1,7 @@ import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; import { Box, Stack, Typography } from "@mui/material"; import { CustomThreshold } from "./CustomThreshold/index.jsx"; -import { capitalizeFirstLetter } from "../../../../../Utils/stringUtils.js"; +import { capitalizeFirstLetter } from "../../../../Utils/stringUtils.js"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; import PropTypes from "prop-types"; diff --git a/client/src/Pages/v1/Infrastructure/Create/Components/CustomThreshold/index.jsx b/client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx similarity index 100% rename from client/src/Pages/v1/Infrastructure/Create/Components/CustomThreshold/index.jsx rename to client/src/Pages/Infrastructure/Create/Components/CustomThreshold/index.jsx diff --git a/client/src/Pages/v1/Infrastructure/Create/Components/DiskSelection.jsx b/client/src/Pages/Infrastructure/Create/Components/DiskSelection.jsx similarity index 100% rename from client/src/Pages/v1/Infrastructure/Create/Components/DiskSelection.jsx rename to client/src/Pages/Infrastructure/Create/Components/DiskSelection.jsx diff --git a/client/src/Pages/v1/Infrastructure/Create/Components/MonitorActionButtons.jsx b/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx similarity index 100% rename from client/src/Pages/v1/Infrastructure/Create/Components/MonitorActionButtons.jsx rename to client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx diff --git a/client/src/Pages/v1/Infrastructure/Create/Components/MonitorStatusHeader.jsx b/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx similarity index 95% rename from client/src/Pages/v1/Infrastructure/Create/Components/MonitorStatusHeader.jsx rename to client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx index 570f38a7e..8fa6f4ff1 100644 --- a/client/src/Pages/v1/Infrastructure/Create/Components/MonitorStatusHeader.jsx +++ b/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx @@ -1,5 +1,5 @@ import { Box, Stack, Tooltip, Typography } from "@mui/material"; -import { useMonitorUtils } from "../../../../../Hooks/v1/useMonitorUtils.js"; +import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils.js"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; import PulseDot from "@/Components/v1/Animated/PulseDot.jsx"; diff --git a/client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx similarity index 100% rename from client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx rename to client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx diff --git a/client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx similarity index 96% rename from client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx rename to client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx index 15a5f3c5d..c5c7aacf2 100644 --- a/client/src/Pages/v1/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx +++ b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx @@ -1,7 +1,4 @@ -import { - useCreateMonitor, - useUpdateMonitor, -} from "../../../../../Hooks/v1/monitorHooks.js"; +import { useCreateMonitor, useUpdateMonitor } from "../../../../Hooks/monitorHooks.js"; const useInfrastructureSubmit = () => { const [createMonitor, isCreating] = useCreateMonitor(); const [updateMonitor, isUpdating] = useUpdateMonitor(); diff --git a/client/src/Pages/v1/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx b/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx similarity index 85% rename from client/src/Pages/v1/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx rename to client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx index 73097d0d8..f7665e65f 100644 --- a/client/src/Pages/v1/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx +++ b/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { infrastructureMonitorValidation } from "../../../../../Validation/validation.js"; -import { createToast } from "../../../../../Utils/toastUtils.jsx"; +import { infrastructureMonitorValidation } from "../../../../Validation/validation.js"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; const useValidateInfrastructureForm = () => { const [errors, setErrors] = useState({}); diff --git a/client/src/Pages/v1/Infrastructure/Create/index.jsx b/client/src/Pages/Infrastructure/Create/index.jsx similarity index 97% rename from client/src/Pages/v1/Infrastructure/Create/index.jsx rename to client/src/Pages/Infrastructure/Create/index.jsx index c74d49816..b27c11cd4 100644 --- a/client/src/Pages/v1/Infrastructure/Create/index.jsx +++ b/client/src/Pages/Infrastructure/Create/index.jsx @@ -13,8 +13,8 @@ import CustomAlertsSection from "./Components/CustomAlertsSection.jsx"; import DiskSelection from "./Components/DiskSelection.jsx"; // Utils import NotificationsConfig from "@/Components/v1/NotificationConfig/index.jsx"; -import { useGetNotificationsByTeamId } from "../../../../Hooks/v1/useNotifications.js"; -import { networkService } from "../../../../Utils/NetworkService"; +import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications.js"; +import { networkService } from "../../../Utils/NetworkService.js"; import { useParams } from "react-router-dom"; import { useState, useEffect } from "react"; import { useTheme } from "@emotion/react"; @@ -24,7 +24,7 @@ import { useFetchGlobalSettings, useFetchHardwareMonitorById, usePauseMonitor, -} from "../../../../Hooks/v1/monitorHooks.js"; +} from "../../../Hooks/monitorHooks.js"; import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm.jsx"; import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm.jsx"; import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit.jsx"; @@ -150,7 +150,8 @@ const CreateInfrastructureMonitor = () => { const handleRemove = async (event) => { event.preventDefault(); - await deleteMonitor({ monitor, redirect: "/infrastructure" }); + const TEMP_MONITOR = { id: monitor._id }; + await deleteMonitor({ monitor: TEMP_MONITOR, redirect: "/infrastructure" }); }; const isBusy = diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx b/client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx similarity index 100% rename from client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx rename to client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/InfraAreaChart.jsx diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/index.jsx b/client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/index.jsx similarity index 100% rename from client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/index.jsx rename to client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/index.jsx diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx b/client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx similarity index 100% rename from client/src/Pages/v1/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx rename to client/src/Pages/Infrastructure/Details/Components/AreaChartBoxes/skeleton.jsx diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/BaseContainer/index.jsx b/client/src/Pages/Infrastructure/Details/Components/BaseContainer/index.jsx similarity index 100% rename from client/src/Pages/v1/Infrastructure/Details/Components/BaseContainer/index.jsx rename to client/src/Pages/Infrastructure/Details/Components/BaseContainer/index.jsx diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx b/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx similarity index 100% rename from client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx rename to client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/Gauge.jsx diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx b/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx similarity index 100% rename from client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/index.jsx rename to client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/index.jsx diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx b/client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx similarity index 100% rename from client/src/Pages/v1/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx rename to client/src/Pages/Infrastructure/Details/Components/GaugeBoxes/skeleton.jsx diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx similarity index 100% rename from client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx rename to client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkCharts.jsx diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx similarity index 100% rename from client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx rename to client/src/Pages/Infrastructure/Details/Components/NetworkStats/NetworkStatBoxes.jsx diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/index.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx similarity index 100% rename from client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/index.jsx rename to client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/skeleton.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx similarity index 100% rename from client/src/Pages/v1/Infrastructure/Details/Components/NetworkStats/skeleton.jsx rename to client/src/Pages/Infrastructure/Details/Components/NetworkStats/skeleton.jsx diff --git a/client/src/Pages/v1/Infrastructure/Details/Components/StatusBoxes/index.jsx b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx similarity index 97% rename from client/src/Pages/v1/Infrastructure/Details/Components/StatusBoxes/index.jsx rename to client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx index 7f09e82d5..ef91709cf 100644 --- a/client/src/Pages/v1/Infrastructure/Details/Components/StatusBoxes/index.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/StatusBoxes/index.jsx @@ -4,7 +4,7 @@ import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; import StatBox from "@/Components/v1/StatBox/index.jsx"; //Utils -import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js"; +import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js"; import { useHardwareUtils } from "../../Hooks/useHardwareUtils.jsx"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/v1/Infrastructure/Details/Hooks/useHardwareUtils.jsx b/client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx similarity index 100% rename from client/src/Pages/v1/Infrastructure/Details/Hooks/useHardwareUtils.jsx rename to client/src/Pages/Infrastructure/Details/Hooks/useHardwareUtils.jsx diff --git a/client/src/Pages/v1/Infrastructure/Details/index.jsx b/client/src/Pages/Infrastructure/Details/index.jsx similarity index 96% rename from client/src/Pages/v1/Infrastructure/Details/index.jsx rename to client/src/Pages/Infrastructure/Details/index.jsx index f9c51c27d..62a5c2642 100644 --- a/client/src/Pages/v1/Infrastructure/Details/index.jsx +++ b/client/src/Pages/Infrastructure/Details/index.jsx @@ -13,8 +13,8 @@ import TabContext from "@mui/lab/TabContext"; // Utils import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; -import { useFetchHardwareMonitorById } from "../../../../Hooks/v1/monitorHooks.js"; +import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; +import { useFetchHardwareMonitorById } from "../../../Hooks/monitorHooks.js"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { useParams } from "react-router-dom"; diff --git a/client/src/Pages/v1/Infrastructure/Monitors/Components/Filters/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx similarity index 100% rename from client/src/Pages/v1/Infrastructure/Monitors/Components/Filters/index.jsx rename to client/src/Pages/Infrastructure/Monitors/Components/Filters/index.jsx diff --git a/client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTable/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx similarity index 95% rename from client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTable/index.jsx rename to client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx index cc2cd5f98..510115d8a 100644 --- a/client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTable/index.jsx +++ b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx @@ -7,12 +7,12 @@ import { Stack } from "@mui/material"; import { InfrastructureMenu } from "../MonitorsTableMenu/index.jsx"; import LoadingSpinner from "../../../../Uptime/Monitors/Components/LoadingSpinner/index.jsx"; // Assets -import CPUChipIcon from "../../../../../../assets/icons/cpu-chip.svg?react"; +import CPUChipIcon from "@/assets/icons/cpu-chip.svg?react"; import CustomGauge from "@/Components/v1/Charts/CustomGauge/index.jsx"; // Utils import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js"; +import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js"; import { useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx similarity index 94% rename from client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx rename to client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx index d3af34d00..39aa84e90 100644 --- a/client/src/Pages/v1/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx +++ b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTableMenu/index.jsx @@ -3,13 +3,13 @@ import { useRef, useState } from "react"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; -import { createToast } from "../../../../../../Utils/toastUtils.jsx"; +import { createToast } from "@/Utils/toastUtils.jsx"; import { IconButton, Menu, MenuItem } from "@mui/material"; -import Settings from "../../../../../../assets/icons/settings-bold.svg?react"; +import Settings from "@/assets/icons/settings-bold.svg?react"; import PropTypes from "prop-types"; import Dialog from "@/Components/v1/Dialog/index.jsx"; -import { networkService } from "../../../../../../Utils/NetworkService.js"; -import { usePauseMonitor } from "../../../../../../Hooks/v1/monitorHooks.js"; +import { networkService } from "@/Utils/NetworkService.js"; +import { usePauseMonitor } from "@/Hooks/monitorHooks.js"; import { useTranslation } from "react-i18next"; /** diff --git a/client/src/Pages/v1/Infrastructure/Monitors/index.jsx b/client/src/Pages/Infrastructure/Monitors/index.jsx similarity index 94% rename from client/src/Pages/v1/Infrastructure/Monitors/index.jsx rename to client/src/Pages/Infrastructure/Monitors/index.jsx index 6c3a657ab..90817f116 100644 --- a/client/src/Pages/v1/Infrastructure/Monitors/index.jsx +++ b/client/src/Pages/Infrastructure/Monitors/index.jsx @@ -11,11 +11,11 @@ import SearchComponent from "../../Uptime/Monitors/Components/SearchComponent/in // Utils import { useTheme } from "@emotion/react"; import { useEffect, useState } from "react"; -import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; +import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; import { useTranslation } from "react-i18next"; -import { useFetchMonitorsByTeamId } from "../../../../Hooks/v1/monitorHooks.js"; +import { useFetchMonitorsByTeamId } from "../../../Hooks/monitorHooks.js"; import { useDispatch, useSelector } from "react-redux"; -import { setRowsPerPage } from "../../../../Features/UI/uiSlice.js"; +import { setRowsPerPage } from "../../../Features/UI/uiSlice.js"; // Constants const TYPES = ["hardware"]; const BREADCRUMBS = [{ name: `infrastructure`, path: "/infrastructure" }]; diff --git a/client/src/Pages/v1/Integrations/index.css b/client/src/Pages/Integrations/index.css similarity index 100% rename from client/src/Pages/v1/Integrations/index.css rename to client/src/Pages/Integrations/index.css diff --git a/client/src/Pages/v1/Integrations/index.jsx b/client/src/Pages/Integrations/index.jsx similarity index 100% rename from client/src/Pages/v1/Integrations/index.jsx rename to client/src/Pages/Integrations/index.jsx diff --git a/client/src/Pages/v1/Logs/Diagnostics/components/gauges/index.jsx b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx similarity index 100% rename from client/src/Pages/v1/Logs/Diagnostics/components/gauges/index.jsx rename to client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx diff --git a/client/src/Pages/v1/Logs/Diagnostics/components/stats/index.jsx b/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx similarity index 96% rename from client/src/Pages/v1/Logs/Diagnostics/components/stats/index.jsx rename to client/src/Pages/Logs/Diagnostics/components/stats/index.jsx index b99f34506..b169a6a85 100644 --- a/client/src/Pages/v1/Logs/Diagnostics/components/stats/index.jsx +++ b/client/src/Pages/Logs/Diagnostics/components/stats/index.jsx @@ -7,7 +7,7 @@ import CircularProgress from "@mui/material/CircularProgress"; import { useTheme } from "@emotion/react"; import PropTypes from "prop-types"; -import { getHumanReadableDuration } from "../../../../../../Utils/timeUtils.js"; +import { getHumanReadableDuration } from "../../../../../Utils/timeUtils.js"; import { formatBytes } from "../../utils/utils.js"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/v1/Logs/Diagnostics/index.jsx b/client/src/Pages/Logs/Diagnostics/index.jsx similarity index 88% rename from client/src/Pages/v1/Logs/Diagnostics/index.jsx rename to client/src/Pages/Logs/Diagnostics/index.jsx index 54feee516..07b504c77 100644 --- a/client/src/Pages/v1/Logs/Diagnostics/index.jsx +++ b/client/src/Pages/Logs/Diagnostics/index.jsx @@ -6,8 +6,8 @@ import StatBox from "@/Components/v1/StatBox/index.jsx"; import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import { useFetchDiagnostics } from "../../../../Hooks/v1/logHooks.js"; -import { getHumanReadableDuration } from "../../../../Utils/timeUtils.js"; +import { useFetchDiagnostics } from "../../../Hooks/logHooks.js"; +import { getHumanReadableDuration } from "../../../Utils/timeUtils.js"; import { formatBytes, getPercentage } from "./utils/utils.js"; const Diagnostics = () => { @@ -26,12 +26,12 @@ const Diagnostics = () => { status="up" heading={t("status")} subHeading={ - error + error ? t("logsPage.logLevelSelect.values.error") - : isLoading + : isLoading ? t("commonSaving") - : diagnostics - ? t("diagnosticsPage.diagnosticDescription") + : diagnostics + ? t("diagnosticsPage.diagnosticDescription") : t("general.noOptionsFound", { unit: "data" }) } /> diff --git a/client/src/Pages/v1/Logs/Diagnostics/utils/utils.js b/client/src/Pages/Logs/Diagnostics/utils/utils.js similarity index 100% rename from client/src/Pages/v1/Logs/Diagnostics/utils/utils.js rename to client/src/Pages/Logs/Diagnostics/utils/utils.js diff --git a/client/src/Pages/v1/Logs/Logs/index.jsx b/client/src/Pages/Logs/Logs/index.jsx similarity index 97% rename from client/src/Pages/v1/Logs/Logs/index.jsx rename to client/src/Pages/Logs/Logs/index.jsx index 64c4cb97a..c44a83ed9 100644 --- a/client/src/Pages/v1/Logs/Logs/index.jsx +++ b/client/src/Pages/Logs/Logs/index.jsx @@ -4,7 +4,7 @@ import Select from "@/Components/v1/Inputs/Select/index.jsx"; import Typography from "@mui/material/Typography"; import Divider from "@mui/material/Divider"; -import { useFetchLogs } from "../../../../Hooks/v1/logHooks.js"; +import { useFetchLogs } from "../../../Hooks/logHooks.js"; import { useTheme } from "@emotion/react"; import { useState } from "react"; import { useTranslation } from "react-i18next"; @@ -51,7 +51,6 @@ const Logs = () => { ]; return ( - { const MS_LOOKUP = { diff --git a/client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx similarity index 95% rename from client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx rename to client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx index d21906e41..c3be0091a 100644 --- a/client/src/Pages/v1/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx +++ b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx @@ -1,4 +1,4 @@ -import { networkService } from "../../../../../main.jsx"; +import { networkService } from "../../../../main.jsx"; import dayjs from "dayjs"; import { MS_PER_SECOND, @@ -6,7 +6,7 @@ import { MS_PER_HOUR, MS_PER_DAY, MS_PER_WEEK, -} from "../../../../../Utils/timeUtils.js"; +} from "../../../../Utils/timeUtils.js"; const useMaintenanceData = () => { const REVERSE_REPEAT_LOOKUP = { 0: "none", diff --git a/client/src/Pages/v1/Maintenance/CreateMaintenance/index.css b/client/src/Pages/Maintenance/CreateMaintenance/index.css similarity index 100% rename from client/src/Pages/v1/Maintenance/CreateMaintenance/index.css rename to client/src/Pages/Maintenance/CreateMaintenance/index.css diff --git a/client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx b/client/src/Pages/Maintenance/CreateMaintenance/index.jsx similarity index 97% rename from client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx rename to client/src/Pages/Maintenance/CreateMaintenance/index.jsx index d0e7b03cd..384805f3a 100644 --- a/client/src/Pages/v1/Maintenance/CreateMaintenance/index.jsx +++ b/client/src/Pages/Maintenance/CreateMaintenance/index.jsx @@ -7,8 +7,8 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; import { LocalizationProvider } from "@mui/x-date-pickers"; import { DatePicker } from "@mui/x-date-pickers/DatePicker"; import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker"; -import { maintenanceWindowValidation } from "../../../../Validation/validation.js"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; +import { maintenanceWindowValidation } from "@/Validation/validation.js"; +import { createToast } from "@/Utils/toastUtils.jsx"; import MonitorList from "./Components/MonitorList/index.jsx"; import ConfigSelect from "./Components/ConfigSelect/index.jsx"; import useMaintenanceData from "./hooks/useMaintenanceData.jsx"; @@ -16,13 +16,13 @@ import useMaintenanceActions from "./hooks/useMaintenanceActions.jsx"; import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import CalendarIcon from "../../../../assets/icons/calendar.svg?react"; +import CalendarIcon from "@/assets/icons/calendar.svg?react"; import "./index.css"; import Search from "@/Components/v1/Inputs/Search/index.jsx"; -import { logger } from "../../../../Utils/Logger.js"; +import { logger } from "@/Utils/Logger.js"; import { useNavigate, useParams } from "react-router-dom"; -import { buildErrors, hasValidationErrors } from "../../../../Validation/error.js"; +import { buildErrors, hasValidationErrors } from "@/Validation/error.js"; import { useTranslation } from "react-i18next"; import dayjs from "dayjs"; diff --git a/client/src/Pages/v1/Maintenance/MaintenanceTable/ActionsMenu/index.jsx b/client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx similarity index 93% rename from client/src/Pages/v1/Maintenance/MaintenanceTable/ActionsMenu/index.jsx rename to client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx index b2654da49..38a3d8e64 100644 --- a/client/src/Pages/v1/Maintenance/MaintenanceTable/ActionsMenu/index.jsx +++ b/client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx @@ -3,11 +3,11 @@ import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; import { useSelector } from "react-redux"; import { IconButton, Menu, MenuItem } from "@mui/material"; -import { logger } from "../../../../../Utils/Logger.js"; -import Settings from "../../../../../assets/icons/settings-bold.svg?react"; +import { logger } from "@/Utils/Logger.js"; +import Settings from "@/assets/icons/settings-bold.svg?react"; import PropTypes from "prop-types"; -import { networkService } from "../../../../../main.jsx"; -import { createToast } from "../../../../../Utils/toastUtils.jsx"; +import { networkService } from "../../../../main.jsx"; +import { createToast } from "@/Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; import Dialog from "@/Components/v1/Dialog/index.jsx"; diff --git a/client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx b/client/src/Pages/Maintenance/MaintenanceTable/index.jsx similarity index 97% rename from client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx rename to client/src/Pages/Maintenance/MaintenanceTable/index.jsx index f1d44f944..faeb2072c 100644 --- a/client/src/Pages/v1/Maintenance/MaintenanceTable/index.jsx +++ b/client/src/Pages/Maintenance/MaintenanceTable/index.jsx @@ -7,9 +7,9 @@ import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; import ActionsMenu from "./ActionsMenu/index.jsx"; import { memo } from "react"; import { useDispatch, useSelector } from "react-redux"; -import { formatDurationRounded } from "../../../../Utils/timeUtils.js"; +import { formatDurationRounded } from "../../../Utils/timeUtils.js"; import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import { setRowsPerPage } from "../../../../Features/UI/uiSlice.js"; +import { setRowsPerPage } from "../../../Features/UI/uiSlice.js"; import { useTranslation } from "react-i18next"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; diff --git a/client/src/Pages/v1/Maintenance/index.css b/client/src/Pages/Maintenance/index.css similarity index 100% rename from client/src/Pages/v1/Maintenance/index.css rename to client/src/Pages/Maintenance/index.css diff --git a/client/src/Pages/v1/Maintenance/index.jsx b/client/src/Pages/Maintenance/index.jsx similarity index 98% rename from client/src/Pages/v1/Maintenance/index.jsx rename to client/src/Pages/Maintenance/index.jsx index cba0e9f18..b49cd9f6e 100644 --- a/client/src/Pages/v1/Maintenance/index.jsx +++ b/client/src/Pages/Maintenance/index.jsx @@ -4,7 +4,7 @@ import { useState, useEffect } from "react"; import "./index.css"; import MaintenanceTable from "./MaintenanceTable/index.jsx"; import { useSelector } from "react-redux"; -import { networkService } from "../../../main.jsx"; +import { networkService } from "../../main.jsx"; import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/v1/NotFound/index.jsx b/client/src/Pages/NotFound/index.jsx similarity index 96% rename from client/src/Pages/v1/NotFound/index.jsx rename to client/src/Pages/NotFound/index.jsx index 48825c9f6..27e3173b0 100644 --- a/client/src/Pages/v1/NotFound/index.jsx +++ b/client/src/Pages/NotFound/index.jsx @@ -1,5 +1,5 @@ import PropTypes from "prop-types"; -import NotFoundSvg from "../../../assets/Images/sushi_404.svg"; +import NotFoundSvg from "@/assets/Images/sushi_404.svg"; import { Button, Stack, Typography } from "@mui/material"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/v1/Notifications/components/ActionMenu.jsx b/client/src/Pages/Notifications/components/ActionMenu.jsx similarity index 100% rename from client/src/Pages/v1/Notifications/components/ActionMenu.jsx rename to client/src/Pages/Notifications/components/ActionMenu.jsx diff --git a/client/src/Pages/v1/Notifications/create/index.jsx b/client/src/Pages/Notifications/create/index.jsx similarity index 97% rename from client/src/Pages/v1/Notifications/create/index.jsx rename to client/src/Pages/Notifications/create/index.jsx index b1c50493c..11bf2a95d 100644 --- a/client/src/Pages/v1/Notifications/create/index.jsx +++ b/client/src/Pages/Notifications/create/index.jsx @@ -18,9 +18,9 @@ import { useEditNotification, useTestNotification, useDeleteNotification, -} from "../../../../Hooks/v1/useNotifications.js"; -import { notificationValidation } from "../../../../Validation/validation.js"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; +} from "../../../Hooks/useNotifications.js"; +import { notificationValidation } from "../../../Validation/validation.js"; +import { createToast } from "../../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; import { useParams, useNavigate } from "react-router-dom"; import { diff --git a/client/src/Pages/v1/Notifications/index.jsx b/client/src/Pages/Notifications/index.jsx similarity index 98% rename from client/src/Pages/v1/Notifications/index.jsx rename to client/src/Pages/Notifications/index.jsx index 78ec6e9bb..8ebdb22ec 100644 --- a/client/src/Pages/v1/Notifications/index.jsx +++ b/client/src/Pages/Notifications/index.jsx @@ -14,7 +14,7 @@ import { useNavigate } from "react-router-dom"; import { useGetNotificationsByTeamId, useDeleteNotification, -} from "../../../Hooks/v1/useNotifications.js"; +} from "../../Hooks/useNotifications.js"; import { useTranslation } from "react-i18next"; const Notifications = () => { diff --git a/client/src/Pages/v1/Notifications/utils.js b/client/src/Pages/Notifications/utils.js similarity index 100% rename from client/src/Pages/v1/Notifications/utils.js rename to client/src/Pages/Notifications/utils.js diff --git a/client/src/Pages/v1/PageSpeed/Create/index.css b/client/src/Pages/PageSpeed/Create/index.css similarity index 100% rename from client/src/Pages/v1/PageSpeed/Create/index.css rename to client/src/Pages/PageSpeed/Create/index.css diff --git a/client/src/Pages/v1/PageSpeed/Create/index.jsx b/client/src/Pages/PageSpeed/Create/index.jsx similarity index 96% rename from client/src/Pages/v1/PageSpeed/Create/index.jsx rename to client/src/Pages/PageSpeed/Create/index.jsx index cb856bd37..2cce26159 100644 --- a/client/src/Pages/v1/PageSpeed/Create/index.jsx +++ b/client/src/Pages/PageSpeed/Create/index.jsx @@ -16,22 +16,22 @@ import Radio from "@/Components/v1/Inputs/Radio/index.jsx"; // Utils import { useState, useEffect } from "react"; import { useSelector } from "react-redux"; -import { monitorValidation } from "../../../../Validation/validation.js"; -import { parseDomainName } from "../../../../Utils/monitorUtils.js"; +import { monitorValidation } from "../../../Validation/validation.js"; +import { parseDomainName } from "../../../Utils/monitorUtils.js"; import { useTranslation } from "react-i18next"; -import { useGetNotificationsByTeamId } from "../../../../Hooks/v1/useNotifications.js"; +import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications.js"; import { useTheme } from "@emotion/react"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; +import { createToast } from "../../../Utils/toastUtils.jsx"; import { useParams } from "react-router"; -import { useMonitorUtils } from "../../../../Hooks/v1/useMonitorUtils.js"; +import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js"; import { useCreateMonitor, useFetchMonitorById, useDeleteMonitor, useUpdateMonitor, usePauseMonitor, -} from "../../../../Hooks/v1/monitorHooks.js"; +} from "../../../Hooks/monitorHooks.js"; const PageSpeedSetup = () => { const { monitorId } = useParams(); @@ -178,7 +178,8 @@ const PageSpeedSetup = () => { const handleRemove = async (event) => { event.preventDefault(); - await deleteMonitor({ monitor, redirect: "/pagespeed" }); + const TEMP_MONITOR = { id: monitor._id }; + await deleteMonitor({ monitor: TEMP_MONITOR, redirect: "/pagespeed" }); }; const isBusy = isLoading || isCreating || isDeleting || isUpdating || isPausing; diff --git a/client/src/Pages/v1/PageSpeed/Create/skeleton.jsx b/client/src/Pages/PageSpeed/Create/skeleton.jsx similarity index 100% rename from client/src/Pages/v1/PageSpeed/Create/skeleton.jsx rename to client/src/Pages/PageSpeed/Create/skeleton.jsx diff --git a/client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChart.jsx b/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx similarity index 99% rename from client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChart.jsx rename to client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx index 5c79cc06c..06b92c933 100644 --- a/client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChart.jsx +++ b/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChart.jsx @@ -11,7 +11,7 @@ import { import { useTheme } from "@emotion/react"; import { useMemo, useState } from "react"; import { Box, Stack, Typography } from "@mui/material"; -import { formatDateWithTz } from "../../../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../../Utils/timeUtils.js"; import { useSelector } from "react-redux"; const config = { diff --git a/client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx b/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx similarity index 94% rename from client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx rename to client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx index c0da1600f..113e4d618 100644 --- a/client/src/Pages/v1/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx +++ b/client/src/Pages/PageSpeed/Details/Components/Charts/AreaChartLegend.jsx @@ -1,6 +1,6 @@ import { Box, Typography, Divider } from "@mui/material"; import Checkbox from "@/Components/v1/Inputs/Checkbox/index.jsx"; -import MetricsIcon from "../../../../../../assets/icons/ruler-icon.svg?react"; +import MetricsIcon from "@/assets/icons/ruler-icon.svg?react"; import LegendBox from "@/Components/v1/Charts/LegendBox/index.jsx"; import { useTranslation } from "react-i18next"; import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/v1/PageSpeed/Details/Components/Charts/PieChart.jsx b/client/src/Pages/PageSpeed/Details/Components/Charts/PieChart.jsx similarity index 100% rename from client/src/Pages/v1/PageSpeed/Details/Components/Charts/PieChart.jsx rename to client/src/Pages/PageSpeed/Details/Components/Charts/PieChart.jsx diff --git a/client/src/Pages/v1/PageSpeed/Details/Components/Charts/PieChartLegend.jsx b/client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx similarity index 96% rename from client/src/Pages/v1/PageSpeed/Details/Components/Charts/PieChartLegend.jsx rename to client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx index 1297bd699..43e2371b4 100644 --- a/client/src/Pages/v1/PageSpeed/Details/Components/Charts/PieChartLegend.jsx +++ b/client/src/Pages/PageSpeed/Details/Components/Charts/PieChartLegend.jsx @@ -1,7 +1,7 @@ import { Stack, Box, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import LegendBox from "@/Components/v1/Charts/LegendBox/index.jsx"; -import SpeedometerIcon from "../../../../../../assets/icons/speedometer-icon.svg?react"; +import SpeedometerIcon from "@/assets/icons/speedometer-icon.svg?react"; import PropTypes from "prop-types"; const PieChartLegend = ({ audits }) => { diff --git a/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx b/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx similarity index 93% rename from client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx rename to client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx index 1281a396e..aa88d57e5 100644 --- a/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx +++ b/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/index.jsx @@ -2,7 +2,7 @@ import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; import AreaChart from "../Charts/AreaChart.jsx"; import AreaChartLegend from "../Charts/AreaChartLegend.jsx"; import SkeletonLayout from "./skeleton.jsx"; -import ScoreIcon from "../../../../../../assets/icons/monitor-graph-line.svg?react"; +import ScoreIcon from "@/assets/icons/monitor-graph-line.svg?react"; import { Stack } from "@mui/material"; import PropTypes from "prop-types"; diff --git a/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx b/client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx similarity index 100% rename from client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx rename to client/src/Pages/PageSpeed/Details/Components/PageSpeedAreaChart/skeleton.jsx diff --git a/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx b/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx similarity index 91% rename from client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx rename to client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx index ff265317c..493d1ece6 100644 --- a/client/src/Pages/v1/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx +++ b/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx @@ -1,7 +1,7 @@ import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; import StatBox from "@/Components/v1/StatBox/index.jsx"; import { Typography } from "@mui/material"; -import { getHumanReadableDuration } from "../../../../../../Utils/timeUtils.js"; +import { getHumanReadableDuration } from "../../../../../Utils/timeUtils.js"; import { useTranslation } from "react-i18next"; const PageSpeedStatusBoxes = ({ shouldRender, monitor }) => { diff --git a/client/src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/index.jsx b/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx similarity index 93% rename from client/src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/index.jsx rename to client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx index 27f9e885b..c2eecaa6a 100644 --- a/client/src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/index.jsx +++ b/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/index.jsx @@ -1,5 +1,5 @@ import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; -import PerformanceIcon from "../../../../../../assets/icons/performance-report.svg?react"; +import PerformanceIcon from "@/assets/icons/performance-report.svg?react"; import PieChart from "../Charts/PieChart.jsx"; import { Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx b/client/src/Pages/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx similarity index 100% rename from client/src/Pages/v1/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx rename to client/src/Pages/PageSpeed/Details/Components/PerformanceReport/skeleton.jsx diff --git a/client/src/Pages/v1/PageSpeed/Details/index.jsx b/client/src/Pages/PageSpeed/Details/index.jsx similarity index 95% rename from client/src/Pages/v1/PageSpeed/Details/index.jsx rename to client/src/Pages/PageSpeed/Details/index.jsx index 836f0ca37..8a87133df 100644 --- a/client/src/Pages/v1/PageSpeed/Details/index.jsx +++ b/client/src/Pages/PageSpeed/Details/index.jsx @@ -9,9 +9,9 @@ import PerformanceReport from "./Components/PerformanceReport/index.jsx"; import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; // Utils import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; +import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; import { useParams } from "react-router-dom"; -import { useFetchStatsByMonitorId } from "../../../../Hooks/v1/monitorHooks.js"; +import { useFetchStatsByMonitorId } from "../../../Hooks/monitorHooks.js"; import { useState } from "react"; import { useTranslation } from "react-i18next"; // Constants diff --git a/client/src/Pages/v1/PageSpeed/Monitors/Components/Card/index.jsx b/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx similarity index 96% rename from client/src/Pages/v1/PageSpeed/Monitors/Components/Card/index.jsx rename to client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx index 4e551526b..8a6444d1b 100644 --- a/client/src/Pages/v1/PageSpeed/Monitors/Components/Card/index.jsx +++ b/client/src/Pages/PageSpeed/Monitors/Components/Card/index.jsx @@ -1,17 +1,14 @@ import PropTypes from "prop-types"; // import PageSpeedIcon from "../../../../assets/icons/page-speed.svg?react"; -import PageSpeedIcon from "../../../../../../assets/icons/page-speed.svg?react"; +import PageSpeedIcon from "@/assets/icons/page-speed.svg?react"; import { StatusLabel } from "@/Components/v1/Label/index.jsx"; import { Box, Grid, Stack, Typography } from "@mui/material"; import { useNavigate } from "react-router-dom"; import { useTheme } from "@emotion/react"; import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts"; import { useSelector } from "react-redux"; -import { - formatDateWithTz, - formatDurationSplit, -} from "../../../../../../Utils/timeUtils.js"; -import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js"; +import { formatDateWithTz, formatDurationSplit } from "@/Utils/timeUtils.js"; +import { useMonitorUtils } from "@/Hooks/useMonitorUtils.js"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import IconBox from "@/Components/v1/IconBox/index.jsx"; @@ -224,7 +221,7 @@ const Card = ({ monitor }) => { navigate(`/pagespeed/${monitor._id}`)} + onClick={() => navigate(`/pagespeed/${monitor.id}`)} border={1} borderColor={theme.palette.primary.lowContrast} borderRadius={theme.shape.borderRadius} diff --git a/client/src/Pages/v1/PageSpeed/Monitors/Components/MonitorGrid/index.jsx b/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx similarity index 100% rename from client/src/Pages/v1/PageSpeed/Monitors/Components/MonitorGrid/index.jsx rename to client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx diff --git a/client/src/Pages/v1/PageSpeed/Monitors/index.jsx b/client/src/Pages/PageSpeed/Monitors/index.jsx similarity index 64% rename from client/src/Pages/v1/PageSpeed/Monitors/index.jsx rename to client/src/Pages/PageSpeed/Monitors/index.jsx index 7485a72c9..548cd3491 100644 --- a/client/src/Pages/v1/PageSpeed/Monitors/index.jsx +++ b/client/src/Pages/PageSpeed/Monitors/index.jsx @@ -10,9 +10,12 @@ import FallbackPageSpeedWarning from "@/Components/v1/Fallback/FallbackPageSpeed // Utils import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; -import { useFetchMonitorsByTeamId } from "../../../../Hooks/v1/monitorHooks.js"; -import { useFetchSettings } from "../../../../Hooks/v1/settingsHooks.js"; +import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; +import { + useFetchMonitorsByTeamId, + useFetchMonitorsWithChecks, +} from "@/Hooks/monitorHooks.js"; +import { useFetchSettings } from "@/Hooks/settingsHooks.js"; // Constants const BREADCRUMBS = [{ name: `pagespeed`, path: "/pagespeed" }]; const TYPES = ["pagespeed"]; @@ -20,14 +23,30 @@ const PageSpeed = () => { const theme = useTheme(); const isAdmin = useIsAdmin(); - const [monitors, monitorsSummary, isLoading, networkError] = useFetchMonitorsByTeamId({ - limit: 10, + // const [monitors, monitorsSummary, isLoading, networkError] = useFetchMonitorsByTeamId({ + // limit: 10, + // types: TYPES, + // page: null, + // rowsPerPage: null, + // filter: null, + // field: null, + // order: null, + // }); + + const [ + monitorsWithChecks, + monitorsWithChecksCount, + monitorsWithChecksIsLoading, + monitorsWithChecksNetworkError, + ] = useFetchMonitorsWithChecks({ types: TYPES, + limit: 10, page: null, rowsPerPage: null, filter: null, field: null, order: null, + monitorUpdateTrigger: null, }); const [settingsData, setSettingsData] = useState(undefined); @@ -40,9 +59,9 @@ const PageSpeed = () => { return ( <> { diff --git a/client/src/Pages/v1/ServerUnreachable.jsx b/client/src/Pages/ServerUnreachable.jsx similarity index 92% rename from client/src/Pages/v1/ServerUnreachable.jsx rename to client/src/Pages/ServerUnreachable.jsx index 8230f473b..bfbbfa90a 100644 --- a/client/src/Pages/v1/ServerUnreachable.jsx +++ b/client/src/Pages/ServerUnreachable.jsx @@ -2,14 +2,14 @@ import React, { useState } from "react"; import { Box, Typography, Button, Stack } from "@mui/material"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; -import { networkService } from "../../Utils/NetworkService.js"; +import { networkService } from "@/Utils/NetworkService.js"; import Alert from "@/Components/v1/Alert/index.jsx"; -import { createToast } from "../../Utils/toastUtils.jsx"; +import { createToast } from "@/Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; -import Background from "../../assets/Images/background-grid.svg?react"; -import Logo from "../../assets/icons/checkmate-icon.svg?react"; +import Background from "@/assets/Images/background-grid.svg?react"; +import Logo from "@/assets/icons/checkmate-icon.svg?react"; import ThemeSwitch from "@/Components/v1/ThemeSwitch/index.jsx"; -import LanguageSelector from "../../Components/LanguageSelector.jsx"; +import LanguageSelector from "@/Components/LanguageSelector.jsx"; const ServerUnreachable = () => { const theme = useTheme(); diff --git a/client/src/Pages/v1/Settings/SettingsAbout.jsx b/client/src/Pages/Settings/SettingsAbout.jsx similarity index 100% rename from client/src/Pages/v1/Settings/SettingsAbout.jsx rename to client/src/Pages/Settings/SettingsAbout.jsx diff --git a/client/src/Pages/v1/Settings/SettingsDemoMonitors.jsx b/client/src/Pages/Settings/SettingsDemoMonitors.jsx similarity index 100% rename from client/src/Pages/v1/Settings/SettingsDemoMonitors.jsx rename to client/src/Pages/Settings/SettingsDemoMonitors.jsx diff --git a/client/src/Pages/v1/Settings/SettingsEmail.jsx b/client/src/Pages/Settings/SettingsEmail.jsx similarity index 98% rename from client/src/Pages/v1/Settings/SettingsEmail.jsx rename to client/src/Pages/Settings/SettingsEmail.jsx index 7b5ad175e..6f0b02137 100644 --- a/client/src/Pages/v1/Settings/SettingsEmail.jsx +++ b/client/src/Pages/Settings/SettingsEmail.jsx @@ -12,8 +12,8 @@ import { PropTypes } from "prop-types"; import { useState } from "react"; import { useTranslation } from "react-i18next"; import { PasswordEndAdornment } from "@/Components/v1/Inputs/TextInput/Adornments/index.jsx"; -import { useSendTestEmail } from "../../../Hooks/v1/useSendTestEmail.js"; -import { createToast } from "../../../Utils/toastUtils.jsx"; +import { useSendTestEmail } from "@/Hooks/useSendTestEmail.js"; +import { createToast } from "@/Utils/toastUtils.jsx"; const SettingsEmail = ({ isAdmin, diff --git a/client/src/Pages/v1/Settings/SettingsExport.jsx b/client/src/Pages/Settings/SettingsExport.jsx similarity index 100% rename from client/src/Pages/v1/Settings/SettingsExport.jsx rename to client/src/Pages/Settings/SettingsExport.jsx diff --git a/client/src/Pages/v1/Settings/SettingsGlobalThresholds.jsx b/client/src/Pages/Settings/SettingsGlobalThresholds.jsx similarity index 100% rename from client/src/Pages/v1/Settings/SettingsGlobalThresholds.jsx rename to client/src/Pages/Settings/SettingsGlobalThresholds.jsx diff --git a/client/src/Pages/v1/Settings/SettingsPagespeed.jsx b/client/src/Pages/Settings/SettingsPagespeed.jsx similarity index 100% rename from client/src/Pages/v1/Settings/SettingsPagespeed.jsx rename to client/src/Pages/Settings/SettingsPagespeed.jsx diff --git a/client/src/Pages/v1/Settings/SettingsStats.jsx b/client/src/Pages/Settings/SettingsStats.jsx similarity index 86% rename from client/src/Pages/v1/Settings/SettingsStats.jsx rename to client/src/Pages/Settings/SettingsStats.jsx index b68006ab2..690b306be 100644 --- a/client/src/Pages/v1/Settings/SettingsStats.jsx +++ b/client/src/Pages/Settings/SettingsStats.jsx @@ -36,16 +36,6 @@ const SettingsStats = ({ isAdmin, HEADING_SX, handleChange, settingsData, errors - {t("settingsPage.statsSettings.clearAllStatsDescription")} diff --git a/client/src/Pages/v1/Settings/SettingsTimeZone.jsx b/client/src/Pages/Settings/SettingsTimeZone.jsx similarity index 97% rename from client/src/Pages/v1/Settings/SettingsTimeZone.jsx rename to client/src/Pages/Settings/SettingsTimeZone.jsx index f684c871d..8ed7e5b3b 100644 --- a/client/src/Pages/v1/Settings/SettingsTimeZone.jsx +++ b/client/src/Pages/Settings/SettingsTimeZone.jsx @@ -3,7 +3,7 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; import Search from "@/Components/v1/Inputs/Search/index.jsx"; -import timezones from "../../../Utils/timezones.json"; +import timezones from "@/Utils/timezones.json"; // Utils import { useTheme } from "@emotion/react"; diff --git a/client/src/Pages/v1/Settings/SettingsUI.jsx b/client/src/Pages/Settings/SettingsUI.jsx similarity index 100% rename from client/src/Pages/v1/Settings/SettingsUI.jsx rename to client/src/Pages/Settings/SettingsUI.jsx diff --git a/client/src/Pages/v1/Settings/SettingsURL.jsx b/client/src/Pages/Settings/SettingsURL.jsx similarity index 100% rename from client/src/Pages/v1/Settings/SettingsURL.jsx rename to client/src/Pages/Settings/SettingsURL.jsx diff --git a/client/src/Pages/v1/Settings/index.jsx b/client/src/Pages/Settings/index.jsx similarity index 95% rename from client/src/Pages/v1/Settings/index.jsx rename to client/src/Pages/Settings/index.jsx index b6137121b..27cc6bb21 100644 --- a/client/src/Pages/v1/Settings/index.jsx +++ b/client/src/Pages/Settings/index.jsx @@ -12,27 +12,22 @@ import SettingsGlobalThresholds from "./SettingsGlobalThresholds.jsx"; import SettingsExport from "./SettingsExport.jsx"; import Button from "@mui/material/Button"; // Utils -import { settingsValidation } from "../../../Validation/validation.js"; +import { settingsValidation } from "@/Validation/validation.js"; import { useState } from "react"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; import { useSelector, useDispatch } from "react-redux"; -import { - setTimezone, - setMode, - setLanguage, - setShowURL, -} from "../../../Features/UI/uiSlice.js"; +import { setTimezone, setMode, setLanguage, setShowURL } from "@/Features/UI/uiSlice.js"; import SettingsStats from "./SettingsStats.jsx"; -import { useFetchSettings, useSaveSettings } from "../../../Hooks/v1/settingsHooks.js"; -import { useIsAdmin } from "../../../Hooks/v1/useIsAdmin.js"; +import { useFetchSettings, useSaveSettings } from "@/Hooks/settingsHooks.js"; +import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; import { useAddDemoMonitors, useDeleteAllMonitors, useDeleteMonitorStats, useFetchJson, -} from "../../../Hooks/v1/monitorHooks.js"; +} from "@/Hooks/monitorHooks.js"; // Constants const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }]; diff --git a/client/src/Pages/v1/StatusPage/Create/Components/MonitorList/index.jsx b/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx similarity index 100% rename from client/src/Pages/v1/StatusPage/Create/Components/MonitorList/index.jsx rename to client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx diff --git a/client/src/Pages/v1/StatusPage/Create/Components/Progress/index.jsx b/client/src/Pages/StatusPage/Create/Components/Progress/index.jsx similarity index 92% rename from client/src/Pages/v1/StatusPage/Create/Components/Progress/index.jsx rename to client/src/Pages/StatusPage/Create/Components/Progress/index.jsx index 232a12f1d..2d9731ea0 100644 --- a/client/src/Pages/v1/StatusPage/Create/Components/Progress/index.jsx +++ b/client/src/Pages/StatusPage/Create/Components/Progress/index.jsx @@ -2,7 +2,7 @@ import { Button, Box } from "@mui/material"; import ProgressUpload from "@/Components/v1/ProgressBars/index.jsx"; import ImageIcon from "@mui/icons-material/Image"; import { useTranslation } from "react-i18next"; -import { formatBytes } from "../../../../../../Utils/fileUtils.js"; +import { formatBytes } from "../../../../../Utils/fileUtils.js"; const Progress = ({ isLoading, progressValue, logo, logoType, removeLogo, errors }) => { const { t } = useTranslation(); if (isLoading) { diff --git a/client/src/Pages/v1/StatusPage/Create/Components/Skeleton/index.jsx b/client/src/Pages/StatusPage/Create/Components/Skeleton/index.jsx similarity index 100% rename from client/src/Pages/v1/StatusPage/Create/Components/Skeleton/index.jsx rename to client/src/Pages/StatusPage/Create/Components/Skeleton/index.jsx diff --git a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/ConfigStack.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx similarity index 100% rename from client/src/Pages/v1/StatusPage/Create/Components/Tabs/ConfigStack.jsx rename to client/src/Pages/StatusPage/Create/Components/Tabs/ConfigStack.jsx diff --git a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Content.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx similarity index 100% rename from client/src/Pages/v1/StatusPage/Create/Components/Tabs/Content.jsx rename to client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx diff --git a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Settings.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx similarity index 98% rename from client/src/Pages/v1/StatusPage/Create/Components/Tabs/Settings.jsx rename to client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx index a668a9db8..d4c583d3a 100644 --- a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/Settings.jsx +++ b/client/src/Pages/StatusPage/Create/Components/Tabs/Settings.jsx @@ -11,7 +11,7 @@ import Progress from "../Progress/index.jsx"; // Utils import { useTheme } from "@emotion/react"; -import timezones from "../../../../../../Utils/timezones.json"; +import timezones from "../../../../../Utils/timezones.json"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; import { useMemo, useState, useCallback } from "react"; diff --git a/client/src/Pages/v1/StatusPage/Create/Components/Tabs/index.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx similarity index 100% rename from client/src/Pages/v1/StatusPage/Create/Components/Tabs/index.jsx rename to client/src/Pages/StatusPage/Create/Components/Tabs/index.jsx diff --git a/client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx b/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx similarity index 83% rename from client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx rename to client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx index 808598052..dac5997f9 100644 --- a/client/src/Pages/v1/StatusPage/Create/Hooks/useCreateStatusPage.jsx +++ b/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { networkService } from "../../../../../main.jsx"; -import { createToast } from "../../../../../Utils/toastUtils.jsx"; +import { networkService } from "../../../../main.jsx"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; const useCreateStatusPage = (isCreate, url) => { const [isLoading, setIsLoading] = useState(false); diff --git a/client/src/Pages/v1/StatusPage/Create/Hooks/useMonitorsFetch.jsx b/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx similarity index 88% rename from client/src/Pages/v1/StatusPage/Create/Hooks/useMonitorsFetch.jsx rename to client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx index 81fefd65c..95ce2b8c0 100644 --- a/client/src/Pages/v1/StatusPage/Create/Hooks/useMonitorsFetch.jsx +++ b/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx @@ -1,7 +1,7 @@ import { useEffect, useState } from "react"; -import { networkService } from "../../../../../main.jsx"; +import { networkService } from "../../../../main.jsx"; import { useSelector } from "react-redux"; -import { createToast } from "../../../../../Utils/toastUtils.jsx"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; const useMonitorsFetch = () => { const { user } = useSelector((state) => state.auth); diff --git a/client/src/Pages/v1/StatusPage/Create/index.jsx b/client/src/Pages/StatusPage/Create/index.jsx similarity index 97% rename from client/src/Pages/v1/StatusPage/Create/index.jsx rename to client/src/Pages/StatusPage/Create/index.jsx index d9fafed70..7fbe6a8dc 100644 --- a/client/src/Pages/v1/StatusPage/Create/index.jsx +++ b/client/src/Pages/StatusPage/Create/index.jsx @@ -8,11 +8,11 @@ import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; //Utils import { useTheme } from "@emotion/react"; import { useState, useEffect, useRef, useCallback } from "react"; -import { statusPageValidation } from "../../../../Validation/validation.js"; -import { buildErrors } from "../../../../Validation/error.js"; +import { statusPageValidation } from "../../../Validation/validation.js"; +import { buildErrors } from "../../../Validation/error.js"; import { useMonitorsFetch } from "./Hooks/useMonitorsFetch.jsx"; import { useCreateStatusPage } from "./Hooks/useCreateStatusPage.jsx"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; +import { createToast } from "../../../Utils/toastUtils.jsx"; import { useNavigate } from "react-router-dom"; import { useStatusPageFetch } from "../Status/Hooks/useStatusPageFetch.jsx"; import { useParams } from "react-router-dom"; diff --git a/client/src/Pages/v1/StatusPage/Status/Components/AdminLink/index.jsx b/client/src/Pages/StatusPage/Status/Components/AdminLink/index.jsx similarity index 100% rename from client/src/Pages/v1/StatusPage/Status/Components/AdminLink/index.jsx rename to client/src/Pages/StatusPage/Status/Components/AdminLink/index.jsx diff --git a/client/src/Pages/v1/StatusPage/Status/Components/ControlsHeader/index.jsx b/client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx similarity index 97% rename from client/src/Pages/v1/StatusPage/Status/Components/ControlsHeader/index.jsx rename to client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx index c56aa40cc..60a4882f5 100644 --- a/client/src/Pages/v1/StatusPage/Status/Components/ControlsHeader/index.jsx +++ b/client/src/Pages/StatusPage/Status/Components/ControlsHeader/index.jsx @@ -1,7 +1,7 @@ // Components import { Box, Stack, Typography, Button } from "@mui/material"; import Image from "@/Components/v1/Image/index.jsx"; -import SettingsIcon from "../../../../../../assets/icons/settings-bold.svg?react"; +import SettingsIcon from "@/assets/icons/settings-bold.svg?react"; import ThemeSwitch from "@/Components/v1/ThemeSwitch/index.jsx"; import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward"; //Utils diff --git a/client/src/Pages/v1/StatusPage/Status/Components/MonitorsList/index.jsx b/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx similarity index 95% rename from client/src/Pages/v1/StatusPage/Status/Components/MonitorsList/index.jsx rename to client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx index 1628b3d44..89e32e5d5 100644 --- a/client/src/Pages/v1/StatusPage/Status/Components/MonitorsList/index.jsx +++ b/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx @@ -6,7 +6,7 @@ import { StatusLabel } from "@/Components/v1/Label/index.jsx"; //Utils import { useTheme } from "@mui/material/styles"; -import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js"; +import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js"; import PropTypes from "prop-types"; import { useSelector } from "react-redux"; diff --git a/client/src/Pages/v1/StatusPage/Status/Components/Skeleton/index.jsx b/client/src/Pages/StatusPage/Status/Components/Skeleton/index.jsx similarity index 100% rename from client/src/Pages/v1/StatusPage/Status/Components/Skeleton/index.jsx rename to client/src/Pages/StatusPage/Status/Components/Skeleton/index.jsx diff --git a/client/src/Pages/v1/StatusPage/Status/Components/StatusBar/index.jsx b/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx similarity index 100% rename from client/src/Pages/v1/StatusPage/Status/Components/StatusBar/index.jsx rename to client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx diff --git a/client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageDelete.jsx b/client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx similarity index 89% rename from client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageDelete.jsx rename to client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx index 038a771ba..73b9a8a5f 100644 --- a/client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageDelete.jsx +++ b/client/src/Pages/StatusPage/Status/Hooks/useStatusPageDelete.jsx @@ -1,6 +1,6 @@ import { useState } from "react"; -import { networkService } from "../../../../../Utils/NetworkService.js"; -import { createToast } from "../../../../../Utils/toastUtils.jsx"; +import { networkService } from "../../../../Utils/NetworkService.js"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; import { useTranslation } from "react-i18next"; /** diff --git a/client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx b/client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx similarity index 87% rename from client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx rename to client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx index 506082887..7db6b6c5e 100644 --- a/client/src/Pages/v1/StatusPage/Status/Hooks/useStatusPageFetch.jsx +++ b/client/src/Pages/StatusPage/Status/Hooks/useStatusPageFetch.jsx @@ -1,9 +1,9 @@ import { useEffect, useState, useCallback } from "react"; -import { networkService } from "../../../../../main.jsx"; +import { networkService } from "../../../../main.jsx"; import { useSelector } from "react-redux"; -import { createToast } from "../../../../../Utils/toastUtils.jsx"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "../../../../../Hooks/v1/useMonitorUtils.js"; +import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils.js"; const useStatusPageFetch = (isCreate = false, url) => { const [isLoading, setIsLoading] = useState(true); diff --git a/client/src/Pages/v1/StatusPage/Status/index.jsx b/client/src/Pages/StatusPage/Status/index.jsx similarity index 98% rename from client/src/Pages/v1/StatusPage/Status/index.jsx rename to client/src/Pages/StatusPage/Status/index.jsx index e700bbbd9..adcd9c2c7 100644 --- a/client/src/Pages/v1/StatusPage/Status/index.jsx +++ b/client/src/Pages/StatusPage/Status/index.jsx @@ -12,7 +12,7 @@ import TextLink from "@/Components/v1/TextLink/index.jsx"; // Utils import { useStatusPageFetch } from "./Hooks/useStatusPageFetch.jsx"; import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; +import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; import { useLocation } from "react-router-dom"; import { useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; diff --git a/client/src/Pages/v1/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx similarity index 100% rename from client/src/Pages/v1/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx rename to client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx diff --git a/client/src/Pages/v1/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx b/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx similarity index 86% rename from client/src/Pages/v1/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx rename to client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx index 89df1b84d..839ff4cdf 100644 --- a/client/src/Pages/v1/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx +++ b/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx @@ -1,7 +1,7 @@ import { useState, useEffect } from "react"; -import { networkService } from "../../../../../main.jsx"; +import { networkService } from "../../../../main.jsx"; import { useSelector } from "react-redux"; -import { createToast } from "../../../../../Utils/toastUtils.jsx"; +import { createToast } from "../../../../Utils/toastUtils.jsx"; const useStatusPagesFetch = () => { const { user } = useSelector((state) => state.auth); diff --git a/client/src/Pages/v1/StatusPage/StatusPages/index.jsx b/client/src/Pages/StatusPage/StatusPages/index.jsx similarity index 95% rename from client/src/Pages/v1/StatusPage/StatusPages/index.jsx rename to client/src/Pages/StatusPage/StatusPages/index.jsx index 3c60fe950..7137d01ad 100644 --- a/client/src/Pages/v1/StatusPage/StatusPages/index.jsx +++ b/client/src/Pages/StatusPage/StatusPages/index.jsx @@ -7,7 +7,7 @@ import PageStateWrapper from "@/Components/v1/PageStateWrapper/index.jsx"; // Utils import { useTheme } from "@emotion/react"; import { useStatusPagesFetch } from "./Hooks/useStatusPagesFetch.jsx"; -import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; +import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; const BREADCRUMBS = [{ name: `Status Pages`, path: "" }]; const StatusPages = () => { diff --git a/client/src/Pages/v1/Uptime/BulkImport/Upload.jsx b/client/src/Pages/Uptime/BulkImport/Upload.jsx similarity index 100% rename from client/src/Pages/v1/Uptime/BulkImport/Upload.jsx rename to client/src/Pages/Uptime/BulkImport/Upload.jsx diff --git a/client/src/Pages/v1/Uptime/BulkImport/index.jsx b/client/src/Pages/Uptime/BulkImport/index.jsx similarity index 95% rename from client/src/Pages/v1/Uptime/BulkImport/index.jsx rename to client/src/Pages/Uptime/BulkImport/index.jsx index 9f3efcd67..6f2f7bcb5 100644 --- a/client/src/Pages/v1/Uptime/BulkImport/index.jsx +++ b/client/src/Pages/Uptime/BulkImport/index.jsx @@ -5,14 +5,14 @@ import { useState } from "react"; import { Box, Stack, Typography, Button, Link } from "@mui/material"; //Components -import { createToast } from "../../../../Utils/toastUtils.jsx"; +import { createToast } from "../../../Utils/toastUtils.jsx"; import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; import UploadFile from "./Upload.jsx"; import { useSelector } from "react-redux"; import { useNavigate } from "react-router-dom"; import { Trans, useTranslation } from "react-i18next"; -import { useCreateBulkMonitors } from "../../../../Hooks/v1/monitorHooks.js"; +import { useCreateBulkMonitors } from "../../../Hooks/monitorHooks.js"; const BulkImport = () => { const theme = useTheme(); diff --git a/client/src/Pages/v1/Uptime/Create/index.css b/client/src/Pages/Uptime/Create/index.css similarity index 100% rename from client/src/Pages/v1/Uptime/Create/index.css rename to client/src/Pages/Uptime/Create/index.css diff --git a/client/src/Pages/v1/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx similarity index 98% rename from client/src/Pages/v1/Uptime/Create/index.jsx rename to client/src/Pages/Uptime/Create/index.jsx index 202f64d85..c3421e875 100644 --- a/client/src/Pages/v1/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -26,14 +26,14 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { monitorValidation } from "../../../../Validation/validation.js"; -import { createToast } from "../../../../Utils/toastUtils.jsx"; +import { monitorValidation } from "../../../Validation/validation.js"; +import { createToast } from "../../../Utils/toastUtils.jsx"; import { PauseOutlined as PauseOutlinedIcon, PlayArrowOutlined as PlayArrowOutlinedIcon, } from "@mui/icons-material"; -import { useMonitorUtils } from "../../../../Hooks/v1/useMonitorUtils.js"; -import { useGetNotificationsByTeamId } from "../../../../Hooks/v1/useNotifications.js"; +import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js"; +import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications.js"; import { useParams } from "react-router-dom"; import { useCreateMonitor, @@ -42,7 +42,7 @@ import { usePauseMonitor, useFetchMonitorById, useFetchMonitorGames, -} from "../../../../Hooks/v1/monitorHooks.js"; +} from "../../../Hooks/monitorHooks.js"; /** * Create page renders monitor creation or configuration views. @@ -287,7 +287,8 @@ const UptimeCreate = ({ isClone = false }) => { const handleRemove = async (event) => { event.preventDefault(); - await deleteMonitor({ monitor, redirect: "/uptime" }); + const TEMP_MONITOR = { id: monitor._id }; + await deleteMonitor({ monitor: TEMP_MONITOR, redirect: "/uptime" }); }; const isBusy = isLoading || isCreating || isDeleting || isUpdating || isPausing; diff --git a/client/src/Pages/v1/Uptime/Create/skeleton.jsx b/client/src/Pages/Uptime/Create/skeleton.jsx similarity index 100% rename from client/src/Pages/v1/Uptime/Create/skeleton.jsx rename to client/src/Pages/Uptime/Create/skeleton.jsx diff --git a/client/src/Pages/v1/Uptime/Details/Components/ChartBoxes/index.jsx b/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx similarity index 92% rename from client/src/Pages/v1/Uptime/Details/Components/ChartBoxes/index.jsx rename to client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx index 45f35a42e..582c7891f 100644 --- a/client/src/Pages/v1/Uptime/Details/Components/ChartBoxes/index.jsx +++ b/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx @@ -1,15 +1,15 @@ // Components import { Stack, Typography, Box } from "@mui/material"; import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; -import UptimeIcon from "../../../../../../assets/icons/uptime-icon.svg?react"; -import IncidentsIcon from "../../../../../../assets/icons/incidents.svg?react"; -import AverageResponseIcon from "../../../../../../assets/icons/average-response-icon.svg?react"; +import UptimeIcon from "@/assets/icons/uptime-icon.svg?react"; +import IncidentsIcon from "@/assets/icons/incidents.svg?react"; +import AverageResponseIcon from "@/assets/icons/average-response-icon.svg?react"; import UpBarChart from "../Charts/UpBarChart.jsx"; import DownBarChart from "../Charts/DownBarChart.jsx"; import ResponseGaugeChart from "../Charts/ResponseGaugeChart.jsx"; import SkeletonLayout from "./skeleton.jsx"; // Utils -import { formatDateWithTz } from "../../../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../../Utils/timeUtils.js"; import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { useState } from "react"; diff --git a/client/src/Pages/v1/Uptime/Details/Components/ChartBoxes/skeleton.jsx b/client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx similarity index 100% rename from client/src/Pages/v1/Uptime/Details/Components/ChartBoxes/skeleton.jsx rename to client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx diff --git a/client/src/Pages/v1/Uptime/Details/Components/Charts/CustomLabels.jsx b/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx similarity index 93% rename from client/src/Pages/v1/Uptime/Details/Components/Charts/CustomLabels.jsx rename to client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx index c9c1ff58d..472c8d058 100644 --- a/client/src/Pages/v1/Uptime/Details/Components/Charts/CustomLabels.jsx +++ b/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx @@ -1,6 +1,6 @@ import PropTypes from "prop-types"; import { useSelector } from "react-redux"; -import { formatDateWithTz } from "../../../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../../Utils/timeUtils.js"; const CustomLabels = ({ x, width, height, firstDataPoint, lastDataPoint, type }) => { const uiTimezone = useSelector((state) => state.ui.timezone); diff --git a/client/src/Pages/v1/Uptime/Details/Components/Charts/DownBarChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx similarity index 100% rename from client/src/Pages/v1/Uptime/Details/Components/Charts/DownBarChart.jsx rename to client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx diff --git a/client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx similarity index 100% rename from client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx rename to client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx diff --git a/client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx similarity index 89% rename from client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChart.jsx rename to client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx index daa7b4f53..861b600d4 100644 --- a/client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChart.jsx +++ b/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx @@ -1,6 +1,6 @@ import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; import MonitorDetailsAreaChart from "@/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx"; -import ResponseTimeIcon from "../../../../../../assets/icons/response-time-icon.svg?react"; +import ResponseTimeIcon from "@/assets/icons/response-time-icon.svg?react"; import SkeletonLayout from "./ResponseTimeChartSkeleton.jsx"; import PropTypes from "prop-types"; diff --git a/client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx b/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx similarity index 100% rename from client/src/Pages/v1/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx rename to client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx diff --git a/client/src/Pages/v1/Uptime/Details/Components/Charts/UpBarChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx similarity index 100% rename from client/src/Pages/v1/Uptime/Details/Components/Charts/UpBarChart.jsx rename to client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx diff --git a/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx b/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx similarity index 97% rename from client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx rename to client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx index 0d0defc3b..0715561d4 100644 --- a/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/index.jsx +++ b/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx @@ -1,11 +1,11 @@ import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; import PropTypes from "prop-types"; -import HistoryIcon from "../../../../../../assets/icons/history-icon.svg?react"; +import HistoryIcon from "@/assets/icons/history-icon.svg?react"; import Table from "@/Components/v1/Table/index.jsx"; import TablePagination from "@/Components/v1/Table/TablePagination/index.jsx"; import { StatusLabel } from "@/Components/v1/Label/index.jsx"; import { useTranslation } from "react-i18next"; -import { formatDateWithTz } from "../../../../../../Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../../Utils/timeUtils.js"; import SkeletonLayout from "./skeleton.jsx"; import Tooltip from "@mui/material/Tooltip"; import Box from "@mui/material/Box"; diff --git a/client/src/Pages/v1/Uptime/Details/Components/ResponseTable/skeleton.jsx b/client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx similarity index 100% rename from client/src/Pages/v1/Uptime/Details/Components/ResponseTable/skeleton.jsx rename to client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx diff --git a/client/src/Pages/v1/Uptime/Details/Components/UptimeStatusBoxes/index.jsx b/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx similarity index 92% rename from client/src/Pages/v1/Uptime/Details/Components/UptimeStatusBoxes/index.jsx rename to client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx index a7b971839..0bfdc1096 100644 --- a/client/src/Pages/v1/Uptime/Details/Components/UptimeStatusBoxes/index.jsx +++ b/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx @@ -2,10 +2,10 @@ import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; import StatBox from "@/Components/v1/StatBox/index.jsx"; import PropTypes from "prop-types"; -import { getHumanReadableDuration } from "../../../../../../Utils/timeUtils.js"; +import { getHumanReadableDuration } from "../../../../../Utils/timeUtils.js"; import { useTheme } from "@mui/material/styles"; import { Typography } from "@mui/material"; -import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js"; +import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js"; const UptimeStatusBoxes = ({ isLoading = false, diff --git a/client/src/Pages/v1/Uptime/Details/Hooks/useCertificateFetch.jsx b/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx similarity index 84% rename from client/src/Pages/v1/Uptime/Details/Hooks/useCertificateFetch.jsx rename to client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx index f85111f97..e74a99d82 100644 --- a/client/src/Pages/v1/Uptime/Details/Hooks/useCertificateFetch.jsx +++ b/client/src/Pages/Uptime/Details/Hooks/useCertificateFetch.jsx @@ -1,7 +1,7 @@ -import { logger } from "../../../../../Utils/Logger.js"; +import { logger } from "../../../../Utils/Logger.js"; import { useEffect, useState } from "react"; -import { networkService } from "../../../../../main.jsx"; -import { formatDateWithTz } from "../../../../../Utils/timeUtils.js"; +import { networkService } from "../../../../main.jsx"; +import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; const useCertificateFetch = ({ monitor, diff --git a/client/src/Pages/v1/Uptime/Details/index.jsx b/client/src/Pages/Uptime/Details/index.jsx similarity index 95% rename from client/src/Pages/v1/Uptime/Details/index.jsx rename to client/src/Pages/Uptime/Details/index.jsx index f5e283495..144ceb4dc 100644 --- a/client/src/Pages/v1/Uptime/Details/index.jsx +++ b/client/src/Pages/Uptime/Details/index.jsx @@ -15,10 +15,10 @@ import { useState } from "react"; import { useParams } from "react-router-dom"; import { useSelector } from "react-redux"; import { useTheme } from "@emotion/react"; -import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; -import { useFetchUptimeMonitorById } from "../../../../Hooks/v1/monitorHooks.js"; +import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; +import { useFetchUptimeMonitorById } from "../../../Hooks/monitorHooks.js"; import useCertificateFetch from "./Hooks/useCertificateFetch.jsx"; -import { useFetchChecksByMonitor } from "../../../../Hooks/v1/checkHooks.js"; +import { useFetchChecksByMonitor } from "../../../Hooks/checkHooks.js"; import { useTranslation } from "react-i18next"; // Constants diff --git a/client/src/Pages/v1/Uptime/Monitors/Components/Filter/index.jsx b/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx similarity index 100% rename from client/src/Pages/v1/Uptime/Monitors/Components/Filter/index.jsx rename to client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx diff --git a/client/src/Pages/v1/Uptime/Monitors/Components/LoadingSpinner/index.jsx b/client/src/Pages/Uptime/Monitors/Components/LoadingSpinner/index.jsx similarity index 100% rename from client/src/Pages/v1/Uptime/Monitors/Components/LoadingSpinner/index.jsx rename to client/src/Pages/Uptime/Monitors/Components/LoadingSpinner/index.jsx diff --git a/client/src/Pages/v1/Uptime/Monitors/Components/SearchComponent/index.jsx b/client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx similarity index 100% rename from client/src/Pages/v1/Uptime/Monitors/Components/SearchComponent/index.jsx rename to client/src/Pages/Uptime/Monitors/Components/SearchComponent/index.jsx diff --git a/client/src/Pages/v1/Uptime/Monitors/Components/Skeleton/index.jsx b/client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx similarity index 100% rename from client/src/Pages/v1/Uptime/Monitors/Components/Skeleton/index.jsx rename to client/src/Pages/Uptime/Monitors/Components/Skeleton/index.jsx diff --git a/client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/index.jsx b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx similarity index 100% rename from client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/index.jsx rename to client/src/Pages/Uptime/Monitors/Components/StatusBoxes/index.jsx diff --git a/client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx similarity index 100% rename from client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx rename to client/src/Pages/Uptime/Monitors/Components/StatusBoxes/skeleton.jsx diff --git a/client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx similarity index 90% rename from client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx rename to client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx index c745e7935..c9015816a 100644 --- a/client/src/Pages/v1/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx @@ -1,9 +1,9 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { Box, Stack, Typography } from "@mui/material"; -import Arrow from "../../../../../../assets/icons/top-right-arrow.svg?react"; -import Background from "../../../../../../assets/Images/background-grid.svg?react"; -import ClockSnooze from "../../../../../../assets/icons/clock-snooze.svg?react"; +import Arrow from "@/assets/icons/top-right-arrow.svg?react"; +import Background from "@/assets/Images/background-grid.svg?react"; +import ClockSnooze from "@/assets/icons/clock-snooze.svg?react"; const StatusBox = ({ title, value, status }) => { const theme = useTheme(); diff --git a/client/src/Pages/v1/Uptime/Monitors/Components/UptimeDataTable/index.jsx b/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx similarity index 97% rename from client/src/Pages/v1/Uptime/Monitors/Components/UptimeDataTable/index.jsx rename to client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx index 9cf2d247d..fe220251d 100644 --- a/client/src/Pages/v1/Uptime/Monitors/Components/UptimeDataTable/index.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx @@ -13,7 +13,7 @@ import TableSkeleton from "@/Components/v1/Table/skeleton.jsx"; // Utils import { useTheme } from "@emotion/react"; -import { useMonitorUtils } from "../../../../../../Hooks/v1/useMonitorUtils.js"; +import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js"; import { useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; @@ -24,13 +24,13 @@ import { useTranslation } from "react-i18next"; * @param {boolean} props.isAdmin - Whether the current user has admin privileges * @param {boolean} props.isLoading - Loading state of the table * @param {Array<{ - * _id: string, + * id: string, * url: string, * title: string, * percentage: number, * percentageColor: string, * monitor: { - * _id: string, + * id: string, * type: string, * checks: Array * } @@ -101,7 +101,7 @@ const UptimeDataTable = ({ ), render: (row) => ( { - navigate(`/uptime/${row._id}`); + navigate(`/uptime/${row.id}`); }, emptyView: "No monitors found", }} diff --git a/client/src/Pages/v1/Uptime/Monitors/Hooks/useDebounce.jsx b/client/src/Pages/Uptime/Monitors/Hooks/useDebounce.jsx similarity index 100% rename from client/src/Pages/v1/Uptime/Monitors/Hooks/useDebounce.jsx rename to client/src/Pages/Uptime/Monitors/Hooks/useDebounce.jsx diff --git a/client/src/Pages/v1/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/index.jsx similarity index 96% rename from client/src/Pages/v1/Uptime/Monitors/index.jsx rename to client/src/Pages/Uptime/Monitors/index.jsx index cacfc23b4..9a73fae63 100644 --- a/client/src/Pages/v1/Uptime/Monitors/index.jsx +++ b/client/src/Pages/Uptime/Monitors/index.jsx @@ -5,7 +5,7 @@ // Components import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import Greeting from "../../../../Utils/greeting.jsx"; +import Greeting from "../../../Utils/greeting.jsx"; import StatusBoxes from "./Components/StatusBoxes/index.jsx"; import UptimeDataTable from "./Components/UptimeDataTable/index.jsx"; import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; @@ -20,16 +20,16 @@ import MonitorCountHeader from "@/Components/v1/MonitorCountHeader/index.jsx"; import { Stack, Box, Button } from "@mui/material"; // Utils import { useState, useCallback, useEffect } from "react"; -import { useIsAdmin } from "../../../../Hooks/v1/useIsAdmin.js"; +import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; import { useSelector, useDispatch } from "react-redux"; -import { setRowsPerPage } from "../../../../Features/UI/uiSlice.js"; +import { setRowsPerPage } from "../../../Features/UI/uiSlice.js"; import PropTypes from "prop-types"; import { useFetchMonitorsWithSummary, useFetchMonitorsWithChecks, -} from "../../../../Hooks/v1/monitorHooks.js"; +} from "../../../Hooks/monitorHooks.js"; import { useTranslation } from "react-i18next"; const TYPES = ["http", "ping", "docker", "port", "game"]; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 255d913c1..d9f6d58cb 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -1,58 +1,58 @@ import { useSelector } from "react-redux"; import { Navigate, Route, Routes as LibRoutes } from "react-router"; import HomeLayout from "@/Components/v1/Layouts/HomeLayout"; -import NotFound from "../Pages/v1/NotFound"; +import NotFound from "../Pages/NotFound/index.jsx"; // Auth -import AuthLogin from "../Pages/v1/Auth/Login"; -import AuthRegister from "../Pages/v1/Auth/Register/"; -import AuthForgotPassword from "../Pages/v1/Auth/ForgotPassword"; -import AuthCheckEmail from "../Pages/v1/Auth/CheckEmail"; -import AuthSetNewPassword from "../Pages/v1/Auth/SetNewPassword"; -import AuthNewPasswordConfirmed from "../Pages/v1/Auth/NewPasswordConfirmed"; +import AuthLogin from "../Pages/Auth/Login/index.jsx"; +import AuthRegister from "../Pages/Auth/Register/index.jsx"; +import AuthForgotPassword from "../Pages/Auth/ForgotPassword.jsx"; +import AuthCheckEmail from "../Pages/Auth/CheckEmail.jsx"; +import AuthSetNewPassword from "../Pages/Auth/SetNewPassword.jsx"; +import AuthNewPasswordConfirmed from "../Pages/Auth/NewPasswordConfirmed.jsx"; // Uptime -import Uptime from "../Pages/v1/Uptime/Monitors"; -import UptimeDetails from "../Pages/v1/Uptime/Details"; -import UptimeCreate from "../Pages/v1/Uptime/Create"; +import Uptime from "../Pages/Uptime/Monitors/index.jsx"; +import UptimeDetails from "../Pages/Uptime/Details/index.jsx"; +import UptimeCreate from "../Pages/Uptime/Create/index.jsx"; // PageSpeed -import PageSpeed from "../Pages/v1/PageSpeed/Monitors"; -import PageSpeedDetails from "../Pages/v1/PageSpeed/Details"; -import PageSpeedCreate from "../Pages/v1/PageSpeed/Create"; +import PageSpeed from "../Pages/PageSpeed/Monitors/index.jsx"; +import PageSpeedDetails from "../Pages/PageSpeed/Details/index.jsx"; +import PageSpeedCreate from "../Pages/PageSpeed/Create/index.jsx"; // Infrastructure -import Infrastructure from "../Pages/v1/Infrastructure/Monitors"; -import InfrastructureCreate from "../Pages/v1/Infrastructure/Create"; -import InfrastructureDetails from "../Pages/v1/Infrastructure/Details"; +import Infrastructure from "../Pages/Infrastructure/Monitors/index.jsx"; +import InfrastructureCreate from "../Pages/Infrastructure/Create/index.jsx"; +import InfrastructureDetails from "../Pages/Infrastructure/Details/index.jsx"; // Server Status -import ServerUnreachable from "../Pages/v1/ServerUnreachable.jsx"; +import ServerUnreachable from "../Pages/ServerUnreachable.jsx"; // Incidents -import Checks from "../Pages/v1/Incidents"; -import Incidents2 from "../Pages/v1/Incidents2"; +import Checks from "../Pages/Incidents"; +import Incidents2 from "../Pages/Incidents2/index.jsx"; // Status pages -import CreateStatus from "../Pages/v1/StatusPage/Create"; -import StatusPages from "../Pages/v1/StatusPage/StatusPages"; -import Status from "../Pages/v1/StatusPage/Status"; +import CreateStatus from "../Pages/StatusPage/Create/index.jsx"; +import StatusPages from "../Pages/StatusPage/StatusPages/index.jsx"; +import Status from "../Pages/StatusPage/Status/index.jsx"; -import Notifications from "../Pages/v1/Notifications"; -import CreateNotifications from "../Pages/v1/Notifications/create"; +import Notifications from "../Pages/Notifications/index.jsx"; +import CreateNotifications from "../Pages/Notifications/create/index.jsx"; // Settings -import Account from "../Pages/v1/Account"; -import EditUser from "../Pages/v1/Account/EditUser"; -import Settings from "../Pages/v1/Settings"; +import Account from "../Pages/Account/index.jsx"; +import EditUser from "../Pages/Account/EditUser/index.jsx"; +import Settings from "../Pages/Settings"; -import Maintenance from "../Pages/v1/Maintenance"; +import Maintenance from "../Pages/Maintenance/index.jsx"; import ProtectedRoute from "../Components/v1/ProtectedRoute"; import RoleProtectedRoute from "../Components/v1/RoleProtectedRoute"; -import CreateNewMaintenanceWindow from "../Pages/v1/Maintenance/CreateMaintenance"; +import CreateNewMaintenanceWindow from "../Pages/Maintenance/CreateMaintenance/index.jsx"; import withAdminCheck from "@/Components/v1/HOC/withAdminCheck"; -import BulkImport from "../Pages/v1/Uptime/BulkImport"; -import Logs from "../Pages/v1/Logs"; +import BulkImport from "../Pages/Uptime/BulkImport/index.jsx"; +import Logs from "../Pages/Logs/index.jsx"; const Routes = () => { const mode = useSelector((state) => state.ui.mode); diff --git a/server/src/db/models/Check.ts b/server/src/db/models/Check.ts index b4a31f8f7..3ebacd3f6 100644 --- a/server/src/db/models/Check.ts +++ b/server/src/db/models/Check.ts @@ -16,6 +16,7 @@ import type { CheckNetworkInterfaceInfo, CheckTimings, CheckTimingPhases, + ILighthouseAudit, } from "@/types/check.js"; type CheckMetadataDocument = Omit & { @@ -149,13 +150,25 @@ const networkInterfaceSchema = new Schema( { _id: false } ); +const lighthouseAuditSchema = new Schema( + { + id: { type: String }, + title: { type: String }, + score: { type: Number }, + displayValue: { type: String }, + numericValue: { type: Number }, + numericUnit: { type: String }, + }, + { _id: false } +); + const auditsSchema = new Schema( { - cls: { type: Number, default: 0 }, - si: { type: Number, default: 0 }, - fcp: { type: Number, default: 0 }, - lcp: { type: Number, default: 0 }, - tbt: { type: Number, default: 0 }, + cls: { type: lighthouseAuditSchema, default: undefined }, + si: { type: lighthouseAuditSchema, default: undefined }, + fcp: { type: lighthouseAuditSchema, default: undefined }, + lcp: { type: lighthouseAuditSchema, default: undefined }, + tbt: { type: lighthouseAuditSchema, default: undefined }, }, { _id: false } ); diff --git a/server/src/db/modules/checkModule.js b/server/src/db/modules/checkModule.js index 871761290..e420d5ce7 100755 --- a/server/src/db/modules/checkModule.js +++ b/server/src/db/modules/checkModule.js @@ -22,7 +22,7 @@ class CheckModule { createChecks = async (checks) => { try { - await CheckModel.insertMany(checks, { ordered: false }); + await CheckModel.insertMany(checks, { ordered: false, runValidators: true }); } catch (error) { error.service = SERVICE_NAME; error.method = "createCheck"; diff --git a/server/src/db/modules/monitorModule.js b/server/src/db/modules/monitorModule.js index 316caa2ea..d1d4abc5a 100755 --- a/server/src/db/modules/monitorModule.js +++ b/server/src/db/modules/monitorModule.js @@ -242,7 +242,6 @@ class MonitorModule { const dateString = formatLookup[dateRange]; - console.log("WTTTTFFF"); const results = await CheckModel.aggregate(buildUptimeDetailsPipeline(monitorId, dates, dateString)); const monitorData = results[0]; diff --git a/server/src/service/business/monitorService.js b/server/src/service/business/monitorService.js index f457e5c7a..4bdc45eb4 100644 --- a/server/src/service/business/monitorService.js +++ b/server/src/service/business/monitorService.js @@ -220,7 +220,6 @@ class MonitorService { }; getMonitorsByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order }) => { - console.log("que"); const monitors = await this.db.monitorModule.getMonitorsByTeamId({ limit, type, diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index 843c356ed..5b64c7419 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -367,18 +367,30 @@ class StatusService { } const categories = payload?.lighthouseResult?.categories ?? {}; const audits = payload?.lighthouseResult?.audits ?? {}; - const { - "cumulative-layout-shift": cls = 0, - "speed-index": si = 0, - "first-contentful-paint": fcp = 0, - "largest-contentful-paint": lcp = 0, - "total-blocking-time": tbt = 0, - } = audits; + const mapAudit = (audit) => { + if (!audit || typeof audit !== "object") { + return undefined; + } + return { + id: audit.id, + title: audit.title, + score: typeof audit.score === "number" ? audit.score : audit.score ?? null, + displayValue: audit.displayValue, + numericValue: typeof audit.numericValue === "number" ? audit.numericValue : undefined, + numericUnit: audit.numericUnit, + }; + }; check.accessibility = (categories?.accessibility?.score || 0) * 100; check.bestPractices = (categories?.["best-practices"]?.score || 0) * 100; check.seo = (categories?.seo?.score || 0) * 100; check.performance = (categories?.performance?.score || 0) * 100; - check.audits = { cls, si, fcp, lcp, tbt }; + check.audits = { + cls: mapAudit(audits?.["cumulative-layout-shift"]), + si: mapAudit(audits?.["speed-index"]), + fcp: mapAudit(audits?.["first-contentful-paint"]), + lcp: mapAudit(audits?.["largest-contentful-paint"]), + tbt: mapAudit(audits?.["total-blocking-time"]), + }; } if (type === "hardware") { diff --git a/server/src/types/check.ts b/server/src/types/check.ts index 2120d5de1..28353497e 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -86,11 +86,20 @@ export interface CheckNetworkInterfaceInfo { } export interface CheckAudits { - cls: number; - si: number; - fcp: number; - lcp: number; - tbt: number; + cls: ILighthouseAudit; + si: ILighthouseAudit; + fcp: ILighthouseAudit; + lcp: ILighthouseAudit; + tbt: ILighthouseAudit; +} + +export interface ILighthouseAudit { + id?: string; + title?: string; + score?: number | null; + displayValue?: string; + numericValue?: number; + numericUnit?: string; } export interface Check { From 2b0a51520a40f28b54877b1658f4c44fbcdc6e35 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 13 Jan 2026 17:50:31 +0000 Subject: [PATCH 193/400] infra --- .../Pages/Infrastructure/Monitors/index.jsx | 20 +++++++++++-------- client/src/Pages/Uptime/Monitors/index.jsx | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/client/src/Pages/Infrastructure/Monitors/index.jsx b/client/src/Pages/Infrastructure/Monitors/index.jsx index 90817f116..4d739417c 100644 --- a/client/src/Pages/Infrastructure/Monitors/index.jsx +++ b/client/src/Pages/Infrastructure/Monitors/index.jsx @@ -13,7 +13,11 @@ import { useTheme } from "@emotion/react"; import { useEffect, useState } from "react"; import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; import { useTranslation } from "react-i18next"; -import { useFetchMonitorsByTeamId } from "../../../Hooks/monitorHooks.js"; +import { + useFetchMonitorsByTeamId, + useFetchMonitorsWithChecks, +} from "@/Hooks/monitorHooks.js"; + import { useDispatch, useSelector } from "react-redux"; import { setRowsPerPage } from "../../../Features/UI/uiSlice.js"; // Constants @@ -70,14 +74,14 @@ const InfrastructureMonitors = () => { const field = toFilterStatus !== undefined ? "status" : undefined; - const [monitors, summary, isLoading, networkError] = useFetchMonitorsByTeamId({ - limit: 1, + const [monitors, count, isLoading, networkError] = useFetchMonitorsWithChecks({ types: TYPES, - page, + limit: 1, + page: page, field: field, filter: toFilterStatus ?? search, - rowsPerPage, - updateTrigger, + rowsPerPage: rowsPerPage, + monitorUpdateTrigger: updateTrigger, }); return ( @@ -99,7 +103,7 @@ const InfrastructureMonitors = () => { { isSearching={isSearching} /> Date: Tue, 13 Jan 2026 18:37:22 +0000 Subject: [PATCH 194/400] _id -> id --- .../Infrastructure/Monitors/Components/MonitorsTable/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx index 510115d8a..5f0360312 100644 --- a/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx +++ b/client/src/Pages/Infrastructure/Monitors/Components/MonitorsTable/index.jsx @@ -115,7 +115,7 @@ const MonitorsTable = ({ : theme.palette.success.main; return { - id: monitor._id, + id: monitor.id, name: monitor.name, url: monitor.url, processor, From f8e88623a881fb5b55126de8b325a1bed3c225ac Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 13 Jan 2026 18:54:28 +0000 Subject: [PATCH 195/400] hardware details pipeline refactor --- server/src/db/modules/monitorModule.js | 16 +- server/src/db/modules/monitorModuleQueries.js | 829 ++++-------------- 2 files changed, 188 insertions(+), 657 deletions(-) diff --git a/server/src/db/modules/monitorModule.js b/server/src/db/modules/monitorModule.js index d1d4abc5a..9c8ca7e32 100755 --- a/server/src/db/modules/monitorModule.js +++ b/server/src/db/modules/monitorModule.js @@ -1,11 +1,13 @@ import { buildUptimeDetailsPipeline, - buildHardwareDetailsPipeline, buildMonitorSummaryByTeamIdPipeline, buildMonitorsByTeamIdPipeline, buildMonitorsAndSummaryByTeamIdPipeline, buildMonitorsWithChecksByTeamIdPipeline, buildFilteredMonitorsByTeamIdPipeline, + getHardwareStats, + getUpChecks, + getAggregateData, } from "./monitorModuleQueries.js"; import { CheckModel } from "@/db/models/index.js"; @@ -317,9 +319,17 @@ class MonitorModule { }; const dateString = formatLookup[dateRange]; - const hardwareStats = await CheckModel.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString)); + const [aggregateData, upChecksCount, metrics] = await Promise.all([ + getAggregateData(monitorId, dates), + getUpChecks(monitorId, dates), + getHardwareStats(monitorId, dates, dateString), + ]); - const stats = hardwareStats[0]; + const stats = { + aggregateData: aggregateData, + upChecks: upChecksCount, + checks: metrics, + }; return { ...monitor.toObject(), diff --git a/server/src/db/modules/monitorModuleQueries.js b/server/src/db/modules/monitorModuleQueries.js index e6c6b4ec6..596f11791 100755 --- a/server/src/db/modules/monitorModuleQueries.js +++ b/server/src/db/modules/monitorModuleQueries.js @@ -1,4 +1,5 @@ import { ObjectId } from "mongodb"; +import { CheckModel } from "@/db/models/index.js"; const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => { return [ @@ -168,659 +169,6 @@ const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => { ]; }; -const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { - return [ - { - $match: { - "metadata.monitorId": monitor._id, - "metadata.type": "hardware", - createdAt: { $gte: dates.start, $lte: dates.end }, - }, - }, - { - $sort: { - createdAt: 1, - }, - }, - { - $facet: { - aggregateData: [ - { - $group: { - _id: null, - latestCheck: { - $last: "$$ROOT", - }, - totalChecks: { - $sum: 1, - }, - }, - }, - ], - upChecks: [ - { - $match: { - status: true, - }, - }, - { - $group: { - _id: null, - totalChecks: { - $sum: 1, - }, - }, - }, - ], - checks: [ - { - $limit: 1, - }, - { - $project: { - diskCount: { - $size: { $ifNull: ["$disk", []] }, - }, - netCount: { $size: { $ifNull: ["$net", []] } }, - }, - }, - { - $lookup: { - from: "checks", - let: { - diskCount: "$diskCount", - netCount: "$netCount", - }, - pipeline: [ - { - $match: { - $expr: { - $and: [ - { $eq: ["$metadata.monitorId", monitor._id] }, - { $eq: ["$metadata.type", "hardware"] }, - { $gte: ["$createdAt", dates.start] }, - { $lte: ["$createdAt", dates.end] }, - ], - }, - }, - }, - { - $group: { - _id: { - $dateToString: { - format: dateString, - date: "$createdAt", - }, - }, - avgCpuUsage: { - $avg: "$cpu.usage_percent", - }, - avgMemoryUsage: { - $avg: "$memory.usage_percent", - }, - avgTemperatures: { - $push: { - $ifNull: ["$cpu.temperature", [0]], - }, - }, - disks: { - $push: "$disk", - }, - net: { - $push: "$net", - }, - updatedAts: { - $push: "$updatedAt", - }, - }, - }, - { - $project: { - _id: 1, - avgCpuUsage: 1, - avgMemoryUsage: 1, - avgTemperature: { - $map: { - input: { - $range: [ - 0, - { - $size: { - // Handle null temperatures array - $ifNull: [ - { $arrayElemAt: ["$avgTemperatures", 0] }, - [0], // Default to single-element array if null - ], - }, - }, - ], - }, - as: "index", - in: { - $avg: { - $map: { - input: "$avgTemperatures", - as: "tempArray", - in: { - $ifNull: [ - { $arrayElemAt: ["$$tempArray", "$$index"] }, - 0, // Default to 0 if element is null - ], - }, - }, - }, - }, - }, - }, - disks: { - $map: { - input: { - $range: [0, "$$diskCount"], - }, - as: "diskIndex", - in: { - name: { - $concat: [ - "disk", - { - $toString: "$$diskIndex", - }, - ], - }, - readSpeed: { - $avg: { - $map: { - input: "$disks", - as: "diskArray", - in: { - $arrayElemAt: ["$$diskArray.read_speed_bytes", "$$diskIndex"], - }, - }, - }, - }, - writeSpeed: { - $avg: { - $map: { - input: "$disks", - as: "diskArray", - in: { - $arrayElemAt: ["$$diskArray.write_speed_bytes", "$$diskIndex"], - }, - }, - }, - }, - totalBytes: { - $avg: { - $map: { - input: "$disks", - as: "diskArray", - in: { - $arrayElemAt: ["$$diskArray.total_bytes", "$$diskIndex"], - }, - }, - }, - }, - freeBytes: { - $avg: { - $map: { - input: "$disks", - as: "diskArray", - in: { - $arrayElemAt: ["$$diskArray.free_bytes", "$$diskIndex"], - }, - }, - }, - }, - usagePercent: { - $avg: { - $map: { - input: "$disks", - as: "diskArray", - in: { - $arrayElemAt: ["$$diskArray.usage_percent", "$$diskIndex"], - }, - }, - }, - }, - }, - }, - }, - net: { - $map: { - input: { $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$net", 0] }, []] } }] }, - as: "netIndex", - in: { - name: { - $arrayElemAt: [ - { - $map: { - input: { $arrayElemAt: ["$net", 0] }, - as: "iface", - in: "$$iface.name", - }, - }, - "$$netIndex", - ], - }, - bytesSentPerSecond: { - $let: { - vars: { - first: { - $arrayElemAt: [ - { $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.bytes_sent" } }, - "$$netIndex", - ], - }, - last: { - $arrayElemAt: [ - { - $map: { - input: { - $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }], - }, - as: "iface", - in: "$$iface.bytes_sent", - }, - }, - "$$netIndex", - ], - }, - tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { - $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }], - }, - }, - in: { - $cond: [ - { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, - { - $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], - }, - 0, - ], - }, - }, - }, - deltaBytesRecv: { - $let: { - vars: { - first: { - $arrayElemAt: [ - { $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.bytes_recv" } }, - "$$netIndex", - ], - }, - last: { - $arrayElemAt: [ - { - $map: { - input: { - $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }], - }, - as: "iface", - in: "$$iface.bytes_recv", - }, - }, - "$$netIndex", - ], - }, - tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { - $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }], - }, - }, - in: { - $cond: [ - { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, - { - $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], - }, - 0, - ], - }, - }, - }, - deltaPacketsSent: { - $let: { - vars: { - first: { - $arrayElemAt: [ - { $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.packets_sent" } }, - "$$netIndex", - ], - }, - last: { - $arrayElemAt: [ - { - $map: { - input: { - $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }], - }, - as: "iface", - in: "$$iface.packets_sent", - }, - }, - "$$netIndex", - ], - }, - tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { - $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }], - }, - }, - in: { - $cond: [ - { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, - { - $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], - }, - 0, - ], - }, - }, - }, - deltaPacketsRecv: { - $let: { - vars: { - first: { - $arrayElemAt: [ - { - $map: { - input: { $arrayElemAt: ["$net", 0] }, - as: "iface", - in: "$$iface.packets_recv", - }, - }, - "$$netIndex", - ], - }, - last: { - $arrayElemAt: [ - { - $map: { - input: { - $arrayElemAt: [ - { $ifNull: ["$net", []] }, - { - $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1], - }, - ], - }, - as: "iface", - in: "$$iface.packets_recv", - }, - }, - "$$netIndex", - ], - }, - tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { - $arrayElemAt: [ - { $ifNull: ["$updatedAts", []] }, - { - $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1], - }, - ], - }, - }, - in: { - $cond: [ - { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, - { - $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], - }, - 0, - ], - }, - }, - }, - deltaErrIn: { - $let: { - vars: { - first: { - $arrayElemAt: [{ $map: { input: { $arrayElemAt: ["$net", 0] }, as: "iface", in: "$$iface.err_in" } }, "$$netIndex"], - }, - last: { - $arrayElemAt: [ - { - $map: { - input: { - $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }], - }, - as: "iface", - in: "$$iface.err_in", - }, - }, - "$$netIndex", - ], - }, - tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { - $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }], - }, - }, - in: { - $cond: [ - { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, - { - $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], - }, - 0, - ], - }, - }, - }, - deltaErrOut: { - $let: { - vars: { - first: { - $arrayElemAt: [ - { - $map: { - input: { $arrayElemAt: ["$net", 0] }, - as: "iface", - in: "$$iface.err_out", - }, - }, - "$$netIndex", - ], - }, - last: { - $arrayElemAt: [ - { - $map: { - input: { - $arrayElemAt: [ - { $ifNull: ["$net", []] }, - { - $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1], - }, - ], - }, - as: "iface", - in: "$$iface.err_out", - }, - }, - "$$netIndex", - ], - }, - tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { - $arrayElemAt: [ - { $ifNull: ["$updatedAts", []] }, - { - $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1], - }, - ], - }, - }, - in: { - $cond: [ - { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, - { - $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], - }, - 0, - ], - }, - }, - }, - deltaDropIn: { - $let: { - vars: { - first: { - $arrayElemAt: [ - { - $map: { - input: { $arrayElemAt: ["$net", 0] }, - as: "iface", - in: "$$iface.drop_in", - }, - }, - "$$netIndex", - ], - }, - last: { - $arrayElemAt: [ - { - $map: { - input: { - $arrayElemAt: [ - { $ifNull: ["$net", []] }, - { - $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1], - }, - ], - }, - as: "iface", - in: "$$iface.drop_in", - }, - }, - "$$netIndex", - ], - }, - tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { - $arrayElemAt: [ - { $ifNull: ["$updatedAts", []] }, - { - $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1], - }, - ], - }, - }, - in: { - $cond: [ - { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, - { - $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], - }, - 0, - ], - }, - }, - }, - deltaDropOut: { - $let: { - vars: { - first: { - $arrayElemAt: [ - { - $map: { - input: { $arrayElemAt: ["$net", 0] }, - as: "iface", - in: "$$iface.drop_out", - }, - }, - "$$netIndex", - ], - }, - last: { - $arrayElemAt: [ - { - $map: { - input: { - $arrayElemAt: [ - { $ifNull: ["$net", []] }, - { - $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1], - }, - ], - }, - as: "iface", - in: "$$iface.drop_out", - }, - }, - "$$netIndex", - ], - }, - tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { - $arrayElemAt: [ - { $ifNull: ["$updatedAts", []] }, - { - $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1], - }, - ], - }, - }, - in: { - $cond: [ - { $gt: [{ $subtract: ["$$tLast", "$$tFirst"] }, 0] }, - { - $divide: [{ $subtract: ["$$last", "$$first"] }, { $divide: [{ $subtract: ["$$tLast", "$$tFirst"] }, 1000] }], - }, - 0, - ], - }, - }, - }, - }, - }, - }, - firstUpdatedAt: 1, - lastUpdatedAt: 1, - }, - }, - ], - as: "hourlyStats", - }, - }, - { - $unwind: "$hourlyStats", - }, - { - $replaceRoot: { - newRoot: "$hourlyStats", - }, - }, - ], - }, - }, - { $unwind: "$checks" }, - { $sort: { "checks._id": 1 } }, - { - $group: { - _id: "$_id", - checks: { $push: "$checks" }, - aggregateData: { $first: "$aggregateData" }, - upChecks: { $first: "$upChecks" }, - }, - }, - { - $project: { - aggregateData: { - $arrayElemAt: ["$aggregateData", 0], - }, - upChecks: { - $arrayElemAt: ["$upChecks", 0], - }, - checks: 1, - }, - }, - ]; -}; - const buildMonitorStatsPipeline = (monitor) => { return [ { @@ -1208,7 +556,6 @@ const buildGetMonitorsByTeamIdPipeline = (req) => { export { buildUptimeDetailsPipeline, - buildHardwareDetailsPipeline, buildMonitorStatsPipeline, buildGetMonitorsByTeamIdPipeline, buildMonitorSummaryByTeamIdPipeline, @@ -1217,3 +564,177 @@ export { buildMonitorsWithChecksByTeamIdPipeline, buildFilteredMonitorsByTeamIdPipeline, }; + +export const getAggregateData = async (monitorId, dates) => { + const result = await CheckModel.aggregate([ + { + $match: { + "metadata.monitorId": new ObjectId(monitorId), + "metadata.type": "hardware", + createdAt: { $gte: dates.start, $lte: dates.end }, + }, + }, + { $sort: { createdAt: -1 } }, + { + $group: { + _id: null, + latestCheck: { $first: "$$ROOT" }, + totalChecks: { $sum: 1 }, + }, + }, + ]); + return result[0] || { totalChecks: 0, latestCheck: null }; +}; + +export const getUpChecks = async (monitorId, dates) => { + const count = await CheckModel.countDocuments({ + "metadata.monitorId": new ObjectId(monitorId), + "metadata.type": "hardware", + createdAt: { $gte: dates.start, $lte: dates.end }, + status: true, + }); + return { totalChecks: count }; +}; + +export const getHardwareStats = async (monitorId, dates, dateString) => { + return await CheckModel.aggregate([ + { + $match: { + "metadata.monitorId": new ObjectId(monitorId), + "metadata.type": "hardware", + createdAt: { $gte: dates.start, $lte: dates.end }, + }, + }, + { $sort: { createdAt: 1 } }, + { + $group: { + _id: { $dateToString: { format: dateString, date: "$createdAt" } }, + avgCpuUsage: { $avg: "$cpu.usage_percent" }, + avgMemoryUsage: { $avg: "$memory.usage_percent" }, + avgTemperatures: { $push: { $ifNull: ["$cpu.temperature", [0]] } }, + disks: { $push: "$disk" }, + net: { $push: "$net" }, + updatedAts: { $push: "$updatedAt" }, + sampleDoc: { $first: "$$ROOT" }, + }, + }, + { + $project: { + _id: 1, + avgCpuUsage: 1, + avgMemoryUsage: 1, + avgTemperature: { + $map: { + input: { $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$avgTemperatures", 0] }, [0]] } }] }, + as: "idx", + in: { $avg: { $map: { input: "$avgTemperatures", as: "t", in: { $arrayElemAt: ["$$t", "$$idx"] } } } }, + }, + }, + disks: { + $map: { + input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.disk", []] } }] }, + as: "dIdx", + in: { + name: { $concat: ["disk", { $toString: "$$dIdx" }] }, + readSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.read_speed_bytes", "$$dIdx"] } } } }, + writeSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.write_speed_bytes", "$$dIdx"] } } } }, + totalBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.total_bytes", "$$dIdx"] } } } }, + freeBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.free_bytes", "$$dIdx"] } } } }, + usagePercent: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.usage_percent", "$$dIdx"] } } } }, + }, + }, + }, + net: { + $map: { + input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.net", []] } }] }, + as: "nIdx", + in: { + name: { $arrayElemAt: ["$sampleDoc.net.name", "$$nIdx"] }, + bytesSentPerSecond: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaBytesRecv: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaPacketsSent: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaPacketsRecv: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaErrIn: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaErrOut: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaDropIn: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaDropOut: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + }, + }, + }, + }, + }, + { $sort: { _id: 1 } }, + ]); +}; From 117a2e00c44604028d4cc39af3a97d31c4c741e9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 13 Jan 2026 19:01:56 +0000 Subject: [PATCH 196/400] status page fix --- server/src/db/modules/statusPageModule.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/db/modules/statusPageModule.js b/server/src/db/modules/statusPageModule.js index aadde0c81..999e0bdc9 100755 --- a/server/src/db/modules/statusPageModule.js +++ b/server/src/db/modules/statusPageModule.js @@ -153,7 +153,7 @@ class StatusPageModule { pipeline: [ { $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, + $expr: { $eq: ["$metadata.monitorId", "$$monitorId"] }, }, }, { $sort: { createdAt: -1 } }, From d0b3bc60dffa9f4f8cb48637b641cca013dc1a40 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 13 Jan 2026 20:20:00 +0000 Subject: [PATCH 197/400] create and update --- server/src/config/services.js | 6 +- server/src/controllers/monitorController.ts | 2 +- .../monitors/IMonitorsRepository.ts | 4 + .../monitors/MongoMonitorsRepository.ts | 46 +- .../{monitorService.js => monitorService.ts} | 436 ++++++++++++------ server/src/service/index.ts | 1 + .../SuperSimpleQueue/SuperSimpleQueue.js | 27 +- .../SuperSimpleQueueHelper.js | 4 +- .../service/infrastructure/networkService.js | 12 +- .../service/infrastructure/statusService.js | 32 +- 10 files changed, 384 insertions(+), 186 deletions(-) rename server/src/service/business/{monitorService.js => monitorService.ts} (53%) create mode 100644 server/src/service/index.ts diff --git a/server/src/config/services.js b/server/src/config/services.js index 0916752ab..6878026d6 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -16,7 +16,7 @@ import CheckService from "../service/business/checkService.js"; import DiagnosticService from "../service/business/diagnosticService.js"; import InviteService from "../service/business/inviteService.js"; import MaintenanceWindowService from "../service/business/maintenanceWindowService.js"; -import MonitorService from "../service/business/monitorService.js"; +import { MonitorService } from "@/service/index.js"; import IncidentService from "../service/business/incidentService.js"; import papaparse from "papaparse"; import axios from "axios"; @@ -152,7 +152,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService const bufferService = new BufferService({ db, logger, envSettings, incidentService }); - const statusService = new StatusService({ db, logger, buffer: bufferService, incidentService }); + const statusService = new StatusService({ db, logger, buffer: bufferService, incidentService, monitorsRepository }); const notificationUtils = new NotificationUtils({ stringService, @@ -182,6 +182,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService db, logger, helper: superSimpleQueueHelper, + monitorsRepository, }); // Business services @@ -218,7 +219,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService }); const monitorService = new MonitorService({ db, - settingsService, jobQueue: superSimpleQueue, stringService, emailService, diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 425979062..ba0a237bf 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -193,7 +193,7 @@ class MonitorController { const userId = req?.user?._id; const teamId = req?.user?.teamId; - const monitor = await this.monitorService.createMonitor({ teamId, userId, body: req.body }); + const monitor = await this.monitorService.createMonitor(teamId, userId, req.body); return res.status(200).json({ success: true, diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 865d7f8e4..99131cb4b 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -12,11 +12,15 @@ export interface TeamQueryConfig { export interface IMonitorsRepository { // create + create(monitor: Monitor, teamId: string, userId: string): Promise; // single fetch + findById(monitorId: string): Promise; + // collection fetch findAll(): Promise; findByTeamId(teamId: string, config: TeamQueryConfig): Promise; // update + update(monitorId: string, updates: Partial): Promise; // delete // counts diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index dd85f9a3d..49687c339 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -3,11 +3,31 @@ import type { MonitorDocument } from "@/db/models/Monitor.js"; import type { Monitor, MonitorType } from "@/types/monitor.js"; import mongoose, { type FilterQuery } from "mongoose"; import type { IMonitorsRepository, TeamQueryConfig } from "./IMonitorsRepository.js"; +import { AppError } from "@/utils/AppError.js"; class MongoMonitorsRepository implements IMonitorsRepository { - findAll = async (): Promise => { - const documents = await MonitorModel.find().exec(); - return this.mapDocuments(documents); + create = async (monitor: Monitor, teamId: string, userId: string) => { + const monitorModel = new MonitorModel({ ...monitor, teamId, userId }); + const saved = await monitorModel.save(); + return this.toEntity(saved); + }; + + findById = async (MonitorModelId: string): Promise => { + const monitor = await MonitorModel.findById(MonitorModelId); + if (!monitor) { + if (monitor === null || monitor === undefined) { + throw new AppError({ + message: `Monitor with ID ${MonitorModelId} not found.`, + status: 404, + }); + } + } + return this.toEntity(monitor); + }; + + findAll = async (): Promise => { + const monitors = await MonitorModel.find(); + return this.mapDocuments(monitors); }; findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise => { @@ -64,9 +84,25 @@ class MongoMonitorsRepository implements IMonitorsRepository { return count; }; - private mapDocuments = (documents: MonitorDocument[]): Monitor[] | null => { + update = async (monitorId: string, patch: Partial) => { + const updatedMonitor = await MonitorModel.findOneAndUpdate( + { _id: monitorId }, + { + $set: { + ...patch, + }, + }, + { new: true, runValidators: true } + ); + if (!updatedMonitor) { + throw new AppError({ message: `Failed to update monitor with id ${monitorId}`, status: 500 }); + } + return this.toEntity(updatedMonitor); + }; + + private mapDocuments = (documents: MonitorDocument[]): Monitor[] => { if (!documents?.length) { - return null; + return []; } return documents.map((doc) => this.toEntity(doc)); }; diff --git a/server/src/service/business/monitorService.js b/server/src/service/business/monitorService.ts similarity index 53% rename from server/src/service/business/monitorService.js rename to server/src/service/business/monitorService.ts index 4bdc45eb4..45a2fc235 100644 --- a/server/src/service/business/monitorService.js +++ b/server/src/service/business/monitorService.ts @@ -1,13 +1,88 @@ import { createMonitorsBodyValidation } from "@/validation/joi.js"; import { NormalizeData } from "@/utils/dataUtils.js"; - +import { type Monitor } from "@/types/index.js"; +import type { IMonitorsRepository } from "@/repositories/index.js"; +import { AppError } from "../infrastructure/errorService.js"; const SERVICE_NAME = "MonitorService"; -class MonitorService { + +export interface IMonitorService { + readonly serviceName: string; + verifyTeamAccess(args: { teamId: string; monitorId: string }): Promise; + + // create + createMonitor(teamId: string, userId: string, body: Monitor): Promise; + createBulkMonitors(args: { fileData: string; userId: string; teamId: string }): Promise; + addDemoMonitors(args: { userId: string; teamId: string }): Promise; + + // read + getAllMonitors(): Promise; + getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise; + getMonitorStatsById(args: { + teamId: string; + monitorId: string; + limit?: number; + sortOrder?: 1 | -1; + dateRange?: string; + numToDisplay?: number; + normalize?: boolean; + }): Promise; + getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; + getMonitorById(args: { teamId: string; monitorId: string }): Promise; + getMonitorsByTeamId(args: { + teamId: string; + limit?: number; + type?: string | string[]; + page?: number; + rowsPerPage?: number; + filter?: string; + field?: string; + order?: "asc" | "desc"; + }): Promise; + getMonitorsAndSummaryByTeamId(args: { teamId: string; type?: string | string[]; explain?: boolean }): Promise; + getMonitorsWithChecksByTeamId(args: { + teamId: string; + limit?: number; + type?: string | string[]; + page?: number; + rowsPerPage?: number; + filter?: string; + field?: string; + order?: "asc" | "desc"; + explain?: boolean; + }): Promise<{ count: number; monitors: any[] }>; + getAllGames(): any; + getGroupsByTeamId(args: { teamId: string }): Promise; + + // update + editMonitor(args: { teamId: string; monitorId: string; body: any }): Promise; + pauseMonitor(args: { teamId: string; monitorId: string }): Promise; + + // delete + deleteMonitor(args: { teamId: string; monitorId: string }): Promise; + deleteAllMonitors(args: { teamId: string }): Promise; + + // other + sendTestEmail(args: { to: string }): Promise; + exportMonitorsToCSV(args: { teamId: string }): Promise; + exportMonitorsToJSON(args: { teamId: string }): Promise; +} + +export class MonitorService implements IMonitorService { static SERVICE_NAME = SERVICE_NAME; + private db: any; + private jobQueue: any; + private stringService: any; + private emailService: any; + private papaparse: any; + private logger: any; + private errorService: any; + private games: any; + private monitorsRepository: IMonitorsRepository; + private checksRepository: any; + constructor({ db, - settingsService, jobQueue, stringService, emailService, @@ -17,9 +92,19 @@ class MonitorService { games, monitorsRepository, checksRepository, + }: { + db: any; + jobQueue: any; + stringService: any; + emailService: any; + papaparse: any; + logger: any; + errorService: any; + games: any; + monitorsRepository: IMonitorsRepository; + checksRepository: any; }) { this.db = db; - this.settingsService = settingsService; this.jobQueue = jobQueue; this.stringService = stringService; this.emailService = emailService; @@ -31,79 +116,34 @@ class MonitorService { this.checksRepository = checksRepository; } - get serviceName() { + get serviceName(): string { return MonitorService.SERVICE_NAME; } - verifyTeamAccess = async ({ teamId, monitorId }) => { + verifyTeamAccess = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { const monitor = await this.db.monitorModule.getMonitorById(monitorId); if (!monitor?.teamId?.equals(teamId)) { throw this.errorService.createAuthorizationError(); } }; - getAllMonitors = async () => { - const monitors = await this.db.monitorModule.getAllMonitors(); - return monitors; + createMonitor = async (teamId: string, userId: string, body: Monitor): Promise => { + const monitor = await this.monitorsRepository.create(body, teamId, userId); + if (!monitor) { + throw new AppError("Failed to create monitor", 500); + } + + this.jobQueue.addJob(monitor.id, monitor); }; - getUptimeDetailsById = async ({ teamId, monitorId, dateRange, normalize }) => { - await this.verifyTeamAccess({ teamId, monitorId }); - const data = await this.db.monitorModule.getUptimeDetailsById({ - monitorId, - dateRange, - normalize, - }); - - return data; - }; - - getMonitorStatsById = async ({ teamId, monitorId, limit, sortOrder, dateRange, numToDisplay, normalize }) => { - await this.verifyTeamAccess({ teamId, monitorId }); - const monitorStats = await this.db.monitorModule.getMonitorStatsById({ - monitorId, - limit, - sortOrder, - dateRange, - numToDisplay, - normalize, - }); - - return monitorStats; - }; - - getHardwareDetailsById = async ({ teamId, monitorId, dateRange }) => { - await this.verifyTeamAccess({ teamId, monitorId }); - const monitor = await this.db.monitorModule.getHardwareDetailsById({ monitorId, dateRange }); - - return monitor; - }; - - getMonitorById = async ({ teamId, monitorId }) => { - await this.verifyTeamAccess({ teamId, monitorId }); - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - - return monitor; - }; - - createMonitor = async ({ teamId, userId, body }) => { - const monitor = await this.db.monitorModule.createMonitor({ - body, - teamId, - userId, - }); - - this.jobQueue.addJob(monitor._id, monitor); - }; - - createBulkMonitors = async ({ fileData, userId, teamId }) => { + createBulkMonitors = async ({ fileData, userId, teamId }: { fileData: string; userId: string; teamId: string }): Promise => { const { parse } = this.papaparse; - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { parse(fileData, { header: true, skipEmptyLines: true, - transform: (value, header) => { + transform: (value: string, header: string): string | number | undefined => { if (value === "") return undefined; // Empty fields become undefined // Handle 'port' and 'interval' fields, check if they're valid numbers @@ -117,7 +157,7 @@ class MonitorService { return value; }, - complete: async ({ data, errors }) => { + complete: async ({ data, errors }: { data: any[]; errors: Error[] }): Promise => { try { if (errors.length > 0) { throw this.errorService.createServerError("Error parsing CSV"); @@ -127,7 +167,7 @@ class MonitorService { throw this.errorService.createServerError("CSV file contains no data rows"); } - const enrichedData = data.map((monitor) => ({ + const enrichedData = data.map((monitor: any) => ({ userId, teamId, ...monitor, @@ -141,7 +181,7 @@ class MonitorService { const monitors = await this.db.monitorModule.createBulkMonitors(enrichedData); await Promise.all( - monitors.map(async (monitor) => { + monitors.map(async (monitor: any) => { this.jobQueue.addJob(monitor._id, monitor); }) ); @@ -155,71 +195,102 @@ class MonitorService { }); }; - deleteMonitor = async ({ teamId, monitorId }) => { - await this.verifyTeamAccess({ teamId, monitorId }); - const monitor = await this.db.monitorModule.deleteMonitor({ teamId, monitorId }); - await this.jobQueue.deleteJob(monitor); - await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor._id); - return monitor; - }; - - deleteAllMonitors = async ({ teamId }) => { - const { monitors, deletedCount } = await this.db.monitorModule.deleteAllMonitors(teamId); - await Promise.all( - monitors.map(async (monitor) => { - try { - await this.jobQueue.deleteJob(monitor); - await this.db.checkModule.deleteChecks(monitor._id); - await this.db.pageSpeedCheckModule.deletePageSpeedChecksByMonitorId(monitor._id); - await this.db.notificationsModule.deleteNotificationsByMonitorId(monitor._id); - } catch (error) { - this.logger.warn({ - message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`, - service: SERVICE_NAME, - method: "deleteAllMonitors", - stack: error.stack, - }); - } - }) - ); - return deletedCount; - }; - - editMonitor = async ({ teamId, monitorId, body }) => { - await this.verifyTeamAccess({ teamId, monitorId }); - const editedMonitor = await this.db.monitorModule.editMonitor({ monitorId, body }); - await this.jobQueue.updateJob(editedMonitor); - }; - - pauseMonitor = async ({ teamId, monitorId }) => { - await this.verifyTeamAccess({ teamId, monitorId }); - const monitor = await this.db.monitorModule.pauseMonitor({ monitorId }); - monitor.isActive === true ? await this.jobQueue.resumeJob(monitor._id, monitor) : await this.jobQueue.pauseJob(monitor); - return monitor; - }; - - addDemoMonitors = async ({ userId, teamId }) => { + addDemoMonitors = async ({ userId, teamId }: { userId: string; teamId: string }): Promise => { const demoMonitors = await this.db.monitorModule.addDemoMonitors(userId, teamId); - await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor._id, monitor))); + await Promise.all(demoMonitors.map((monitor: any) => this.jobQueue.addJob(monitor._id, monitor))); return demoMonitors; }; - sendTestEmail = async ({ to }) => { - const subject = this.stringService.testEmailSubject; - const context = { testName: "Monitoring System" }; - - const html = await this.emailService.buildEmail("testEmailTemplate", context); - const messageId = await this.emailService.sendEmail(to, subject, html); - - if (!messageId) { - throw this.errorService.createServerError("Failed to send test email."); - } - - return messageId; + getAllMonitors = async (): Promise => { + const monitors = await this.db.monitorModule.getAllMonitors(); + return monitors; }; - getMonitorsByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order }) => { + getUptimeDetailsById = async ({ + teamId, + monitorId, + dateRange, + normalize, + }: { + teamId: string; + monitorId: string; + dateRange: string; + normalize?: boolean; + }): Promise => { + await this.verifyTeamAccess({ teamId, monitorId }); + const data = await this.db.monitorModule.getUptimeDetailsById({ + monitorId, + dateRange, + normalize, + }); + + return data; + }; + + getMonitorStatsById = async ({ + teamId, + monitorId, + limit, + sortOrder, + dateRange, + numToDisplay, + normalize, + }: { + teamId: string; + monitorId: string; + limit?: number; + sortOrder?: 1 | -1; + dateRange?: string; + numToDisplay?: number; + normalize?: boolean; + }): Promise => { + await this.verifyTeamAccess({ teamId, monitorId }); + const monitorStats = await this.db.monitorModule.getMonitorStatsById({ + monitorId, + limit, + sortOrder, + dateRange, + numToDisplay, + normalize, + }); + + return monitorStats; + }; + + getHardwareDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise => { + await this.verifyTeamAccess({ teamId, monitorId }); + const monitor = await this.db.monitorModule.getHardwareDetailsById({ monitorId, dateRange }); + + return monitor; + }; + + getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { + await this.verifyTeamAccess({ teamId, monitorId }); + const monitor = await this.db.monitorModule.getMonitorById(monitorId); + + return monitor; + }; + + getMonitorsByTeamId = async ({ + teamId, + limit, + type, + page, + rowsPerPage, + filter, + field, + order, + }: { + teamId: string; + limit?: number; + type?: string | string[]; + page?: number; + rowsPerPage?: number; + filter?: string; + field?: string; + order?: "asc" | "desc"; + }): Promise => { const monitors = await this.db.monitorModule.getMonitorsByTeamId({ limit, type, @@ -233,7 +304,15 @@ class MonitorService { return monitors; }; - getMonitorsAndSummaryByTeamId = async ({ teamId, type, explain }) => { + getMonitorsAndSummaryByTeamId = async ({ + teamId, + type, + explain, + }: { + teamId: string; + type?: string | string[]; + explain?: boolean; + }): Promise => { const result = await this.db.monitorModule.getMonitorsAndSummaryByTeamId({ type, explain, @@ -242,7 +321,27 @@ class MonitorService { return result; }; - getMonitorsWithChecksByTeamId = async ({ teamId, limit, type, page, rowsPerPage, filter, field, order, explain }) => { + getMonitorsWithChecksByTeamId = async ({ + teamId, + limit, + type, + page, + rowsPerPage, + filter, + field, + order, + explain, + }: { + teamId: string; + limit?: number; + type?: string | string[]; + page?: number; + rowsPerPage?: number; + filter?: string; + field?: string; + order?: "asc" | "desc"; + explain?: boolean; + }): Promise<{ count: number; monitors: any[] }> => { const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter }); const monitors = await this.monitorsRepository.findByTeamId(teamId, { limit, @@ -254,9 +353,9 @@ class MonitorService { order, }); - const monitorIds = monitors?.map((m) => m.id) ?? []; + const monitorIds = monitors?.map((m: any) => m.id) ?? []; const checksMap = await this.checksRepository.findLatestChecksByMonitorIds(monitorIds); - const monitorsWithChecks = (monitors ?? []).map((monitor) => { + const monitorsWithChecks = (monitors ?? []).map((monitor: any) => { const checks = NormalizeData(checksMap[monitor.id] ?? [], 10, 100); return { ...monitor, @@ -267,14 +366,80 @@ class MonitorService { return { count, monitors: monitorsWithChecks }; }; - exportMonitorsToCSV = async ({ teamId }) => { + getAllGames = (): any => { + return this.games; + }; + + getGroupsByTeamId = async ({ teamId }: { teamId: string }): Promise => { + const groups = await this.db.monitorModule.getGroupsByTeamId({ teamId }); + return groups; + }; + + editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: any }): Promise => { + await this.verifyTeamAccess({ teamId, monitorId }); + const editedMonitor = await this.db.monitorModule.editMonitor({ monitorId, body }); + await this.jobQueue.updateJob(editedMonitor); + }; + + pauseMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { + await this.verifyTeamAccess({ teamId, monitorId }); + const monitor = await this.db.monitorModule.pauseMonitor({ monitorId }); + monitor.isActive === true ? await this.jobQueue.resumeJob(monitor._id, monitor) : await this.jobQueue.pauseJob(monitor); + return monitor; + }; + + deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { + await this.verifyTeamAccess({ teamId, monitorId }); + const monitor = await this.db.monitorModule.deleteMonitor({ teamId, monitorId }); + await this.jobQueue.deleteJob(monitor); + await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor._id); + return monitor; + }; + + deleteAllMonitors = async ({ teamId }: { teamId: string }): Promise => { + const { monitors, deletedCount } = await this.db.monitorModule.deleteAllMonitors(teamId); + await Promise.all( + monitors.map(async (monitor: any) => { + try { + await this.jobQueue.deleteJob(monitor); + await this.db.checkModule.deleteChecks(monitor._id); + await this.db.pageSpeedCheckModule.deletePageSpeedChecksByMonitorId(monitor._id); + await this.db.notificationsModule.deleteNotificationsByMonitorId(monitor._id); + } catch (error: any) { + this.logger.warn({ + message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`, + service: SERVICE_NAME, + method: "deleteAllMonitors", + stack: error.stack, + }); + } + }) + ); + return deletedCount; + }; + + sendTestEmail = async ({ to }: { to: string }): Promise => { + const subject = this.stringService.testEmailSubject; + const context = { testName: "Monitoring System" }; + + const html = await this.emailService.buildEmail("testEmailTemplate", context); + const messageId = await this.emailService.sendEmail(to, subject, html); + + if (!messageId) { + throw this.errorService.createServerError("Failed to send test email."); + } + + return messageId; + }; + + exportMonitorsToCSV = async ({ teamId }: { teamId: string }): Promise => { const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId }); if (!monitors || monitors.length === 0) { throw this.errorService.createNotFoundError("No monitors to export"); } - const csvData = monitors?.filteredMonitors?.map((monitor) => ({ + const csvData = monitors?.filteredMonitors?.map((monitor: any) => ({ name: monitor.name, description: monitor.description, type: monitor.type, @@ -288,7 +453,7 @@ class MonitorService { const csv = this.papaparse.unparse(csvData); return csv; }; - exportMonitorsToJSON = async ({ teamId }) => { + exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise => { const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId }); if (!monitors || monitors.length === 0) { @@ -296,7 +461,7 @@ class MonitorService { } const json = monitors?.filteredMonitors - ?.map((monitor) => { + ?.map((monitor: any) => { const initialType = monitor.type; let parsedType; @@ -328,15 +493,4 @@ class MonitorService { return json; }; - - getAllGames = () => { - return this.games; - }; - - getGroupsByTeamId = async ({ teamId }) => { - const groups = await this.db.monitorModule.getGroupsByTeamId({ teamId }); - return groups; - }; } - -export default MonitorService; diff --git a/server/src/service/index.ts b/server/src/service/index.ts new file mode 100644 index 000000000..4793f055b --- /dev/null +++ b/server/src/service/index.ts @@ -0,0 +1 @@ +export * from "@/service/business/monitorService.js"; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js index a3cdea8e7..a6637d4cc 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js @@ -4,19 +4,20 @@ const SERVICE_NAME = "JobQueue"; class SuperSimpleQueue { static SERVICE_NAME = SERVICE_NAME; - constructor({ envSettings, db, logger, helper }) { + constructor({ envSettings, db, logger, helper, monitorsRepository }) { this.envSettings = envSettings; this.db = db; this.logger = logger; this.helper = helper; + this.monitorsRepository = monitorsRepository; } get serviceName() { return SuperSimpleQueue.SERVICE_NAME; } - static async create({ envSettings, db, logger, helper }) { - const instance = new SuperSimpleQueue({ envSettings, db, logger, helper }); + static async create({ envSettings, db, logger, helper, monitorsRepository }) { + const instance = new SuperSimpleQueue({ envSettings, db, logger, helper, monitorsRepository }); await instance.init(); return instance; } @@ -33,11 +34,11 @@ class SuperSimpleQueue { this.scheduler.start(); this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob()); - const monitors = await this.db.monitorModule.getAllMonitors(); + const monitors = await this.monitorsRepository.findAll(); for (const monitor of monitors) { const randomOffset = Math.floor(Math.random() * 100); setTimeout(() => { - this.addJob(monitor._id, monitor); + this.addJob(monitor.id, monitor); }, randomOffset); } @@ -55,44 +56,44 @@ class SuperSimpleQueue { addJob = async (monitorId, monitor) => { this.scheduler.addJob({ - id: monitorId.toString(), + id: monitorId, template: "monitor-job", repeat: monitor.interval, active: monitor.isActive, - data: monitor.toObject(), + data: monitor, }); }; deleteJob = async (monitor) => { - this.scheduler.removeJob(monitor._id.toString()); + this.scheduler.removeJob(monitor.id); }; pauseJob = async (monitor) => { - const result = this.scheduler.pauseJob(monitor._id.toString()); + const result = this.scheduler.pauseJob(monitor.id); if (result === false) { throw new Error("Failed to resume monitor"); } this.logger.debug({ - message: `Paused monitor ${monitor._id}`, + message: `Paused monitor ${monitor.id}`, service: SERVICE_NAME, method: "pauseJob", }); }; resumeJob = async (monitor) => { - const result = this.scheduler.resumeJob(monitor._id.toString()); + const result = this.scheduler.resumeJob(monitor.id); if (result === false) { throw new Error("Failed to resume monitor"); } this.logger.debug({ - message: `Resumed monitor ${monitor._id}`, + message: `Resumed monitor ${monitor.id}`, service: SERVICE_NAME, method: "resumeJob", }); }; updateJob = async (monitor) => { - this.scheduler.updateJob(monitor._id.toString(), { repeat: monitor.interval, data: monitor.toObject() }); + this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor }); }; shutdown = async () => { diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js index 2833a59f5..3cd6b37a8 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js @@ -27,7 +27,7 @@ class SuperSimpleQueueHelper { getMonitorJob = () => { return async (monitor) => { try { - const monitorId = monitor._id; + const monitorId = monitor.id; const teamId = monitor.teamId; if (!monitorId) { throw new Error("No monitor id"); @@ -62,7 +62,7 @@ class SuperSimpleQueueHelper { message: error.message, service: SERVICE_NAME, method: "getMonitorJob", - details: `Error sending notifications for job ${monitor._id}: ${error.message}`, + details: `Error sending notifications for job ${monitor.id}: ${error.message}`, stack: error.stack, }); }); diff --git a/server/src/service/infrastructure/networkService.js b/server/src/service/infrastructure/networkService.js index d364f373e..2375e7233 100644 --- a/server/src/service/infrastructure/networkService.js +++ b/server/src/service/infrastructure/networkService.js @@ -95,7 +95,7 @@ class NetworkService { } const pingResponse = { - monitorId: monitor._id, + monitorId: monitor.id, type: "ping", status: response.alive, code: 200, @@ -120,9 +120,9 @@ class NetworkService { } async requestHttp(monitor) { - const { url, secret, _id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor; + const { url, secret, id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor; const httpResponse = { - monitorId: _id, + monitorId: id, teamId: teamId, type, }; @@ -298,7 +298,7 @@ class NetworkService { }); const dockerResponse = { - monitorId: monitor._id, + monitorId: monitor.id, type: monitor.type, }; @@ -416,7 +416,7 @@ class NetworkService { code: 200, status: response.success, message: this.stringService.portSuccess, - monitorId: monitor._id, + monitorId: monitor.id, type: monitor.type, responseTime: responseTime, }; @@ -444,7 +444,7 @@ class NetworkService { code: 200, status: true, message: "Success", - monitorId: monitor._id, + monitorId: monitor.id, type: "game", }; diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index 5b64c7419..30a9fc12c 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -10,11 +10,12 @@ class StatusService { * buffer: import("./bufferService.js").BufferService * incidentService: import("../business/incidentService.js").IncidentService * }} - */ constructor({ db, logger, buffer, incidentService }) { + */ constructor({ db, logger, buffer, incidentService, monitorsRepository }) { this.db = db; this.logger = logger; this.buffer = buffer; this.incidentService = incidentService; + this.monitorsRepository = monitorsRepository; } get serviceName() { @@ -23,7 +24,7 @@ class StatusService { async updateRunningStats({ monitor, networkResponse }) { try { - const monitorId = monitor._id; + const monitorId = monitor.id; const { responseTime, status } = networkResponse; // Get stats let stats = await MonitorStats.findOne({ monitorId }); @@ -124,7 +125,7 @@ class StatusService { service: this.SERVICE_NAME, method: "handleIncidentForCheck", message: `Failed to save check immediately for ${errorContext}: ${checkError.message}`, - monitorId: monitor._id, + monitorId: monitor.id, stack: checkError.stack, }); savedCheck = null; @@ -139,7 +140,7 @@ class StatusService { service: this.SERVICE_NAME, method: "handleIncidentForCheck", message: `Failed to add incident to buffer for ${errorContext}: ${incidentError.message}`, - monitorId: monitor._id, + monitorId: monitor.id, action, stack: incidentError.stack, }); @@ -150,7 +151,7 @@ class StatusService { service: this.SERVICE_NAME, method: "handleIncidentForCheck", message: `Error in ${errorContext}: ${error.message}`, - monitorId: monitor?._id, + monitorId: monitor?.id, stack: error.stack, }); } @@ -172,7 +173,8 @@ class StatusService { await this.insertCheck(check); try { const { monitorId, status, code } = networkResponse; - const monitor = await this.db.monitorModule.getMonitorById(monitorId); + + const monitor = await this.monitorsRepository.findById(monitorId); // Update running stats this.updateRunningStats({ monitor, networkResponse }); @@ -198,9 +200,9 @@ class StatusService { // Return early if not enough data points if (monitor.statusWindow.length < monitor.statusWindowSize) { - await monitor.save(); + const updated = await this.monitorsRepository.update(monitor.id, monitor); return { - monitor, + monitor: updated, statusChanged: false, prevStatus, code, @@ -240,14 +242,14 @@ class StatusService { if (monitor.status === false && !statusChanged) { try { - const lastManuallyResolvedIncident = await this.db.incidentModule.getLastManuallyResolvedIncident(monitor._id); + const lastManuallyResolvedIncident = await this.db.incidentModule.getLastManuallyResolvedIncident(monitor.id); let calculatedFailureRate = failureRate; if (lastManuallyResolvedIncident && lastManuallyResolvedIncident.endTime) { try { const checksAfterResolution = await Check.find({ - monitorId: monitor._id, + monitorId: monitor.id, createdAt: { $gt: lastManuallyResolvedIncident.endTime }, }) .sort({ createdAt: 1 }) @@ -267,7 +269,7 @@ class StatusService { service: this.SERVICE_NAME, method: "updateStatus", message: `Failed to query checks after manual resolution: ${checkQueryError.message}`, - monitorId: monitor._id, + monitorId: monitor.id, stack: checkQueryError.stack, }); } @@ -281,17 +283,17 @@ class StatusService { service: this.SERVICE_NAME, method: "updateStatus", message: `Error handling threshold check without status change: ${error.message}`, - monitorId: monitor._id, + monitorId: monitor.id, stack: error.stack, }); } } monitor.status = newStatus; - await monitor.save(); + const updated = await this.monitorsRepository.update(monitor.id, monitor); return { - monitor, + monitor: updated, statusChanged, prevStatus, code, @@ -374,7 +376,7 @@ class StatusService { return { id: audit.id, title: audit.title, - score: typeof audit.score === "number" ? audit.score : audit.score ?? null, + score: typeof audit.score === "number" ? audit.score : (audit.score ?? null), displayValue: audit.displayValue, numericValue: typeof audit.numericValue === "number" ? audit.numericValue : undefined, numericUnit: audit.numericUnit, From e79bf28bdc8b58ff5bdf5180fbc52f89e9acbd0f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 13 Jan 2026 20:20:29 +0000 Subject: [PATCH 198/400] server format --- .../0002_convertChecksToTimeSeries.ts | 22 ++++++++++--------- server/src/db/migration/index.ts | 6 +---- server/src/db/models/Check.ts | 10 ++------- .../service/infrastructure/statusService.js | 2 +- 4 files changed, 16 insertions(+), 24 deletions(-) diff --git a/server/src/db/migration/0002_convertChecksToTimeSeries.ts b/server/src/db/migration/0002_convertChecksToTimeSeries.ts index 46e6fbc97..a6c503804 100644 --- a/server/src/db/migration/0002_convertChecksToTimeSeries.ts +++ b/server/src/db/migration/0002_convertChecksToTimeSeries.ts @@ -19,10 +19,10 @@ const backupAndDropExistingCollection = async () => { return false; } - await db.collection(CHECKS_COLLECTION).aggregate([ - { $match: {} }, - { $out: BACKUP_COLLECTION }, - ]).toArray(); + await db + .collection(CHECKS_COLLECTION) + .aggregate([{ $match: {} }, { $out: BACKUP_COLLECTION }]) + .toArray(); await db.collection(CHECKS_COLLECTION).drop(); return true; }; @@ -40,12 +40,14 @@ const createTimeSeriesCollection = async () => { }); } - await db.collection(CHECKS_COLLECTION).createIndexes([ - { key: { updatedAt: 1 } }, - { key: { "metadata.monitorId": 1, updatedAt: 1 } }, - { key: { "metadata.monitorId": 1, updatedAt: -1 } }, - { key: { "metadata.teamId": 1, updatedAt: -1 } }, - ]); + await db + .collection(CHECKS_COLLECTION) + .createIndexes([ + { key: { updatedAt: 1 } }, + { key: { "metadata.monitorId": 1, updatedAt: 1 } }, + { key: { "metadata.monitorId": 1, updatedAt: -1 } }, + { key: { "metadata.teamId": 1, updatedAt: -1 } }, + ]); }; const migrateBackupData = async () => { diff --git a/server/src/db/migration/index.ts b/server/src/db/migration/index.ts index de2a1dee7..c8e9e7c75 100644 --- a/server/src/db/migration/index.ts +++ b/server/src/db/migration/index.ts @@ -32,11 +32,7 @@ const runMigrations = async (logger?: { info: Function; error: Function }) => { logger?.info({ message: `Completed ${migration.name}`, service: "Migrations" }); } catch (error) { const err = error as Error; - await MigrationModel.findOneAndUpdate( - { name: migration.name }, - { status: "failed", error: err?.message }, - { upsert: true } - ); + await MigrationModel.findOneAndUpdate({ name: migration.name }, { status: "failed", error: err?.message }, { upsert: true }); throw error; } } diff --git a/server/src/db/models/Check.ts b/server/src/db/models/Check.ts index 3ebacd3f6..fe6f16836 100644 --- a/server/src/db/models/Check.ts +++ b/server/src/db/models/Check.ts @@ -1,8 +1,5 @@ import { Schema, model, Types } from "mongoose"; -import { - MonitorTypes, - type MonitorType, -} from "@/types/monitor.js"; +import { MonitorTypes, type MonitorType } from "@/types/monitor.js"; import type { Check, CheckAudits, @@ -25,10 +22,7 @@ type CheckMetadataDocument = Omit & { type: MonitorType; }; -type CheckDocumentBase = Omit< - Check, - "id" | "metadata" | "expiry" | "ackAt" | "createdAt" | "updatedAt" -> & { +type CheckDocumentBase = Omit & { metadata: CheckMetadataDocument; expiry: Date; ackAt?: Date | null; diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index 5b64c7419..3aa1ced52 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -374,7 +374,7 @@ class StatusService { return { id: audit.id, title: audit.title, - score: typeof audit.score === "number" ? audit.score : audit.score ?? null, + score: typeof audit.score === "number" ? audit.score : (audit.score ?? null), displayValue: audit.displayValue, numericValue: typeof audit.numericValue === "number" ? audit.numericValue : undefined, numericUnit: audit.numericUnit, From f763126b38daad0977b20383e2b4add98a8585ee Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 13 Jan 2026 20:20:50 +0000 Subject: [PATCH 199/400] format' --- .../src/Pages/Incidents/Components/IncidentTable/index.jsx | 6 +++++- client/src/Pages/Incidents/index.jsx | 5 ++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx index 78d6d26be..e066049e6 100644 --- a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx @@ -18,7 +18,11 @@ import { useSelector } from "react-redux"; import { useState } from "react"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; -import { useFetchChecksTeam, useFetchChecksByMonitor, useResolveIncident } from "@/Hooks/checkHooks.js"; +import { + useFetchChecksTeam, + useFetchChecksByMonitor, + useResolveIncident, +} from "@/Hooks/checkHooks.js"; import { Button, Typography, useTheme } from "@mui/material"; import { lighten } from "@mui/material/styles"; diff --git a/client/src/Pages/Incidents/index.jsx b/client/src/Pages/Incidents/index.jsx index c8462f649..698fc4a2f 100644 --- a/client/src/Pages/Incidents/index.jsx +++ b/client/src/Pages/Incidents/index.jsx @@ -10,7 +10,10 @@ import { Box, Button } from "@mui/material"; //Utils import { useTheme } from "@emotion/react"; import { useFetchMonitorsByTeamId } from "@/Hooks/monitorHooks.js"; -import { useFetchChecksSummaryByTeamId, useAcknowledgeChecks } from "@/Hooks/checkHooks.js"; +import { + useFetchChecksSummaryByTeamId, + useAcknowledgeChecks, +} from "@/Hooks/checkHooks.js"; import { useState, useEffect } from "react"; import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; import { useTranslation } from "react-i18next"; From 0a60cc0363b06b55314308ba2c0f4df513493e21 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 13 Jan 2026 21:44:16 +0000 Subject: [PATCH 200/400] check repo fix --- .../checks/MongoChecksRepistory.ts | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index d04118da0..f5c398fc8 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -173,28 +173,33 @@ class MongoChecksRepistory implements IChecksRepository { }; findLatestChecksByMonitorIds = async (monitorIds: string[]): Promise => { + if (monitorIds.length === 0) { + return {}; + } const mongoIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); - const checkMap = await CheckModel.aggregate([ + const limitPerMonitor = 25; + const checkGroups = await CheckModel.aggregate([ { $match: { "metadata.monitorId": { $in: mongoIds }, }, }, - { $sort: { createdAt: -1 } }, { $group: { _id: "$metadata.monitorId", - latestChecks: { $push: "$$ROOT" }, - }, - }, - { - $project: { - latestChecks: { $slice: [{ $ifNull: ["$latestChecks", []] }, 25] }, + latestChecks: { + $topN: { + n: limitPerMonitor, + sortBy: { createdAt: -1 }, + output: "$$ROOT", + }, + }, }, }, ]); - return checkMap.reduce((acc, cm) => { - acc[cm._id.toString()] = cm.latestChecks.map((c: CheckDocument) => this.toEntity(c)); + return checkGroups.reduce((acc, group) => { + const monitorId = group._id.toString(); + acc[monitorId] = (group.latestChecks ?? []).map((doc: CheckDocument) => this.toEntity(doc)); return acc; }, {}); }; From 8de06df8c41089d3c3a0953f508fc3f3b696df28 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 13 Jan 2026 14:45:54 -0800 Subject: [PATCH 201/400] add index and cutof --- server/src/db/models/Check.ts | 2 ++ server/src/repositories/checks/MongoChecksRepistory.ts | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/server/src/db/models/Check.ts b/server/src/db/models/Check.ts index fe6f16836..c176aa3bb 100644 --- a/server/src/db/models/Check.ts +++ b/server/src/db/models/Check.ts @@ -288,7 +288,9 @@ const CheckSchema = new Schema( CheckSchema.index({ updatedAt: 1 }); CheckSchema.index({ "metadata.monitorId": 1, updatedAt: 1 }); CheckSchema.index({ "metadata.monitorId": 1, updatedAt: -1 }); +CheckSchema.index({ "metadata.monitorId": 1, createdAt: -1 }); CheckSchema.index({ "metadata.teamId": 1, updatedAt: -1 }); +CheckSchema.index({ "metadata.teamId": 1, createdAt: -1 }); const CheckModel = model("Check", CheckSchema); diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index f5c398fc8..de26efff4 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -178,10 +178,15 @@ class MongoChecksRepistory implements IChecksRepository { } const mongoIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); const limitPerMonitor = 25; + const maxIntervalMs = Number(process.env.MONITOR_MAX_INTERVAL_MS ?? 10 * 60 * 1000); + const bufferMs = Number(process.env.MONITOR_INTERVAL_BUFFER_MS ?? maxIntervalMs); + const lookbackMs = 25 * maxIntervalMs + bufferMs; + const cutoffDate = new Date(Date.now() - lookbackMs); const checkGroups = await CheckModel.aggregate([ { $match: { "metadata.monitorId": { $in: mongoIds }, + createdAt: { $gte: cutoffDate }, }, }, { From 8a986d2e58e89e5b763123105a2387d3d4544ab5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 13 Jan 2026 22:56:51 +0000 Subject: [PATCH 202/400] add indexes --- server/src/controllers/monitorController.ts | 2 +- server/src/db/models/Check.ts | 2 ++ .../checks/MongoChecksRepistory.ts | 6 +++++ .../monitors/IMonitorsRepository.ts | 1 + .../monitors/MongoMonitorsRepository.ts | 6 +++++ server/src/service/business/monitorService.ts | 22 ++++++++++--------- 6 files changed, 28 insertions(+), 11 deletions(-) diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index ba0a237bf..9c117e9af 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -231,7 +231,7 @@ class MonitorController { throw new AppError({ message: "Cannot get file from buffer", status: 400 }); } - const monitors = await this.monitorService.createBulkMonitors({ fileData, userId, teamId }); + const monitors = await this.monitorService.createBulkMonitors(fileData, userId, teamId); return res.status(200).json({ success: true, diff --git a/server/src/db/models/Check.ts b/server/src/db/models/Check.ts index fe6f16836..c176aa3bb 100644 --- a/server/src/db/models/Check.ts +++ b/server/src/db/models/Check.ts @@ -288,7 +288,9 @@ const CheckSchema = new Schema( CheckSchema.index({ updatedAt: 1 }); CheckSchema.index({ "metadata.monitorId": 1, updatedAt: 1 }); CheckSchema.index({ "metadata.monitorId": 1, updatedAt: -1 }); +CheckSchema.index({ "metadata.monitorId": 1, createdAt: -1 }); CheckSchema.index({ "metadata.teamId": 1, updatedAt: -1 }); +CheckSchema.index({ "metadata.teamId": 1, createdAt: -1 }); const CheckModel = model("Check", CheckSchema); diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index f5c398fc8..10b482fb2 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -178,10 +178,15 @@ class MongoChecksRepistory implements IChecksRepository { } const mongoIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); const limitPerMonitor = 25; + const maxIntervalMs = Number(10 * 60 * 1000); + const bufferMs = Number(maxIntervalMs); + const lookbackMs = 25 * maxIntervalMs + bufferMs; + const cutoffDate = new Date(Date.now() - lookbackMs); const checkGroups = await CheckModel.aggregate([ { $match: { "metadata.monitorId": { $in: mongoIds }, + createdAt: { $gte: cutoffDate }, }, }, { @@ -197,6 +202,7 @@ class MongoChecksRepistory implements IChecksRepository { }, }, ]); + return checkGroups.reduce((acc, group) => { const monitorId = group._id.toString(); acc[monitorId] = (group.latestChecks ?? []).map((doc: CheckDocument) => this.toEntity(doc)); diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 99131cb4b..fc69d5740 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -13,6 +13,7 @@ export interface TeamQueryConfig { export interface IMonitorsRepository { // create create(monitor: Monitor, teamId: string, userId: string): Promise; + createBulkMonitors(monitors: Monitor[]): Promise; // single fetch findById(monitorId: string): Promise; diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 49687c339..383442ac1 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -12,6 +12,12 @@ class MongoMonitorsRepository implements IMonitorsRepository { return this.toEntity(saved); }; + createBulkMonitors = async (monitors: Monitor[]): Promise => { + const newMonitors = monitors.map((monitor) => new MonitorModel({ ...monitor, notifications: undefined })); + await MonitorModel.bulkSave(newMonitors); + return monitors; + }; + findById = async (MonitorModelId: string): Promise => { const monitor = await MonitorModel.findById(MonitorModelId); if (!monitor) { diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 45a2fc235..0f027b5b8 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -1,8 +1,10 @@ import { createMonitorsBodyValidation } from "@/validation/joi.js"; import { NormalizeData } from "@/utils/dataUtils.js"; import { type Monitor } from "@/types/index.js"; +import type { MonitorType } from "@/types/monitor.js"; import type { IMonitorsRepository } from "@/repositories/index.js"; import { AppError } from "../infrastructure/errorService.js"; + const SERVICE_NAME = "MonitorService"; export interface IMonitorService { @@ -11,7 +13,7 @@ export interface IMonitorService { // create createMonitor(teamId: string, userId: string, body: Monitor): Promise; - createBulkMonitors(args: { fileData: string; userId: string; teamId: string }): Promise; + createBulkMonitors(fileData: string, userId: string, teamId: string): Promise; addDemoMonitors(args: { userId: string; teamId: string }): Promise; // read @@ -31,7 +33,7 @@ export interface IMonitorService { getMonitorsByTeamId(args: { teamId: string; limit?: number; - type?: string | string[]; + type?: MonitorType | MonitorType[]; page?: number; rowsPerPage?: number; filter?: string; @@ -42,7 +44,7 @@ export interface IMonitorService { getMonitorsWithChecksByTeamId(args: { teamId: string; limit?: number; - type?: string | string[]; + type?: MonitorType | MonitorType[]; page?: number; rowsPerPage?: number; filter?: string; @@ -136,7 +138,7 @@ export class MonitorService implements IMonitorService { this.jobQueue.addJob(monitor.id, monitor); }; - createBulkMonitors = async ({ fileData, userId, teamId }: { fileData: string; userId: string; teamId: string }): Promise => { + createBulkMonitors = async (fileData: string, userId: string, teamId: string): Promise => { const { parse } = this.papaparse; return new Promise((resolve, reject) => { @@ -167,10 +169,10 @@ export class MonitorService implements IMonitorService { throw this.errorService.createServerError("CSV file contains no data rows"); } - const enrichedData = data.map((monitor: any) => ({ + const enrichedData = data.map((monitor: Monitor) => ({ + ...monitor, userId, teamId, - ...monitor, description: monitor.description || monitor.name || monitor.url, name: monitor.name || monitor.url, type: monitor.type || "http", @@ -178,11 +180,11 @@ export class MonitorService implements IMonitorService { await createMonitorsBodyValidation.validateAsync(enrichedData); - const monitors = await this.db.monitorModule.createBulkMonitors(enrichedData); + const monitors = await this.monitorsRepository.createBulkMonitors(enrichedData); await Promise.all( - monitors.map(async (monitor: any) => { - this.jobQueue.addJob(monitor._id, monitor); + monitors.map(async (monitor: Monitor) => { + this.jobQueue.addJob(monitor.id, monitor); }) ); @@ -334,7 +336,7 @@ export class MonitorService implements IMonitorService { }: { teamId: string; limit?: number; - type?: string | string[]; + type?: MonitorType | MonitorType[]; page?: number; rowsPerPage?: number; filter?: string; From 77764c51eecccb765c9cbfb8cc0165b6feeb8efa Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 13 Jan 2026 23:22:34 +0000 Subject: [PATCH 203/400] addDemo --- .../monitors/IMonitorsRepository.ts | 1 + .../monitors/MongoMonitorsRepository.ts | 16 ++++++-- server/src/service/business/monitorService.ts | 37 +++++++++++++++---- 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index fc69d5740..4d76d49db 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -23,6 +23,7 @@ export interface IMonitorsRepository { // update update(monitorId: string, updates: Partial): Promise; // delete + deleteByTeamId(teamId: string): Promise<{ monitors: Monitor[]; deletedCount: number }>; // counts findMonitorCountByTeamIdAndType(teamId: string, config: TeamQueryConfig): Promise; diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 383442ac1..96795569b 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -13,9 +13,12 @@ class MongoMonitorsRepository implements IMonitorsRepository { }; createBulkMonitors = async (monitors: Monitor[]): Promise => { - const newMonitors = monitors.map((monitor) => new MonitorModel({ ...monitor, notifications: undefined })); - await MonitorModel.bulkSave(newMonitors); - return monitors; + if (!monitors.length) { + return []; + } + const payload = monitors.map((monitor) => ({ ...monitor, notifications: undefined })); + const inserted = await MonitorModel.insertMany(payload, { ordered: false }); + return this.mapDocuments(inserted); }; findById = async (MonitorModelId: string): Promise => { @@ -106,6 +109,13 @@ class MongoMonitorsRepository implements IMonitorsRepository { return this.toEntity(updatedMonitor); }; + deleteByTeamId = async (teamId: string) => { + const monitors = await MonitorModel.find({ teamId }); + const { deletedCount } = await MonitorModel.deleteMany({ teamId }); + + return { monitors: this.mapDocuments(monitors), deletedCount }; + }; + private mapDocuments = (documents: MonitorDocument[]): Monitor[] => { if (!documents?.length) { return []; diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 0f027b5b8..004b9f25f 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -3,6 +3,10 @@ import { NormalizeData } from "@/utils/dataUtils.js"; import { type Monitor } from "@/types/index.js"; import type { MonitorType } from "@/types/monitor.js"; import type { IMonitorsRepository } from "@/repositories/index.js"; +import fs from "fs"; +import { fileURLToPath } from "url"; +import path from "path"; + import { AppError } from "../infrastructure/errorService.js"; const SERVICE_NAME = "MonitorService"; @@ -82,6 +86,7 @@ export class MonitorService implements IMonitorService { private games: any; private monitorsRepository: IMonitorsRepository; private checksRepository: any; + private fs: any; constructor({ db, @@ -198,9 +203,25 @@ export class MonitorService implements IMonitorService { }; addDemoMonitors = async ({ userId, teamId }: { userId: string; teamId: string }): Promise => { - const demoMonitors = await this.db.monitorModule.addDemoMonitors(userId, teamId); + const __filename = fileURLToPath(import.meta.url); + const __dirname = path.dirname(__filename); + const demoMonitorsPath = path.resolve(__dirname, "../../utils/demoMonitors.json"); - await Promise.all(demoMonitors.map((monitor: any) => this.jobQueue.addJob(monitor._id, monitor))); + const demoData = JSON.parse(fs.readFileSync(demoMonitorsPath, "utf8")); + const monitors: Monitor[] = demoData.map((monitor: Monitor) => { + return { + userId, + teamId, + name: monitor.name, + description: monitor.name, + type: "http", + url: monitor.url, + interval: 60000, + }; + }); + const demoMonitors = await this.monitorsRepository.createBulkMonitors(monitors); + + await Promise.all(demoMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor))); return demoMonitors; }; @@ -399,17 +420,17 @@ export class MonitorService implements IMonitorService { }; deleteAllMonitors = async ({ teamId }: { teamId: string }): Promise => { - const { monitors, deletedCount } = await this.db.monitorModule.deleteAllMonitors(teamId); + const { monitors, deletedCount } = await this.monitorsRepository.deleteByTeamId(teamId); await Promise.all( - monitors.map(async (monitor: any) => { + monitors.map(async (monitor) => { try { await this.jobQueue.deleteJob(monitor); - await this.db.checkModule.deleteChecks(monitor._id); - await this.db.pageSpeedCheckModule.deletePageSpeedChecksByMonitorId(monitor._id); - await this.db.notificationsModule.deleteNotificationsByMonitorId(monitor._id); + await this.db.checkModule.deleteChecks(monitor.id); + await this.db.pageSpeedCheckModule.deletePageSpeedChecksByMonitorId(monitor.id); + await this.db.notificationsModule.deleteNotificationsByMonitorId(monitor.id); } catch (error: any) { this.logger.warn({ - message: `Error deleting associated records for monitor ${monitor._id} with name ${monitor.name}`, + message: `Error deleting associated records for monitor ${monitor.id} with name ${monitor.name}`, service: SERVICE_NAME, method: "deleteAllMonitors", stack: error.stack, From 76fa99f7bd7ade0f4691fe8c13b6d0fb498ff143 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 13 Jan 2026 23:59:09 +0000 Subject: [PATCH 204/400] getUptimeDetails --- server/src/controllers/monitorController.ts | 13 --- server/src/db/modules/monitorModule.js | 11 -- .../repositories/checks/IChecksRepository.ts | 17 ++- .../checks/MongoChecksRepistory.ts | 100 ++++++++++++++++++ .../monitors/IMonitorsRepository.ts | 2 +- .../monitors/MongoMonitorsRepository.ts | 10 +- server/src/routes/v1/monitorRoute.ts | 1 - server/src/service/business/monitorService.ts | 69 +++++++++--- 8 files changed, 175 insertions(+), 48 deletions(-) diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 9c117e9af..527fd334d 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -39,19 +39,6 @@ class MonitorController { } } - getAllMonitors = async (req: Request, res: Response, next: NextFunction) => { - try { - const monitors = await this.monitorService.getAllMonitors(); - return res.status(200).json({ - success: true, - msg: "Retrieved all monitors successfully", - data: monitors, - }); - } catch (error) { - next(error); - } - }; - getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => { try { await getCertificateParamValidation.validateAsync(req.params); diff --git a/server/src/db/modules/monitorModule.js b/server/src/db/modules/monitorModule.js index 9c8ca7e32..08b8e1371 100755 --- a/server/src/db/modules/monitorModule.js +++ b/server/src/db/modules/monitorModule.js @@ -194,17 +194,6 @@ class MonitorModule { }; }; - getAllMonitors = async () => { - try { - const monitors = await this.Monitor.find(); - return monitors; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getAllMonitors"; - throw error; - } - }; - getMonitorById = async (monitorId) => { try { const monitor = await this.Monitor.findById(monitorId); diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index df69cf18a..7741c071b 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -1,10 +1,17 @@ import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js"; export interface IChecksRepository { - // create - // single fetch - // collection fetch findLatestChecksByMonitorIds(monitorIds: string[]): Promise; - // update - // delete + findDateRangeChecksByMonitor( + monitorId: string, + startDate: Date, + endDate: Date, + dateString: string + ): Promise<{ + groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>; + groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; + groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; + uptimePercentage: number; + avgResponseTime: number; + }>; } diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 10b482fb2..76d3658cd 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -209,6 +209,106 @@ class MongoChecksRepistory implements IChecksRepository { return acc; }, {}); }; + + findDateRangeChecksByMonitor = async ( + monitorId: string, + startDate: Date, + endDate: Date, + dateString: string + ): Promise<{ + groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>; + groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; + groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; + uptimePercentage: number; + avgResponseTime: number; + }> => { + const matchStage = { + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + updatedAt: { $gte: startDate, $lte: endDate }, + }; + const [result] = await CheckModel.aggregate([ + { $match: matchStage }, + { $sort: { updatedAt: 1 } }, + { + $facet: { + uptimePercentage: [ + { + $group: { + _id: null, + upChecks: { $sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] } }, + totalChecks: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + percentage: { + $cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }], + }, + }, + }, + ], + groupedAvgResponseTime: [ + { + $group: { + _id: null, + avgResponseTime: { $avg: "$responseTime" }, + }, + }, + ], + groupedChecks: [ + { + $group: { + _id: { + $dateToString: { format: dateString, date: "$createdAt" }, + }, + avgResponseTime: { $avg: "$responseTime" }, + totalChecks: { $sum: 1 }, + }, + }, + { $sort: { _id: 1 } }, + ], + groupedUpChecks: [ + { $match: { status: true } }, + { + $group: { + _id: { + $dateToString: { format: dateString, date: "$createdAt" }, + }, + totalChecks: { $sum: 1 }, + avgResponseTime: { $avg: "$responseTime" }, + }, + }, + { $sort: { _id: 1 } }, + ], + groupedDownChecks: [ + { $match: { status: false } }, + { + $group: { + _id: { + $dateToString: { format: dateString, date: "$createdAt" }, + }, + totalChecks: { $sum: 1 }, + avgResponseTime: { $avg: "$responseTime" }, + }, + }, + { $sort: { _id: 1 } }, + ], + }, + }, + ]).exec(); + + const uptimePercentage = result?.uptimePercentage?.[0]?.percentage ?? 0; + const avgResponseTime = result?.groupedAvgResponseTime?.[0]?.avgResponseTime ?? 0; + + return { + groupedChecks: result?.groupedChecks ?? [], + groupedUpChecks: result?.groupedUpChecks ?? [], + groupedDownChecks: result?.groupedDownChecks ?? [], + uptimePercentage, + avgResponseTime, + }; + }; } export default MongoChecksRepistory; diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 4d76d49db..0da9c7279 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -15,7 +15,7 @@ export interface IMonitorsRepository { create(monitor: Monitor, teamId: string, userId: string): Promise; createBulkMonitors(monitors: Monitor[]): Promise; // single fetch - findById(monitorId: string): Promise; + findById(monitorId: string, teamId?: string): Promise; // collection fetch findAll(): Promise; diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 96795569b..c9af6668d 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -21,12 +21,16 @@ class MongoMonitorsRepository implements IMonitorsRepository { return this.mapDocuments(inserted); }; - findById = async (MonitorModelId: string): Promise => { - const monitor = await MonitorModel.findById(MonitorModelId); + findById = async (monitorId: string, teamId?: string): Promise => { + const match: { _id: string; teamId?: string } = { _id: monitorId }; + if (teamId) { + match.teamId = teamId; + } + const monitor = await MonitorModel.findOne(match); if (!monitor) { if (monitor === null || monitor === undefined) { throw new AppError({ - message: `Monitor with ID ${MonitorModelId} not found.`, + message: `Monitor with ID ${monitorId} not found.`, status: 404, }); } diff --git a/server/src/routes/v1/monitorRoute.ts b/server/src/routes/v1/monitorRoute.ts index e68e0019f..a355efbb9 100755 --- a/server/src/routes/v1/monitorRoute.ts +++ b/server/src/routes/v1/monitorRoute.ts @@ -38,7 +38,6 @@ class MonitorRoutes { }); // General monitor CRUD routes - this.router.get("/", this.monitorController.getAllMonitors); this.router.post("/", isAllowed(["admin", "superadmin"]), this.monitorController.createMonitor); this.router.delete("/", isAllowed(["superadmin"]), this.monitorController.deleteAllMonitors); diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 004b9f25f..b189f9077 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -1,8 +1,8 @@ import { createMonitorsBodyValidation } from "@/validation/joi.js"; -import { NormalizeData } from "@/utils/dataUtils.js"; +import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js"; import { type Monitor } from "@/types/index.js"; import type { MonitorType } from "@/types/monitor.js"; -import type { IMonitorsRepository } from "@/repositories/index.js"; +import type { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js"; import fs from "fs"; import { fileURLToPath } from "url"; import path from "path"; @@ -10,6 +10,7 @@ import path from "path"; import { AppError } from "../infrastructure/errorService.js"; const SERVICE_NAME = "MonitorService"; +type DateRangeKey = "recent" | "day" | "week" | "month" | "all"; export interface IMonitorService { readonly serviceName: string; @@ -21,7 +22,6 @@ export interface IMonitorService { addDemoMonitors(args: { userId: string; teamId: string }): Promise; // read - getAllMonitors(): Promise; getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise; getMonitorStatsById(args: { teamId: string; @@ -85,7 +85,7 @@ export class MonitorService implements IMonitorService { private errorService: any; private games: any; private monitorsRepository: IMonitorsRepository; - private checksRepository: any; + private checksRepository: IChecksRepository; private fs: any; constructor({ @@ -109,7 +109,7 @@ export class MonitorService implements IMonitorService { errorService: any; games: any; monitorsRepository: IMonitorsRepository; - checksRepository: any; + checksRepository: IChecksRepository; }) { this.db = db; this.jobQueue = jobQueue; @@ -127,6 +127,31 @@ export class MonitorService implements IMonitorService { return MonitorService.SERVICE_NAME; } + private getDateRange = (dateRange: DateRangeKey) => { + const startDates = { + recent: new Date(new Date().setHours(new Date().getHours() - 2)), + day: new Date(new Date().setDate(new Date().getDate() - 1)), + week: new Date(new Date().setDate(new Date().getDate() - 7)), + month: new Date(new Date().setMonth(new Date().getMonth() - 1)), + all: new Date(0), + }; + return { + start: startDates[dateRange], + end: new Date(), + }; + }; + + private getDateFormat = (dateRange: DateRangeKey): string => { + const formatLookup = { + recent: "%Y-%m-%dT%H:%M:00Z", + day: "%Y-%m-%dT%H:00:00Z", + week: "%Y-%m-%dT00:00:00Z", + month: "%Y-%m-%dT00:00:00Z", + all: "%Y-%m-%dT00:00:00Z", + }; + return formatLookup[dateRange]; + }; + verifyTeamAccess = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { const monitor = await this.db.monitorModule.getMonitorById(monitorId); if (!monitor?.teamId?.equals(teamId)) { @@ -225,11 +250,6 @@ export class MonitorService implements IMonitorService { return demoMonitors; }; - getAllMonitors = async (): Promise => { - const monitors = await this.db.monitorModule.getAllMonitors(); - return monitors; - }; - getUptimeDetailsById = async ({ teamId, monitorId, @@ -242,13 +262,34 @@ export class MonitorService implements IMonitorService { normalize?: boolean; }): Promise => { await this.verifyTeamAccess({ teamId, monitorId }); - const data = await this.db.monitorModule.getUptimeDetailsById({ + + const monitor = await this.monitorsRepository.findById(monitorId, teamId); + if (!monitor) { + throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); + } + const rangeKey = (dateRange as DateRangeKey) ?? "recent"; + const { start, end } = this.getDateRange(rangeKey); + const checksData = await this.checksRepository.findDateRangeChecksByMonitor( + monitorId: monitor.id, + startDate: start, + endDate: end, + dateString: this.getDateFormat(rangeKey), + ); + const monitorStats = await this.db.monitorModule.getMonitorStatsById({ monitorId, - dateRange, - normalize, }); - return data; + return { + monitorData: { + monitor, + groupedChecks: NormalizeDataUptimeDetails(checksData.groupedChecks, 10, 100), + groupedUpChecks: NormalizeDataUptimeDetails(checksData.groupedUpChecks, 10, 100), + groupedDownChecks: NormalizeDataUptimeDetails(checksData.groupedDownChecks, 10, 100), + groupedAvgResponseTime: checksData.avgResponseTime, + groupedUptimePercentage: checksData.uptimePercentage, + }, + monitorStats, + }; }; getMonitorStatsById = async ({ From d1faeab042d3fc9ab33d853616284fa9d667a077 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 14 Jan 2026 00:04:22 +0000 Subject: [PATCH 205/400] fix params --- server/src/service/business/monitorService.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index b189f9077..3e4d39ade 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -269,12 +269,7 @@ export class MonitorService implements IMonitorService { } const rangeKey = (dateRange as DateRangeKey) ?? "recent"; const { start, end } = this.getDateRange(rangeKey); - const checksData = await this.checksRepository.findDateRangeChecksByMonitor( - monitorId: monitor.id, - startDate: start, - endDate: end, - dateString: this.getDateFormat(rangeKey), - ); + const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey)); const monitorStats = await this.db.monitorModule.getMonitorStatsById({ monitorId, }); From 665eb9a67fce1fce0ce2cc8eb2c9813a468992e8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 14 Jan 2026 19:28:17 +0000 Subject: [PATCH 206/400] hardware details --- server/jest.config.ts | 20 + server/package-lock.json | 3663 +++++++++++++++-- server/package.json | 14 +- server/scripts/generate-checks.js | 176 + .../src/config/{services.js => services.ts} | 14 +- server/src/controllers/monitorController.ts | 35 - server/src/db/MongoDB.js | 6 - server/src/db/models/MonitorStats.js | 49 - server/src/db/models/MonitorStats.ts | 63 + server/src/db/models/index.ts | 3 + server/src/db/modules/monitorModule.js | 42 - .../repositories/checks/IChecksRepository.ts | 58 +- .../checks/MongoChecksRepistory.ts | 85 +- server/src/repositories/index.ts | 3 + .../monitor-stats/IMonitorStatsRepository.ts | 9 + .../MongoMonitorStatsRepository.ts | 44 + server/src/routes/v1/monitorRoute.ts | 1 - server/src/service/business/monitorService.ts | 90 +- .../service/infrastructure/statusService.js | 6 +- server/src/types/index.ts | 1 + server/src/types/monitorStats.ts | 14 + server/src/validation/joi.js | 14 - server/test/monitorService.test.ts | 218 + server/tsconfig.jest.json | 9 + server/tsconfig.json | 3 +- 25 files changed, 4050 insertions(+), 590 deletions(-) create mode 100644 server/jest.config.ts create mode 100644 server/scripts/generate-checks.js rename server/src/config/{services.js => services.ts} (94%) delete mode 100755 server/src/db/models/MonitorStats.js create mode 100755 server/src/db/models/MonitorStats.ts create mode 100644 server/src/repositories/monitor-stats/IMonitorStatsRepository.ts create mode 100644 server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts create mode 100644 server/src/types/monitorStats.ts create mode 100644 server/test/monitorService.test.ts create mode 100644 server/tsconfig.jest.json diff --git a/server/jest.config.ts b/server/jest.config.ts new file mode 100644 index 000000000..2bcfcbf21 --- /dev/null +++ b/server/jest.config.ts @@ -0,0 +1,20 @@ +import type { Config } from "jest"; + +const config: Config = { + rootDir: ".", + testEnvironment: "node", + extensionsToTreatAsEsm: [".ts"], + transform: { + "^.+\\.(t|j)sx?$": ["ts-jest", { useESM: true, tsconfig: "./tsconfig.jest.json" }], + }, + moduleNameMapper: { + "^@/(.*)$": "/src/$1", + }, + testMatch: ["/test/**/*.test.ts"], + setupFilesAfterEnv: [], + collectCoverageFrom: ["src/**/*.ts"], + coveragePathIgnorePatterns: ["/node_modules/", "/test/"], + clearMocks: true, +}; + +export default config; diff --git a/server/package-lock.json b/server/package-lock.json index d254da407..11e1829b4 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -46,21 +46,27 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/dockerode": "^4.0.0", "@types/express": "5.0.3", + "@types/gamedig": "^5.0.3", + "@types/jest": "^30.0.0", + "@types/jmespath": "^0.15.2", "@types/jsonwebtoken": "9.0.10", + "@types/mjml": "^4.7.4", "@types/multer": "^2.0.0", "@types/nodemailer": "7.0.1", + "@types/papaparse": "^5.5.2", "@types/ping": "0.4.4", "c8": "10.1.3", - "chai": "5.2.0", "eslint": "^9.17.0", "eslint-plugin-mocha": "^10.5.0", "esm": "3.2.25", "globals": "^15.14.0", - "mocha": "11.1.0", + "jest": "^30.2.0", "nodemon": "^3.1.11", "prettier": "^3.3.3", - "sinon": "19.0.2", + "ts-jest": "^29.4.6", + "ts-node": "^10.9.2", "tsc-alias": "1.8.16", "tsx": "4.20.5", "typescript": "5.9.2" @@ -834,12 +840,12 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.28.6.tgz", + "integrity": "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q==", "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" }, @@ -847,6 +853,174 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/compat-data": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.6.tgz", + "integrity": "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.6.tgz", + "integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.6.tgz", + "integrity": "sha512-lOoVRwADj8hjf7al89tvQ2a1lf53Z+7tiXMgpZJL3maQPDxh0DgLMN62B2MKUOFcoodBHLMbDM6WAbKgNy5Suw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-validator-identifier": { "version": "7.28.5", "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", @@ -856,6 +1030,285 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.6.tgz", + "integrity": "sha512-TeR9zWR18BvbfPmGbLampPMW+uW1NZnJlRuuHso8i87QZNq2JRF9i6RgxRqtEq+wQGsS19NNTWr2duhnE49mfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, "node_modules/@babel/runtime": { "version": "7.28.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", @@ -865,6 +1318,54 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.6.tgz", + "integrity": "sha512-fgWX62k02qtjqdSNTAGxmKYY/7FSL9WAS1o2Hu5+I5m9T0yxZzr4cnrfXQ/MX0rIifthCSs6FKTlzYbJcPtMNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/generator": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.6", + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.6.tgz", + "integrity": "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@balena/dockerignore": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@balena/dockerignore/-/dockerignore-1.0.2.tgz", @@ -890,6 +1391,30 @@ "node": ">=0.1.90" } }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@cspotcode/source-map-support/node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, "node_modules/@csstools/color-helpers": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", @@ -1035,6 +1560,18 @@ "kuler": "^2.0.0" } }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/runtime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.6.0.tgz", @@ -1045,6 +1582,17 @@ "tslib": "^2.4.0" } }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.25.11", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz", @@ -2144,6 +2692,113 @@ "node": ">=12" } }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -2154,6 +2809,377 @@ "node": ">=8" } }, + "node_modules/@jest/console": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz", + "integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz", + "integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.2.0", + "jest-config": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-resolve-dependencies": "30.2.0", + "jest-runner": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "jest-watcher": "30.2.0", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.2.0", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz", + "integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/types": "30.2.0", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz", + "integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz", + "integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz", + "integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/types": "30.2.0", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz", + "integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz", + "integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.2.0", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.1", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, "node_modules/@jridgewell/resolve-uri": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", @@ -2279,6 +3305,19 @@ "win32" ] }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -2327,6 +3366,19 @@ "node": ">=14" } }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -2439,6 +3491,13 @@ "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", "license": "BSD-3-Clause" }, + "node_modules/@sinclair/typebox": { + "version": "0.34.47", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.47.tgz", + "integrity": "sha512-ZGIBQ+XDvO5JQku9wmwtabcVTHJsgSWAHYtVuM9pBNNR5E88v6Jcj/llpmsjivig5X8A8HHOb4/mbEKPS5EvAw==", + "dev": true, + "license": "MIT" + }, "node_modules/@sindresorhus/is": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.1.0.tgz", @@ -2471,34 +3530,6 @@ "@sinonjs/commons": "^3.0.1" } }, - "node_modules/@sinonjs/samsam": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.3.tgz", - "integrity": "sha512-hw6HbX+GyVZzmaYNh82Ecj1vdGZrqVIn/keDTg63IgAwiQPO+xCz99uG6Woqgb4tM0mUiFENKZ4cqd7IX94AXQ==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "type-detect": "^4.1.0" - } - }, - "node_modules/@sinonjs/samsam/node_modules/type-detect": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", - "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@sinonjs/text-encoding": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.3.tgz", - "integrity": "sha512-DE427ROAphMQzU4ENbliGYrBSYPXF+TtLg9S8vzeA+OF4ZKzoDdzfL8sxuMUGS/lgRhM6j1URSk9ghf7Xo1tyA==", - "dev": true, - "license": "(Unlicense OR Apache-2.0)" - }, "node_modules/@smithy/abort-controller": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.2.3.tgz", @@ -3197,6 +4228,90 @@ "node": ">=10.13.0" } }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, "node_modules/@types/body-parser": { "version": "1.19.6", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", @@ -3218,6 +4333,29 @@ "@types/node": "*" } }, + "node_modules/@types/docker-modem": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", + "integrity": "sha512-yKpAGEuKRSS8wwx0joknWxsmLha78wNMe9R2S3UNsVOkZded8UqOrV8KoeDXoXsjndxwyF3eIhyClGbO1SEhEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/ssh2": "*" + } + }, + "node_modules/@types/dockerode": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/dockerode/-/dockerode-4.0.0.tgz", + "integrity": "sha512-NBm/UIL1KYWivh9ihJELK4Jv0dzOQCleS0OT5JklErra8mZ/t0UQpHfTkMmPrAqtra7OyFius1EFLVs0DhiHQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/docker-modem": "*", + "@types/node": "*", + "@types/ssh2": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3250,6 +4388,13 @@ "@types/send": "*" } }, + "node_modules/@types/gamedig": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@types/gamedig/-/gamedig-5.0.3.tgz", + "integrity": "sha512-VLrOVRf72wVE6P0DhFtRVodiONwL51uojn6dNpP/Ks/Vdm3CDH6dZTAw74WmCDLuyvpytNy8auKzlTKfBlO8jg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/http-cache-semantics": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", @@ -3270,6 +4415,44 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/jmespath": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@types/jmespath/-/jmespath-0.15.2.tgz", + "integrity": "sha512-pegh49FtNsC389Flyo9y8AfkVIZn9MMPE9yJrO9svhq6Fks2MwymULWjZqySuxmctd3ZH4/n7Mr98D+1Qo5vGA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -3295,6 +4478,23 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mjml": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/@types/mjml/-/mjml-4.7.4.tgz", + "integrity": "sha512-vyi1vzWgMzFMwZY7GSZYX0GU0dmtC8vLHwpgk+NWmwbwRSrlieVyJ9sn5elodwUfklJM7yGl0zQeet1brKTWaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mjml-core": "*" + } + }, + "node_modules/@types/mjml-core": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/@types/mjml-core/-/mjml-core-4.15.2.tgz", + "integrity": "sha512-Q7SxFXgoX979HP57DEVsRI50TV8x1V4lfCA4Up9AvfINDM5oD/X9ARgfoyX1qS987JCnDLv85JjkqAjt3hZSiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", @@ -3317,6 +4517,7 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3332,6 +4533,16 @@ "@types/node": "*" } }, + "node_modules/@types/papaparse": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.5.2.tgz", + "integrity": "sha512-gFnFp/JMzLHCwRf7tQHrNnfhN4eYBVYYI897CGX4MY1tzY9l2aLkVyx2IlKZ/SAqDbB3I1AOZW5gTMGGsqWliA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/ping": { "version": "0.4.4", "resolved": "https://registry.npmjs.org/@types/ping/-/ping-0.4.4.tgz", @@ -3392,6 +4603,40 @@ "@types/node": "*" } }, + "node_modules/@types/ssh2": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/ssh2/-/ssh2-1.15.5.tgz", + "integrity": "sha512-N1ASjp/nXH3ovBHddRJpli4ozpk6UdDYIX4RJWFa9L1YKnzdhTlVmiGHm4DZnj/jLbqZpes4aeR30EFGQtvhQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "^18.11.18" + } + }, + "node_modules/@types/ssh2/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/ssh2/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/triple-beam": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/triple-beam/-/triple-beam-1.3.5.tgz", @@ -3420,6 +4665,299 @@ "@types/webidl-conversions": "*" } }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -3466,6 +5004,19 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -3501,6 +5052,35 @@ "node": ">=6" } }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/ansi-regex": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", @@ -3553,6 +5133,13 @@ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==", "license": "MIT" }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -3584,16 +5171,6 @@ "safer-buffer": "~2.1.0" } }, - "node_modules/assertion-error": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", - "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - } - }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -3617,6 +5194,142 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/babel-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz", + "integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.2.0", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.1", + "babel-preset-jest": "30.2.0", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz", + "integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==", + "dev": true, + "license": "BSD-3-Clause", + "workspaces": [ + "test/babel-8" + ], + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz", + "integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz", + "integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0 || ^8.0.0-beta.1" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -3833,13 +5546,6 @@ "node": ">=8" } }, - "node_modules/browser-stdout": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", - "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", - "dev": true, - "license": "ISC" - }, "node_modules/browserslist": { "version": "4.27.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.27.0.tgz", @@ -3874,6 +5580,29 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, "node_modules/bson": { "version": "6.10.4", "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.4.tgz", @@ -4063,16 +5792,13 @@ } }, "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, "license": "MIT", "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6" } }, "node_modules/caniuse-api": { @@ -4107,23 +5833,6 @@ ], "license": "CC-BY-4.0" }, - "node_modules/chai": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", - "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", - "dev": true, - "license": "MIT", - "dependencies": { - "assertion-error": "^2.0.1", - "check-error": "^2.1.1", - "deep-eql": "^5.0.1", - "loupe": "^3.1.0", - "pathval": "^2.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -4141,14 +5850,14 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/check-error": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", - "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true, "license": "MIT", "engines": { - "node": ">= 16" + "node": ">=10" } }, "node_modules/cheerio": { @@ -4231,6 +5940,29 @@ "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", "license": "ISC" }, + "node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz", + "integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -4312,6 +6044,24 @@ "node": ">=0.10.0" } }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, "node_modules/color": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", @@ -4598,6 +6348,13 @@ "node": ">=10.0.0" } }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, "node_modules/cron-parser": { "version": "4.9.0", "resolved": "https://registry.npmjs.org/cron-parser/-/cron-parser-4.9.0.tgz", @@ -4875,19 +6632,6 @@ } } }, - "node_modules/decamelize": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", - "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", @@ -4921,14 +6665,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/deep-eql": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", - "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "node_modules/dedent": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", "dev": true, "license": "MIT", - "engines": { - "node": ">=6" + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } } }, "node_modules/deep-is": { @@ -4938,6 +6687,16 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/defer-to-connect": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", @@ -4993,6 +6752,16 @@ "node": ">=8" } }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/detect-node": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", @@ -5000,9 +6769,9 @@ "license": "MIT" }, "node_modules/diff": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.0.tgz", - "integrity": "sha512-uIFDxqpRZGZ6ThOk84hEfqWoHx2devRFvpTZcTHur85vImfaxUbTW9Ryh4CpCuDnToOP1CEtXKIgytHBPVff5A==", + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", "dev": true, "license": "BSD-3-Clause", "engines": { @@ -5193,6 +6962,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -5600,6 +7382,20 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", @@ -5662,6 +7458,91 @@ "deprecated": "Use promise-toolbox/fromEvent instead", "license": "MIT" }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/express": { "version": "4.22.1", "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", @@ -5827,6 +7708,16 @@ "reusify": "^1.0.4" } }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, "node_modules/fecha": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/fecha/-/fecha-4.2.3.tgz", @@ -5931,16 +7822,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, "node_modules/flat-cache": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", @@ -6065,6 +7946,13 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "license": "MIT" }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -6255,6 +8143,16 @@ "node": ">=0.10" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -6288,6 +8186,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/get-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", @@ -6458,6 +8366,13 @@ "url": "https://github.com/sindresorhus/got?sponsor=1" } }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -6528,16 +8443,6 @@ "node": ">= 0.4" } }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, "node_modules/helmet": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/helmet/-/helmet-8.1.0.tgz", @@ -6702,6 +8607,16 @@ "numbered": "^1.1.0" } }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -6767,6 +8682,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", @@ -6777,6 +8712,18 @@ "node": ">=0.8.19" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, "node_modules/inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", @@ -6861,6 +8808,16 @@ "node": ">=8" } }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -6888,16 +8845,6 @@ "node": ">=0.12.0" } }, - "node_modules/is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/is-potential-custom-element-name": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", @@ -6916,19 +8863,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isarray": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", @@ -7168,6 +9102,23 @@ "node": ">=8" } }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/istanbul-lib-report": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", @@ -7183,6 +9134,21 @@ "node": ">=10" } }, + "node_modules/istanbul-lib-source-maps": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/istanbul-reports": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", @@ -7212,6 +9178,600 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jest": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz", + "integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@jest/core": "30.2.0", + "@jest/types": "30.2.0", + "import-local": "^3.2.0", + "jest-cli": "30.2.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz", + "integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.2.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz", + "integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/expect": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-runtime": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "p-limit": "^3.1.0", + "pretty-format": "30.2.0", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz", + "integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz", + "integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.2.0", + "@jest/types": "30.2.0", + "babel-jest": "30.2.0", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.2.0", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-runner": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz", + "integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz", + "integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "chalk": "^4.1.2", + "jest-util": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz", + "integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0", + "jest-validate": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz", + "integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.2.0", + "jest-worker": "30.2.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz", + "integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz", + "integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.2.0", + "jest-validate": "30.2.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz", + "integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz", + "integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.2.0", + "@jest/environment": "30.2.0", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.2.0", + "jest-environment-node": "30.2.0", + "jest-haste-map": "30.2.0", + "jest-leak-detector": "30.2.0", + "jest-message-util": "30.2.0", + "jest-resolve": "30.2.0", + "jest-runtime": "30.2.0", + "jest-util": "30.2.0", + "jest-watcher": "30.2.0", + "jest-worker": "30.2.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz", + "integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/globals": "30.2.0", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.2.0", + "jest-snapshot": "30.2.0", + "jest-util": "30.2.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz", + "integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.2.0", + "@jest/transform": "30.2.0", + "@jest/types": "30.2.0", + "babel-preset-current-node-syntax": "^1.2.0", + "chalk": "^4.1.2", + "expect": "30.2.0", + "graceful-fs": "^4.2.11", + "jest-diff": "30.2.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-util": "30.2.0", + "pretty-format": "30.2.0", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz", + "integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.2.0", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz", + "integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.2.0", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz", + "integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.2.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, "node_modules/jmespath": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", @@ -7291,6 +9851,19 @@ } } }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -7317,6 +9890,19 @@ "dev": true, "license": "MIT" }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/jsonwebtoken": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", @@ -7427,13 +10013,6 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, - "node_modules/just-extend": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", - "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", - "dev": true, - "license": "MIT" - }, "node_modules/jwa": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/jwa/-/jwa-1.4.2.tgz", @@ -7479,6 +10058,16 @@ "integrity": "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A==", "license": "MIT" }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -7612,23 +10201,6 @@ "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", "license": "MIT" }, - "node_modules/log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/logform": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/logform/-/logform-2.7.0.tgz", @@ -7652,13 +10224,6 @@ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", "license": "Apache-2.0" }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true, - "license": "MIT" - }, "node_modules/lowercase-keys": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", @@ -7713,6 +10278,23 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -7758,6 +10340,13 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -7833,6 +10422,16 @@ "node": ">= 0.6" } }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/mimic-response": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", @@ -8323,81 +10922,6 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, - "node_modules/mocha": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.1.0.tgz", - "integrity": "sha512-8uJR5RTC2NgpY3GrYcgpZrsEd9zKbPDpob1RezyR2upGHRQtHWofmzTMzTMSV6dru3tj5Ukt0+Vnq1qhFEEwAg==", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^4.1.3", - "browser-stdout": "^1.3.1", - "chokidar": "^3.5.3", - "debug": "^4.3.5", - "diff": "^5.2.0", - "escape-string-regexp": "^4.0.0", - "find-up": "^5.0.0", - "glob": "^10.4.5", - "he": "^1.2.0", - "js-yaml": "^4.1.0", - "log-symbols": "^4.1.0", - "minimatch": "^5.1.6", - "ms": "^2.1.3", - "serialize-javascript": "^6.0.2", - "strip-json-comments": "^3.1.1", - "supports-color": "^8.1.1", - "workerpool": "^6.5.1", - "yargs": "^17.7.2", - "yargs-parser": "^21.1.1", - "yargs-unparser": "^2.0.0" - }, - "bin": { - "_mocha": "bin/_mocha", - "mocha": "bin/mocha.js" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - } - }, - "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0" - } - }, - "node_modules/mocha/node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/mocha/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, "node_modules/moment": { "version": "2.30.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", @@ -8613,6 +11137,22 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -8641,31 +11181,6 @@ "integrity": "sha512-kbhcj2SVVR4caaVnGLJKmlk2+f+oLkjqdKeQlmUtz6nGzOpbcobwVIeSURNgraV/v3tlmGIX82OcPCl0K6RbHQ==", "license": "MIT" }, - "node_modules/nise": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/nise/-/nise-6.1.1.tgz", - "integrity": "sha512-aMSAzLVY7LyeM60gvBS423nBmIPP+Wy7St7hsb+8/fc1HmeoHJfLO8CKse4u3BtOZvQLJghYPI2i/1WZrEj5/g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^13.0.1", - "@sinonjs/text-encoding": "^0.7.3", - "just-extend": "^6.2.0", - "path-to-regexp": "^8.1.0" - } - }, - "node_modules/nise/node_modules/path-to-regexp": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz", - "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==", - "dev": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, "node_modules/node-abort-controller": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz", @@ -8749,6 +11264,13 @@ "node-gyp-build-optional-packages-test": "build-test.js" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.26", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.26.tgz", @@ -8837,6 +11359,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -8921,6 +11456,22 @@ "fn.name": "1.x.x" } }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -8980,6 +11531,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -9090,6 +11651,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -9131,16 +11702,6 @@ "node": ">=8" } }, - "node_modules/pathval": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", - "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 14.16" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -9168,6 +11729,85 @@ "node": ">=4.0.0" } }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/plimit-lit": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/plimit-lit/-/plimit-lit-1.6.1.tgz", @@ -9819,6 +12459,34 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -9894,6 +12562,23 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/qs": { "version": "6.14.1", "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz", @@ -9959,16 +12644,6 @@ "dev": true, "license": "MIT" }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -10022,6 +12697,13 @@ "node": ">= 0.8" } }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/readable-stream": { "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", @@ -10093,6 +12775,29 @@ "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", "license": "MIT" }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", @@ -10306,16 +13011,6 @@ "node": ">= 0.8" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -10515,35 +13210,6 @@ "node": ">=10" } }, - "node_modules/sinon": { - "version": "19.0.2", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-19.0.2.tgz", - "integrity": "sha512-euuToqM+PjO4UgXeLETsfQiuoyPXlqFezr6YZDFwHR3t4qaX0fZUe1MfPMznTL5f8BWrVS89KduLdMUsxFCO6g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "@sinonjs/commons": "^3.0.1", - "@sinonjs/fake-timers": "^13.0.2", - "@sinonjs/samsam": "^8.0.1", - "diff": "^7.0.0", - "nise": "^6.1.1", - "supports-color": "^7.2.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/sinon" - } - }, - "node_modules/sinon/node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.3.1" - } - }, "node_modules/slash": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", @@ -10581,6 +13247,17 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, "node_modules/sparse-bitfield": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", @@ -10596,6 +13273,13 @@ "integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==", "license": "ISC" }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/ssh2": { "version": "1.17.0", "resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz", @@ -10628,6 +13312,29 @@ "node": "*" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -10669,6 +13376,43 @@ "safe-buffer": "~5.2.0" } }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/string-to-stream": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/string-to-stream/-/string-to-stream-1.1.1.tgz", @@ -10811,6 +13555,26 @@ "node": ">=8" } }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -11009,6 +13773,22 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "license": "MIT" }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, "node_modules/tar-fs": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", @@ -11116,6 +13896,13 @@ "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", "license": "MIT" }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -11180,6 +13967,104 @@ "node": ">= 14.0.0" } }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, "node_modules/tsc-alias": { "version": "1.8.16", "resolved": "https://registry.npmjs.org/tsc-alias/-/tsc-alias-1.8.16.tgz", @@ -11366,6 +14251,41 @@ "node": ">= 0.8" } }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.4.tgz", @@ -11434,6 +14354,13 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, "node_modules/v8-to-istanbul": { "version": "9.3.0", "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", @@ -11485,6 +14412,16 @@ "node": ">=18" } }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, "node_modules/web-resource-inliner": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/web-resource-inliner/-/web-resource-inliner-7.0.0.tgz", @@ -11753,13 +14690,6 @@ "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", "license": "MIT" }, - "node_modules/workerpool": { - "version": "6.5.1", - "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", - "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/wrap-ansi": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", @@ -11854,6 +14784,20 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "license": "ISC" }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/ws": { "version": "8.18.3", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", @@ -11937,6 +14881,13 @@ "node": ">=10" } }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", @@ -11964,22 +14915,6 @@ "node": ">=12" } }, - "node_modules/yargs-unparser": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", - "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", - "dev": true, - "license": "MIT", - "dependencies": { - "camelcase": "^6.0.0", - "decamelize": "^4.0.0", - "flat": "^5.0.2", - "is-plain-obj": "^2.1.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/yargs/node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -12021,6 +14956,16 @@ "node": ">=8" } }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", diff --git a/server/package.json b/server/package.json index 3547810ce..91ba82988 100755 --- a/server/package.json +++ b/server/package.json @@ -5,7 +5,7 @@ "main": "index.js", "type": "module", "scripts": { - "test": "c8 mocha", + "test": "NODE_OPTIONS=--experimental-vm-modules c8 jest --runInBand", "dev": "nodemon --exec tsx src/index.js", "start": "node --watch ./dist/index.js", "build": "tsc && tsc-alias", @@ -58,21 +58,27 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/dockerode": "^4.0.0", "@types/express": "5.0.3", + "@types/gamedig": "^5.0.3", + "@types/jest": "^30.0.0", + "@types/jmespath": "^0.15.2", "@types/jsonwebtoken": "9.0.10", + "@types/mjml": "^4.7.4", "@types/multer": "^2.0.0", "@types/nodemailer": "7.0.1", + "@types/papaparse": "^5.5.2", "@types/ping": "0.4.4", "c8": "10.1.3", - "chai": "5.2.0", "eslint": "^9.17.0", "eslint-plugin-mocha": "^10.5.0", "esm": "3.2.25", "globals": "^15.14.0", - "mocha": "11.1.0", + "jest": "^30.2.0", "nodemon": "^3.1.11", "prettier": "^3.3.3", - "sinon": "19.0.2", + "ts-jest": "^29.4.6", + "ts-node": "^10.9.2", "tsc-alias": "1.8.16", "tsx": "4.20.5", "typescript": "5.9.2" diff --git a/server/scripts/generate-checks.js b/server/scripts/generate-checks.js new file mode 100644 index 000000000..d3a33b033 --- /dev/null +++ b/server/scripts/generate-checks.js @@ -0,0 +1,176 @@ +import mongoose from "mongoose"; +import { MonitorModel } from "../dist/db/models/Monitor.js"; +import { CheckModel } from "../dist/db/models/Check.js"; + +const DEFAULT_MONITOR_ID = "000000000000000000000001"; +const DEFAULT_TEAM_ID = "0000000000000000000000aa"; +const DEFAULT_USER_ID = "0000000000000000000000bb"; +const DEFAULT_MONITOR_TYPE = "http"; +const DEFAULT_TOTAL = 1_000_000; +const DEFAULT_BATCH_SIZE = 5_000; + +const parseObjectId = (value, fallback) => { + try { + return new mongoose.Types.ObjectId(value || fallback); + } catch (error) { + console.warn(`Invalid ObjectId '${value}', falling back to '${fallback}'.`); + return new mongoose.Types.ObjectId(fallback); + } +}; + +async function ensureMonitor({ monitorId, teamId, userId, type }) { + const existing = await MonitorModel.findById(monitorId); + if (existing) { + return existing; + } + + console.log(`Monitor ${monitorId.toString()} not found, creating it.`); + const monitor = new MonitorModel({ + _id: monitorId, + userId, + teamId, + name: `Seed Monitor ${monitorId.toString()}`, + description: "Synthetic monitor for performance testing", + statusWindow: [], + statusWindowSize: 5, + statusWindowThreshold: 60, + type, + ignoreTlsErrors: false, + url: "https://example.com", + isActive: true, + interval: 60000, + alertThreshold: 5, + cpuAlertThreshold: 5, + memoryAlertThreshold: 5, + diskAlertThreshold: 5, + tempAlertThreshold: 5, + selectedDisks: [], + }); + + await monitor.save(); + return monitor; +} + +async function run() { + const mongoUri = process.env.MONGO_URI ?? "mongodb://localhost:27017/uptime_db"; + const monitorId = parseObjectId(process.env.MONITOR_ID ?? DEFAULT_MONITOR_ID, DEFAULT_MONITOR_ID); + const teamId = parseObjectId("69648b0578209af45f9ffe30"); + const userId = parseObjectId("69648b0678209af45f9ffe32"); + const monitorType = process.env.MONITOR_TYPE ?? DEFAULT_MONITOR_TYPE; + const total = Number(process.env.CHECK_TOTAL ?? DEFAULT_TOTAL); + const batchSize = Number(process.env.CHECK_BATCH_SIZE ?? DEFAULT_BATCH_SIZE); + + console.log(`Connecting to MongoDB at ${mongoUri}`); + await mongoose.connect(mongoUri); + + await ensureMonitor({ monitorId, teamId, userId, type: monitorType }); + + console.log(`Seeding ${total} checks for monitor ${monitorId.toString()} (team ${teamId.toString()}) in batches of ${batchSize}.`); + + const docs = []; + const startTime = Date.now(); + + for (let i = 0; i < total; i += 1) { + const baseTime = Date.now() - (total - i) * 1000; + const createdAt = new Date(baseTime); + docs.push({ + metadata: { + monitorId, + teamId, + type: monitorType, + }, + status: i % 50 !== 0, + statusCode: i % 50 !== 0 ? 200 : 500, + responseTime: Math.floor(Math.random() * 1000), + message: i % 50 !== 0 ? "OK" : "Error", + expiry: createdAt, + createdAt, + updatedAt: createdAt, + timings: { + start: baseTime, + socket: baseTime, + lookup: baseTime, + connect: baseTime, + secureConnect: baseTime, + upload: baseTime, + response: baseTime + 40, + end: baseTime + 45, + phases: { + wait: 0, + dns: 1, + tcp: 2, + tls: 4, + request: 0, + firstByte: 30, + download: 5, + total: 45, + }, + }, + cpu: { + physical_core: 8, + logical_core: 16, + frequency: 3600, + temperature: [50 + Math.random() * 10], + free_percent: 40, + usage_percent: Math.random() * 100, + }, + memory: { + total_bytes: 32 * 1024 ** 3, + available_bytes: 16 * 1024 ** 3, + used_bytes: 16 * 1024 ** 3, + usage_percent: Math.random() * 100, + }, + disk: [ + { + device: "/dev/sda1", + mountpoint: "/", + read_speed_bytes: Math.random() * 10_000_000, + write_speed_bytes: Math.random() * 10_000_000, + total_bytes: 512 * 1024 ** 3, + free_bytes: 128 * 1024 ** 3, + usage_percent: Math.random() * 100, + }, + ], + host: { + os: "linux", + platform: "ubuntu", + kernel_version: "5.15.0", + }, + net: [ + { + name: "eth0", + bytes_sent: Math.random() * 10_000_000, + bytes_recv: Math.random() * 10_000_000, + packets_sent: Math.random() * 1_000_000, + packets_recv: Math.random() * 1_000_000, + err_in: 0, + err_out: 0, + drop_in: 0, + drop_out: 0, + fifo_in: 0, + fifo_out: 0, + }, + ], + errors: i % 50 === 0 ? [{ metric: ["uptime"], err: "500" }] : [], + }); + + if (docs.length === batchSize) { + await CheckModel.insertMany(docs, { ordered: false }); + console.log(`Inserted ${i + 1} / ${total}`); + docs.length = 0; + } + } + + if (docs.length > 0) { + await CheckModel.insertMany(docs, { ordered: false }); + } + + await mongoose.disconnect(); + const duration = ((Date.now() - startTime) / 1000).toFixed(2); + console.log(`Finished inserting ${total} checks in ${duration}s`); +} + +run().catch((error) => { + console.error("Failed to seed checks", error); + process.exit(1); +}); diff --git a/server/src/config/services.js b/server/src/config/services.ts similarity index 94% rename from server/src/config/services.js rename to server/src/config/services.ts index 6878026d6..74c68c86e 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.ts @@ -34,9 +34,8 @@ const { compile } = pkg; import mjml2html from "mjml"; import jwt from "jsonwebtoken"; import crypto from "crypto"; -import { games } from "gamedig"; +import { games, GameDig } from "gamedig"; import jmespath from "jmespath"; -import { GameDig } from "gamedig"; import { fileURLToPath } from "url"; import { ObjectId } from "mongodb"; @@ -47,7 +46,6 @@ import { GenerateAvatarImage } from "../utils/imageProcessing.js"; import { ParseBoolean } from "../utils/utils.js"; // Models -import { CheckModel } from "@/db/models/index.js"; import Monitor from "../db/models/Monitor.js"; import User from "../db/models/User.js"; import InviteToken from "../db/models/InviteToken.js"; @@ -72,11 +70,11 @@ import SettingsModule from "../db/modules/settingsModule.js"; import IncidentModule from "../db/modules/incidentModule.js"; // repositories -import { MongoMonitorsRepository, MongoChecksRepository } from "@/repositories/index.js"; +import { MongoMonitorsRepository, MongoChecksRepository, MongoMonitorStatsRepository } from "@/repositories/index.js"; -export const initializeServices = async ({ logger, envSettings, settingsService }) => { +export const initializeServices = async ({ logger, envSettings, settingsService }: { logger: any; envSettings: any; settingsService: any }) => { const serviceRegistry = new ServiceRegistry({ logger }); - ServiceRegistry.instance = serviceRegistry; + (ServiceRegistry as any).instance = serviceRegistry; const translationService = new TranslationService(logger); await translationService.initialize(); @@ -84,7 +82,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService const stringService = new StringService(translationService); // Create DB - const checkModule = new CheckModule({ logger, CheckModel, Monitor, User }); + const checkModule = new CheckModule({ logger, Monitor, User }); const inviteModule = new InviteModule({ InviteToken, crypto, stringService }); const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService }); const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); @@ -125,6 +123,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService // Repositories const monitorsRepository = new MongoMonitorsRepository(); const checksRepository = new MongoChecksRepository(); + const monitorStatsRepository = new MongoMonitorStatsRepository(); const networkService = new NetworkService({ axios, @@ -228,6 +227,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService games, monitorsRepository, checksRepository, + monitorStatsRepository, }); const services = { diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 527fd334d..ce0347512 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -8,8 +8,6 @@ import { createMonitorBodyValidation, editMonitorBodyValidation, pauseMonitorParamValidation, - getMonitorStatsByIdParamValidation, - getMonitorStatsByIdQueryValidation, getCertificateParamValidation, getHardwareDetailsByIdParamValidation, getHardwareDetailsByIdQueryValidation, @@ -90,39 +88,6 @@ class MonitorController { } }; - getMonitorStatsById = async (req: Request, res: Response, next: NextFunction) => { - try { - await getMonitorStatsByIdParamValidation.validateAsync(req.params); - await getMonitorStatsByIdQueryValidation.validateAsync(req.query); - - let { limit, sortOrder, dateRange, numToDisplay, normalize } = req.query; - const monitorId = req?.params?.monitorId; - - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } - - const monitorStats = await this.monitorService.getMonitorStatsById({ - teamId, - monitorId, - limit, - sortOrder, - dateRange, - numToDisplay, - normalize, - }); - - return res.status(200).json({ - success: true, - msg: "Monitor stats retrieved successfully", - data: monitorStats, - }); - } catch (error) { - next(error); - } - }; - getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => { try { await getHardwareDetailsByIdParamValidation.validateAsync(req.params); diff --git a/server/src/db/MongoDB.js b/server/src/db/MongoDB.js index 115c7e9ee..3db17c78f 100755 --- a/server/src/db/MongoDB.js +++ b/server/src/db/MongoDB.js @@ -11,12 +11,9 @@ class MongoDB { inviteModule, statusPageModule, userModule, - hardwareCheckModule, maintenanceWindowModule, monitorModule, - networkCheckModule, notificationModule, - pageSpeedCheckModule, recoveryModule, settingsModule, incidentModule, @@ -26,15 +23,12 @@ class MongoDB { this.userModule = userModule; this.inviteModule = inviteModule; this.recoveryModule = recoveryModule; - this.pageSpeedCheckModule = pageSpeedCheckModule; - this.hardwareCheckModule = hardwareCheckModule; this.checkModule = checkModule; this.maintenanceWindowModule = maintenanceWindowModule; this.monitorModule = monitorModule; this.notificationModule = notificationModule; this.settingsModule = settingsModule; this.statusPageModule = statusPageModule; - this.networkCheckModule = networkCheckModule; this.incidentModule = incidentModule; } diff --git a/server/src/db/models/MonitorStats.js b/server/src/db/models/MonitorStats.js deleted file mode 100755 index c081c8a4f..000000000 --- a/server/src/db/models/MonitorStats.js +++ /dev/null @@ -1,49 +0,0 @@ -import mongoose from "mongoose"; - -const MonitorStatsSchema = new mongoose.Schema( - { - monitorId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - immutable: true, - index: true, - }, - avgResponseTime: { - type: Number, - default: 0, - }, - totalChecks: { - type: Number, - default: 0, - }, - totalUpChecks: { - type: Number, - default: 0, - }, - totalDownChecks: { - type: Number, - default: 0, - }, - uptimePercentage: { - type: Number, - default: 0, - }, - lastCheckTimestamp: { - type: Number, - default: 0, - }, - lastResponseTime: { - type: Number, - default: 0, - }, - timeOfLastFailure: { - type: Number, - default: 0, - }, - }, - { timestamps: true } -); - -const MonitorStats = mongoose.model("MonitorStats", MonitorStatsSchema); - -export default MonitorStats; diff --git a/server/src/db/models/MonitorStats.ts b/server/src/db/models/MonitorStats.ts new file mode 100755 index 000000000..d1d49ca9f --- /dev/null +++ b/server/src/db/models/MonitorStats.ts @@ -0,0 +1,63 @@ +import { Schema, model, type Types } from "mongoose"; +import type { MonitorStats as MonitorStatsEntity } from "@/types/monitorStats.js"; + +type MonitorStatsDocumentBase = Omit & { + monitorId: Types.ObjectId; +}; + +interface MonitorStatsDocument extends MonitorStatsDocumentBase { + _id: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const MonitorStatsSchema = new Schema( + { + monitorId: { + type: Schema.Types.ObjectId, + ref: "Monitor", + immutable: true, + index: true, + required: true, + }, + avgResponseTime: { + type: Number, + default: 0, + }, + totalChecks: { + type: Number, + default: 0, + }, + totalUpChecks: { + type: Number, + default: 0, + }, + totalDownChecks: { + type: Number, + default: 0, + }, + uptimePercentage: { + type: Number, + default: 0, + }, + lastCheckTimestamp: { + type: Number, + default: 0, + }, + lastResponseTime: { + type: Number, + default: 0, + }, + timeOfLastFailure: { + type: Number, + default: undefined, + }, + }, + { timestamps: true } +); + +const MonitorStatsModel = model("MonitorStats", MonitorStatsSchema); + +export type { MonitorStatsDocument }; +export { MonitorStatsModel }; +export default MonitorStatsModel; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index 473940874..29f3539c9 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -3,3 +3,6 @@ export { default as MonitorModel } from "@/db/models/Monitor.js"; export * from "@/db/models/Check.js"; export { default as CheckModel } from "@/db/models/Check.js"; + +export * from "@/db/models/MonitorStats.js"; +export { default as MonitorStatsModel } from "@/db/models/MonitorStats.js"; \ No newline at end of file diff --git a/server/src/db/modules/monitorModule.js b/server/src/db/modules/monitorModule.js index 08b8e1371..87237632a 100755 --- a/server/src/db/modules/monitorModule.js +++ b/server/src/db/modules/monitorModule.js @@ -253,48 +253,6 @@ class MonitorModule { } }; - getMonitorStatsById = async ({ monitorId, sortOrder, dateRange, numToDisplay, normalize }) => { - try { - // Get monitor, if we can't find it, abort with error - const monitor = await this.Monitor.findById(monitorId); - if (monitor === null || monitor === undefined) { - throw new Error(this.stringService.getDbFindMonitorById(monitorId)); - } - - // Get query params - const sort = sortOrder === "asc" ? 1 : -1; - - // Get Checks for monitor in date range requested - const dates = this.getDateRange(dateRange); - const { checksAll, checksForDateRange } = await this.getMonitorChecks(monitorId, dates, sort); - - // Build monitor stats - const monitorStats = { - ...monitor.toObject(), - uptimeDuration: this.calculateUptimeDuration(checksAll), - lastChecked: this.getLastChecked(checksAll), - latestResponseTime: this.getLatestResponseTime(checksAll), - periodIncidents: this.getIncidents(checksForDateRange), - periodTotalChecks: checksForDateRange.length, - checks: this.processChecksForDisplay(this.NormalizeData, checksForDateRange, numToDisplay, normalize), - }; - - if (monitor.type === "http" || monitor.type === "ping" || monitor.type === "docker" || monitor.type === "port" || monitor.type === "game") { - // HTTP/PING Specific stats - monitorStats.periodAvgResponseTime = this.getAverageResponseTime(checksForDateRange); - monitorStats.periodUptime = this.getUptimePercentage(checksForDateRange); - const groupedChecks = this.groupChecksByTime(checksForDateRange, dateRange); - monitorStats.aggregateData = Object.values(groupedChecks).map(this.calculateGroupStats); - } - - return monitorStats; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorStatsById"; - throw error; - } - }; - getHardwareDetailsById = async ({ monitorId, dateRange }) => { try { const monitor = await this.Monitor.findById(monitorId); diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index 7741c071b..9451e63af 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -1,3 +1,4 @@ +import type { Check, MonitorType } from "@/types/index.js"; import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js"; export interface IChecksRepository { @@ -6,12 +7,53 @@ export interface IChecksRepository { monitorId: string, startDate: Date, endDate: Date, - dateString: string - ): Promise<{ - groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>; - groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; - groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; - uptimePercentage: number; - avgResponseTime: number; - }>; + dateString: string, + options?: { type?: MonitorType } + ): Promise< + | { + monitorType: "uptime"; + groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>; + groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; + groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; + uptimePercentage: number; + avgResponseTime: number; + } + | { + monitorType: "hardware"; + aggregateData: { + latestCheck: Check | null; + totalChecks: number; + }; + upChecks: { + totalChecks: number; + }; + checks: Array<{ + _id: string; + avgCpuUsage: number; + avgMemoryUsage: number; + avgTemperature: number[]; + disks: Array<{ + name: string; + readSpeed: number; + writeSpeed: number; + totalBytes: number; + freeBytes: number; + usagePercent: number; + }>; + net: Array<{ + name: string; + bytesSentPerSecond: number; + deltaBytesRecv: number; + deltaPacketsSent: number; + deltaPacketsRecv: number; + deltaErrIn: number; + deltaErrOut: number; + deltaDropIn: number; + deltaDropOut: number; + deltaFifoIn: number; + deltaFifoOut: number; + }>; + }>; + } + >; } diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 76d3658cd..23c7071be 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -14,6 +14,11 @@ import type { } from "@/types/index.js"; import { CheckModel, type CheckDocument } from "@/db/models/index.js"; import mongoose from "mongoose"; +import { + getAggregateData as getHardwareAggregateData, + getHardwareStats, + getUpChecks as getHardwareUpChecks, +} from "@/db/modules/monitorModuleQueries.js"; export type LatestChecksMap = Record; @@ -210,20 +215,17 @@ class MongoChecksRepistory implements IChecksRepository { }, {}); }; - findDateRangeChecksByMonitor = async ( - monitorId: string, - startDate: Date, - endDate: Date, - dateString: string - ): Promise<{ - groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>; - groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; - groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; - uptimePercentage: number; - avgResponseTime: number; - }> => { + findDateRangeChecksByMonitor = async (monitorId: string, startDate: Date, endDate: Date, dateString: string, options?: { type?: string }) => { + const monitorObjectId = new mongoose.Types.ObjectId(monitorId); + if (options?.type === "hardware") { + return this.findHardwareDateRangeChecks(monitorObjectId, startDate, endDate, dateString); + } + return this.findUptimeDateRangeChecks(monitorObjectId, startDate, endDate, dateString); + }; + + private findUptimeDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date, dateString: string) => { const matchStage = { - "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + "metadata.monitorId": monitorObjectId, updatedAt: { $gte: startDate, $lte: endDate }, }; const [result] = await CheckModel.aggregate([ @@ -296,12 +298,13 @@ class MongoChecksRepistory implements IChecksRepository { ], }, }, - ]).exec(); + ]); const uptimePercentage = result?.uptimePercentage?.[0]?.percentage ?? 0; const avgResponseTime = result?.groupedAvgResponseTime?.[0]?.avgResponseTime ?? 0; return { + monitorType: "uptime" as const, groupedChecks: result?.groupedChecks ?? [], groupedUpChecks: result?.groupedUpChecks ?? [], groupedDownChecks: result?.groupedDownChecks ?? [], @@ -309,6 +312,60 @@ class MongoChecksRepistory implements IChecksRepository { avgResponseTime, }; }; + + private findHardwareDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date, dateString: string) => { + const monitorId = monitorObjectId.toHexString(); + const dates = { start: startDate, end: endDate }; + const [aggregateDataDoc, upChecksDoc, hardwareMetrics] = await Promise.all([ + getHardwareAggregateData(monitorId, dates), + getHardwareUpChecks(monitorId, dates), + getHardwareStats(monitorId, dates, dateString), + ]); + + const aggregateData = { + latestCheck: aggregateDataDoc?.latestCheck ? this.toEntity(aggregateDataDoc.latestCheck as CheckDocument) : null, + totalChecks: aggregateDataDoc?.totalChecks ?? 0, + }; + + const upChecks = { + totalChecks: upChecksDoc?.totalChecks ?? 0, + }; + + const checks = (hardwareMetrics ?? []).map((metric) => ({ + _id: metric._id, + avgCpuUsage: metric.avgCpuUsage ?? 0, + avgMemoryUsage: metric.avgMemoryUsage ?? 0, + avgTemperature: metric.avgTemperature ?? [], + disks: (metric.disks ?? []).map((disk: { [key: string]: number | string | undefined }) => ({ + name: disk?.name ?? "", + readSpeed: disk?.readSpeed ?? 0, + writeSpeed: disk?.writeSpeed ?? 0, + totalBytes: disk?.totalBytes ?? 0, + freeBytes: disk?.freeBytes ?? 0, + usagePercent: disk?.usagePercent ?? 0, + })), + net: (metric.net ?? []).map((iface: { [key: string]: number | string | undefined }) => ({ + name: iface?.name ?? "", + bytesSentPerSecond: iface?.bytesSentPerSecond ?? 0, + deltaBytesRecv: iface?.deltaBytesRecv ?? 0, + deltaPacketsSent: iface?.deltaPacketsSent ?? 0, + deltaPacketsRecv: iface?.deltaPacketsRecv ?? 0, + deltaErrIn: iface?.deltaErrIn ?? 0, + deltaErrOut: iface?.deltaErrOut ?? 0, + deltaDropIn: iface?.deltaDropIn ?? 0, + deltaDropOut: iface?.deltaDropOut ?? 0, + deltaFifoIn: iface?.deltaFifoIn ?? 0, + deltaFifoOut: iface?.deltaFifoOut ?? 0, + })), + })); + + return { + monitorType: "hardware" as const, + aggregateData, + upChecks, + checks, + }; + }; } export default MongoChecksRepistory; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index c2bae6939..44d64f5bb 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -3,3 +3,6 @@ export { default as MongoMonitorsRepository } from "@/repositories/monitors/Mong export * from "@/repositories/checks/IChecksRepository.js"; export { default as MongoChecksRepository } from "@/repositories/checks/MongoChecksRepistory.js"; + +export * from "@/repositories/monitor-stats/IMonitorStatsRepository.js"; +export { default as MongoMonitorStatsRepository } from "@/repositories/monitor-stats/MongoMonitorStatsRepository.js"; diff --git a/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts b/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts new file mode 100644 index 000000000..a0720619f --- /dev/null +++ b/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts @@ -0,0 +1,9 @@ +import type { MonitorStats } from "@/types/index.js"; +export interface IMonitorStatsRepository { + // create + // single fetch + findByMonitorId(monitorId: string): Promise; + // update + // delete + // other +} diff --git a/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts b/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts new file mode 100644 index 000000000..d1d6a4825 --- /dev/null +++ b/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts @@ -0,0 +1,44 @@ +import { type MonitorStatsDocument, MonitorStatsModel } from "@/db/models/index.js"; +import type { MonitorStats } from "@/types/index.js"; +import { IMonitorStatsRepository } from "@/repositories/index.js"; +import mongoose from "mongoose"; +import { AppError } from "@/utils/AppError.js"; +class MongoMonitorStatsRepository implements IMonitorStatsRepository { + private toEntity = (doc: MonitorStatsDocument): MonitorStats => { + const toStringId = (value: unknown): string => { + if (value instanceof mongoose.Types.ObjectId) { + return value.toString(); + } + return value?.toString() ?? ""; + }; + + const toDateString = (value: Date | string): string => { + return value instanceof Date ? value.toISOString() : value; + }; + + return { + id: toStringId(doc._id), + monitorId: toStringId(doc.monitorId), + avgResponseTime: doc.avgResponseTime, + totalChecks: doc.totalChecks, + totalUpChecks: doc.totalUpChecks, + totalDownChecks: doc.totalDownChecks, + uptimePercentage: doc.uptimePercentage, + lastCheckTimestamp: doc.lastCheckTimestamp, + lastResponseTime: doc.lastResponseTime, + timeOfLastFailure: doc.timeOfLastFailure, + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), + }; + }; + + findByMonitorId = async (monitorId: string): Promise => { + const monitorStats = await MonitorStatsModel.findOne({ monitorId: new mongoose.Types.ObjectId(monitorId) }); + if (!monitorStats) { + throw new AppError({ message: "Monitor stats not found", status: 404 }); + } + return this.toEntity(monitorStats); + }; +} + +export default MongoMonitorStatsRepository; diff --git a/server/src/routes/v1/monitorRoute.ts b/server/src/routes/v1/monitorRoute.ts index a355efbb9..bcbf5a1c6 100755 --- a/server/src/routes/v1/monitorRoute.ts +++ b/server/src/routes/v1/monitorRoute.ts @@ -30,7 +30,6 @@ class MonitorRoutes { // General monitor routes this.router.post("/pause/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.pauseMonitor); - this.router.get("/stats/:monitorId", this.monitorController.getMonitorStatsById); // Util routes this.router.get("/certificate/:monitorId", (req, res, next) => { diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 3e4d39ade..4a2a2421c 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -2,7 +2,7 @@ import { createMonitorsBodyValidation } from "@/validation/joi.js"; import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js"; import { type Monitor } from "@/types/index.js"; import type { MonitorType } from "@/types/monitor.js"; -import type { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js"; +import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository } from "@/repositories/index.js"; import fs from "fs"; import { fileURLToPath } from "url"; import path from "path"; @@ -23,17 +23,8 @@ export interface IMonitorService { // read getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise; - getMonitorStatsById(args: { - teamId: string; - monitorId: string; - limit?: number; - sortOrder?: 1 | -1; - dateRange?: string; - numToDisplay?: number; - normalize?: boolean; - }): Promise; getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; - getMonitorById(args: { teamId: string; monitorId: string }): Promise; + getMonitorById(args: { teamId: string; monitorId: string }): Promise; getMonitorsByTeamId(args: { teamId: string; limit?: number; @@ -86,7 +77,7 @@ export class MonitorService implements IMonitorService { private games: any; private monitorsRepository: IMonitorsRepository; private checksRepository: IChecksRepository; - private fs: any; + private monitorStatsRepository: IMonitorStatsRepository; constructor({ db, @@ -99,6 +90,7 @@ export class MonitorService implements IMonitorService { games, monitorsRepository, checksRepository, + monitorStatsRepository, }: { db: any; jobQueue: any; @@ -110,6 +102,7 @@ export class MonitorService implements IMonitorService { games: any; monitorsRepository: IMonitorsRepository; checksRepository: IChecksRepository; + monitorStatsRepository: IMonitorStatsRepository; }) { this.db = db; this.jobQueue = jobQueue; @@ -121,6 +114,7 @@ export class MonitorService implements IMonitorService { this.games = games; this.monitorsRepository = monitorsRepository; this.checksRepository = checksRepository; + this.monitorStatsRepository = monitorStatsRepository; } get serviceName(): string { @@ -269,10 +263,14 @@ export class MonitorService implements IMonitorService { } const rangeKey = (dateRange as DateRangeKey) ?? "recent"; const { start, end } = this.getDateRange(rangeKey); - const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey)); - const monitorStats = await this.db.monitorModule.getMonitorStatsById({ - monitorId, + const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey), { + type: monitor.type, }); + const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id); + + if (checksData.monitorType !== "uptime") { + throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 }); + } return { monitorData: { @@ -287,47 +285,41 @@ export class MonitorService implements IMonitorService { }; }; - getMonitorStatsById = async ({ - teamId, - monitorId, - limit, - sortOrder, - dateRange, - numToDisplay, - normalize, - }: { - teamId: string; - monitorId: string; - limit?: number; - sortOrder?: 1 | -1; - dateRange?: string; - numToDisplay?: number; - normalize?: boolean; - }): Promise => { - await this.verifyTeamAccess({ teamId, monitorId }); - const monitorStats = await this.db.monitorModule.getMonitorStatsById({ - monitorId, - limit, - sortOrder, - dateRange, - numToDisplay, - normalize, - }); - - return monitorStats; - }; - getHardwareDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise => { await this.verifyTeamAccess({ teamId, monitorId }); - const monitor = await this.db.monitorModule.getHardwareDetailsById({ monitorId, dateRange }); + const monitor = await this.monitorsRepository.findById(monitorId, teamId); + if (!monitor) { + throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); + } + if (monitor.type !== "hardware") { + throw new AppError({ message: `${monitor.type} monitors are not supported for hardware details`, status: 400 }); + } - return monitor; + const rangeKey = (dateRange as DateRangeKey) ?? "recent"; + const { start, end } = this.getDateRange(rangeKey); + const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey), { + type: monitor.type, + }); + + if (checksData.monitorType !== "hardware") { + throw new AppError({ message: "Unable to load hardware stats for this monitor", status: 500 }); + } + + const stats = { + aggregateData: checksData.aggregateData, + upChecks: checksData.upChecks, + checks: checksData.checks, + }; + + return { + ...monitor, + stats, + }; }; getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { await this.verifyTeamAccess({ teamId, monitorId }); - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - + const monitor = await this.monitorsRepository.findById(monitorId, teamId); return monitor; }; diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index 30a9fc12c..e849bbb83 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -7,10 +7,14 @@ class StatusService { /** * @param {{ + * db: any + * logger: any * buffer: import("./bufferService.js").BufferService * incidentService: import("../business/incidentService.js").IncidentService + * monitorsRepository: any * }} - */ constructor({ db, logger, buffer, incidentService, monitorsRepository }) { + */ + constructor({ db, logger, buffer, incidentService, monitorsRepository }) { this.db = db; this.logger = logger; this.buffer = buffer; diff --git a/server/src/types/index.ts b/server/src/types/index.ts index 92bcb69b0..2367f1c1c 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -1,2 +1,3 @@ export * from "@/types/check.js"; export * from "@/types/monitor.js"; +export * from "@/types/monitorStats.js"; diff --git a/server/src/types/monitorStats.ts b/server/src/types/monitorStats.ts new file mode 100644 index 000000000..8f9603529 --- /dev/null +++ b/server/src/types/monitorStats.ts @@ -0,0 +1,14 @@ +export interface MonitorStats { + id: string; + monitorId: string; + avgResponseTime: number; + totalChecks: number; + totalUpChecks: number; + totalDownChecks: number; + uptimePercentage: number; + lastCheckTimestamp: number; + lastResponseTime: number; + timeOfLastFailure?: number; + createdAt: string; + updatedAt: string; +} diff --git a/server/src/validation/joi.js b/server/src/validation/joi.js index b3342de83..e1d04047d 100755 --- a/server/src/validation/joi.js +++ b/server/src/validation/joi.js @@ -134,18 +134,6 @@ const getMonitorsByTeamIdQueryValidation = joi.object({ order: joi.string().valid("asc", "desc"), }); -const getMonitorStatsByIdParamValidation = joi.object({ - monitorId: joi.string().required(), -}); -const getMonitorStatsByIdQueryValidation = joi.object({ - status: joi.string(), - limit: joi.number(), - sortOrder: joi.string().valid("asc", "desc"), - dateRange: joi.string().valid("hour", "day", "week", "month", "all"), - numToDisplay: joi.number(), - normalize: joi.boolean(), -}); - const getCertificateParamValidation = joi.object({ monitorId: joi.string().required(), }); @@ -725,8 +713,6 @@ export { getMonitorByIdQueryValidation, getMonitorsByTeamIdParamValidation, getMonitorsByTeamIdQueryValidation, - getMonitorStatsByIdParamValidation, - getMonitorStatsByIdQueryValidation, getHardwareDetailsByIdParamValidation, getHardwareDetailsByIdQueryValidation, getCertificateParamValidation, diff --git a/server/test/monitorService.test.ts b/server/test/monitorService.test.ts new file mode 100644 index 000000000..4fdcbeb49 --- /dev/null +++ b/server/test/monitorService.test.ts @@ -0,0 +1,218 @@ +import { jest } from "@jest/globals"; +import { MonitorService } from "../src/service/business/monitorService.ts"; +import type { IMonitorsRepository, IChecksRepository } from "../src/repositories/index.ts"; + +const createMonitorsRepositoryMock = () => + ({ + findMonitorCountByTeamIdAndType: jest.fn(), + findByTeamId: jest.fn(), + findById: jest.fn(), + create: jest.fn(), + createBulkMonitors: jest.fn(), + deleteByTeamId: jest.fn(), + }) as unknown as IMonitorsRepository; + +const createChecksRepositoryMock = () => + ({ + findLatestChecksByMonitorIds: jest.fn(), + findDateRangeChecksByMonitor: jest.fn(), + }) as unknown as IChecksRepository; + +const createService = ({ + monitorsRepository = createMonitorsRepositoryMock(), + checksRepository = createChecksRepositoryMock(), + monitorStatsRepository = { findByMonitorId: jest.fn() }, + monitorModuleOverrides = {}, +}: { + monitorsRepository?: IMonitorsRepository; + checksRepository?: IChecksRepository; + monitorStatsRepository?: { findByMonitorId: jest.Mock }; + monitorModuleOverrides?: Record; +} = {}) => { + const monitorModule = { + getMonitorById: jest.fn().mockResolvedValue({ teamId: { equals: () => true } }), + getMonitorStatsById: jest.fn().mockResolvedValue({ latest: {} }), + getMonitorsByTeamId: jest.fn().mockResolvedValue([]), + getMonitorsAndSummaryByTeamId: jest.fn().mockResolvedValue({ monitors: [], summary: {} }), + ...monitorModuleOverrides, + }; + + return new MonitorService({ + db: { + monitorModule, + statusPageModule: { deleteStatusPagesByMonitorId: jest.fn() }, + checkModule: { deleteChecks: jest.fn() }, + pageSpeedCheckModule: { deletePageSpeedChecksByMonitorId: jest.fn() }, + notificationsModule: { deleteNotificationsByMonitorId: jest.fn() }, + }, + jobQueue: { + addJob: jest.fn(), + updateJob: jest.fn(), + resumeJob: jest.fn(), + pauseJob: jest.fn(), + deleteJob: jest.fn(), + }, + stringService: {}, + emailService: { buildEmail: jest.fn(), sendEmail: jest.fn() }, + papaparse: { parse: jest.fn(), unparse: jest.fn() }, + logger: { info: jest.fn(), error: jest.fn(), warn: jest.fn() }, + errorService: { + createAuthorizationError: jest.fn(() => new Error("unauthorized")), + createServerError: jest.fn(() => new Error("server")), + createBadRequestError: jest.fn(() => new Error("bad request")), + createNotFoundError: jest.fn(() => new Error("not found")), + }, + games: [], + monitorsRepository, + checksRepository, + monitorStatsRepository, + }); +}; + +describe("MonitorService", () => { + describe("getMonitorsWithChecksByTeamId", () => { + it("returns monitors enriched with normalized checks", async () => { + const monitorsRepository = createMonitorsRepositoryMock(); + (monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(2); + (monitorsRepository.findByTeamId as jest.Mock).mockResolvedValue([ + { id: "m1", name: "Monitor 1", interval: 60000 }, + { id: "m2", name: "Monitor 2", interval: 60000 }, + ]); + + const checksRepository = createChecksRepositoryMock(); + (checksRepository.findLatestChecksByMonitorIds as jest.Mock).mockResolvedValue({ + m1: [ + { responseTime: 10, status: true, message: "OK" }, + { responseTime: 20, status: true, message: "OK" }, + ], + m2: [{ responseTime: 50, status: true, message: "OK" }], + }); + + const service = createService({ monitorsRepository, checksRepository }); + const result = await service.getMonitorsWithChecksByTeamId({ teamId: "team" }); + + expect(result).toMatchObject({ count: 2 }); + expect(result.monitors).toHaveLength(2); + expect(result.monitors[0]).toHaveProperty("checks"); + expect(result.monitors[0].checks.length).toBeGreaterThan(0); + expect(result.monitors[0].checks[0]).toEqual( + expect.objectContaining({ + responseTime: expect.any(Number), + status: expect.any(Boolean), + message: expect.any(String), + }) + ); + }); + }); + + describe("getMonitorsByTeamId", () => { + it("returns monitors array from db module", async () => { + const monitorsPayload = [ + { id: "m1", name: "Monitor 1" }, + { id: "m2", name: "Monitor 2" }, + ]; + const monitorModuleOverrides = { + getMonitorsByTeamId: jest.fn().mockResolvedValue(monitorsPayload), + }; + const service = createService({ monitorModuleOverrides }); + const result = await service.getMonitorsByTeamId({ teamId: "team" } as any); + expect(result).toHaveLength(2); + expect(result[0]).toHaveProperty("id", "m1"); + }); + }); + + describe("getMonitorsAndSummaryByTeamId", () => { + it("returns monitors with summary block", async () => { + const monitorModuleOverrides = { + getMonitorsAndSummaryByTeamId: jest.fn().mockResolvedValue({ monitors: [{ id: "m1" }], summary: { total: 1, uptime: 0.99 } }), + }; + const service = createService({ monitorModuleOverrides }); + const result = await service.getMonitorsAndSummaryByTeamId({ teamId: "team" }); + expect(result).toEqual({ monitors: [{ id: "m1" }], summary: { total: 1, uptime: 0.99 } }); + }); + }); + + describe("getUptimeDetailsById", () => { + it("returns monitorData and monitorStats with expected shape", async () => { + const TEAM_ID = "team"; + const monitor = { + id: "monitor-1", + teamId: TEAM_ID, + name: "Hardware monitor", + interval: 60000, + statusWindow: [], + statusWindowSize: 5, + statusWindowThreshold: 60, + type: "http", + ignoreTlsErrors: false, + url: "https://example.com", + isActive: true, + alertThreshold: 5, + cpuAlertThreshold: 5, + memoryAlertThreshold: 5, + diskAlertThreshold: 5, + tempAlertThreshold: 5, + selectedDisks: [], + notifications: [], + group: null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + const monitorsRepository = createMonitorsRepositoryMock(); + (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); + const checksRepository = createChecksRepositoryMock(); + (checksRepository.findDateRangeChecksByMonitor as jest.Mock).mockResolvedValue({ + monitorType: "uptime", + groupedChecks: [{ _id: "2024-01-01", avgResponseTime: 100, totalChecks: 2 }], + groupedUpChecks: [{ _id: "2024-01-01", totalChecks: 2, avgResponseTime: 90 }], + groupedDownChecks: [{ _id: "2024-01-01", totalChecks: 0, avgResponseTime: 0 }], + uptimePercentage: 0.99, + avgResponseTime: 95, + }); + + const monitorStatsRepository = { + findByMonitorId: jest.fn().mockResolvedValue({ + id: "stats-1", + monitorId: monitor.id, + avgResponseTime: 90, + totalChecks: 10, + totalUpChecks: 9, + totalDownChecks: 1, + uptimePercentage: 0.9, + lastCheckTimestamp: 123456789, + lastResponseTime: 80, + timeOfLastFailure: 123456700, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }), + } as any; + const monitorModuleOverrides = { + getMonitorById: jest.fn().mockResolvedValue({ teamId: { equals: (value: string) => value === TEAM_ID } }), + }; + + const service = createService({ monitorsRepository, checksRepository, monitorModuleOverrides, monitorStatsRepository }); + const result = await service.getUptimeDetailsById({ teamId: TEAM_ID, monitorId: "monitor-1", dateRange: "recent" }); + + expect(result).toHaveProperty("monitorData"); + expect(result.monitorData.monitor).toMatchObject({ id: monitor.id, name: monitor.name }); + expect(result.monitorData.groupedChecks[0]).toEqual( + expect.objectContaining({ + _id: expect.any(String), + avgResponseTime: expect.any(Number), + totalChecks: expect.any(Number), + }) + ); + expect(result.monitorStats).toEqual( + expect.objectContaining({ + monitorId: monitor.id, + avgResponseTime: 90, + totalChecks: 10, + totalUpChecks: 9, + totalDownChecks: 1, + }) + ); + }); + + }); +}); diff --git a/server/tsconfig.jest.json b/server/tsconfig.jest.json new file mode 100644 index 000000000..928b0977c --- /dev/null +++ b/server/tsconfig.jest.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": ".", + "types": ["jest"], + "noEmit": true + }, + "include": ["src", "test"] +} diff --git a/server/tsconfig.json b/server/tsconfig.json index f0b8e7223..cbb874656 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -12,7 +12,8 @@ "module": "nodenext", "moduleResolution": "nodenext", "skipLibCheck": true, - "noUncheckedIndexedAccess": true + "noUncheckedIndexedAccess": true, + "isolatedModules": true }, "include": ["src"], "exclude": ["tests", "dist", "node_modules"] From 4ad09fad4f73c00151084427c2e31f878036b12c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 14 Jan 2026 20:06:37 +0000 Subject: [PATCH 207/400] snapshots --- .../repositories/checks/IChecksRepository.ts | 2 +- .../checks/MongoChecksRepistory.ts | 9 ++++++--- server/src/service/business/monitorService.ts | 20 +++++++++++++++---- 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index 9451e63af..507389e6a 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -2,7 +2,7 @@ import type { Check, MonitorType } from "@/types/index.js"; import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js"; export interface IChecksRepository { - findLatestChecksByMonitorIds(monitorIds: string[]): Promise; + findLatestChecksByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise; findDateRangeChecksByMonitor( monitorId: string, startDate: Date, diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 23c7071be..2aeb32b2d 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -177,15 +177,18 @@ class MongoChecksRepistory implements IChecksRepository { }; }; - findLatestChecksByMonitorIds = async (monitorIds: string[]): Promise => { + findLatestChecksByMonitorIds = async ( + monitorIds: string[], + options?: { limitPerMonitor?: number } + ): Promise => { if (monitorIds.length === 0) { return {}; } const mongoIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); - const limitPerMonitor = 25; + const limitPerMonitor = options?.limitPerMonitor ?? 25; const maxIntervalMs = Number(10 * 60 * 1000); const bufferMs = Number(maxIntervalMs); - const lookbackMs = 25 * maxIntervalMs + bufferMs; + const lookbackMs = limitPerMonitor * maxIntervalMs + bufferMs; const cutoffDate = new Date(Date.now() - lookbackMs); const checkGroups = await CheckModel.aggregate([ { diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 4a2a2421c..0296ce9ca 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -404,10 +404,22 @@ export class MonitorService implements IMonitorService { order, }); - const monitorIds = monitors?.map((m: any) => m.id) ?? []; - const checksMap = await this.checksRepository.findLatestChecksByMonitorIds(monitorIds); - const monitorsWithChecks = (monitors ?? []).map((monitor: any) => { - const checks = NormalizeData(checksMap[monitor.id] ?? [], 10, 100); + const monitorsList = (monitors ?? []) as Monitor[]; + const snapshotTypes: MonitorType[] = ["hardware", "pagespeed"]; + const requestedTypes = Array.isArray(type) ? type : type ? [type] : []; + const snapshotOnlyRequest = + requestedTypes.length > 0 && requestedTypes.every((requestedType) => snapshotTypes.includes(requestedType as MonitorType)); + + const limitPerMonitor = snapshotOnlyRequest ? 1 : 25; + const checksMap = await this.checksRepository.findLatestChecksByMonitorIds( + monitorsList.map((monitor) => monitor.id), + { limitPerMonitor } + ); + + const monitorsWithChecks = monitorsList.map((monitor: Monitor) => { + const rawChecks = checksMap[monitor.id] ?? []; + const isSnapshotType = snapshotOnlyRequest || snapshotTypes.includes(monitor.type); + const checks = isSnapshotType ? rawChecks.slice(0, 1) : NormalizeData(rawChecks, 10, 100); return { ...monitor, checks, From 2b0c52304797018eb7e471115b92faeafe2e81c8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 14 Jan 2026 20:11:05 +0000 Subject: [PATCH 208/400] unify requests --- .../src/Pages/Infrastructure/Monitors/index.jsx | 5 +---- client/src/Pages/PageSpeed/Monitors/index.jsx | 15 +-------------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/client/src/Pages/Infrastructure/Monitors/index.jsx b/client/src/Pages/Infrastructure/Monitors/index.jsx index 4d739417c..b25636fa9 100644 --- a/client/src/Pages/Infrastructure/Monitors/index.jsx +++ b/client/src/Pages/Infrastructure/Monitors/index.jsx @@ -13,10 +13,7 @@ import { useTheme } from "@emotion/react"; import { useEffect, useState } from "react"; import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; import { useTranslation } from "react-i18next"; -import { - useFetchMonitorsByTeamId, - useFetchMonitorsWithChecks, -} from "@/Hooks/monitorHooks.js"; +import { useFetchMonitorsWithChecks } from "@/Hooks/monitorHooks.js"; import { useDispatch, useSelector } from "react-redux"; import { setRowsPerPage } from "../../../Features/UI/uiSlice.js"; diff --git a/client/src/Pages/PageSpeed/Monitors/index.jsx b/client/src/Pages/PageSpeed/Monitors/index.jsx index 548cd3491..4530a41e9 100644 --- a/client/src/Pages/PageSpeed/Monitors/index.jsx +++ b/client/src/Pages/PageSpeed/Monitors/index.jsx @@ -11,10 +11,7 @@ import FallbackPageSpeedWarning from "@/Components/v1/Fallback/FallbackPageSpeed // Utils import { useTheme } from "@emotion/react"; import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; -import { - useFetchMonitorsByTeamId, - useFetchMonitorsWithChecks, -} from "@/Hooks/monitorHooks.js"; +import { useFetchMonitorsWithChecks } from "@/Hooks/monitorHooks.js"; import { useFetchSettings } from "@/Hooks/settingsHooks.js"; // Constants const BREADCRUMBS = [{ name: `pagespeed`, path: "/pagespeed" }]; @@ -23,16 +20,6 @@ const PageSpeed = () => { const theme = useTheme(); const isAdmin = useIsAdmin(); - // const [monitors, monitorsSummary, isLoading, networkError] = useFetchMonitorsByTeamId({ - // limit: 10, - // types: TYPES, - // page: null, - // rowsPerPage: null, - // filter: null, - // field: null, - // order: null, - // }); - const [ monitorsWithChecks, monitorsWithChecksCount, From 940247b7bf86d12a7776bcac0b98f3f989a4eb38 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 14 Jan 2026 21:47:37 +0000 Subject: [PATCH 209/400] teamid --- .../src/Components/v1/Inputs/Search/index.jsx | 8 +-- .../src/Components/v1/Inputs/Select/index.jsx | 37 +++++++---- client/src/Hooks/monitorHooks.js | 38 ++--------- client/src/Pages/Incidents2/index.jsx | 2 +- .../Components/MonitorList/index.jsx | 8 +-- .../hooks/useMaintenanceActions.jsx | 8 ++- .../hooks/useMaintenanceData.jsx | 4 +- .../Maintenance/CreateMaintenance/index.jsx | 1 + .../Create/Components/MonitorList/index.jsx | 6 +- .../Create/Components/Tabs/Content.jsx | 2 +- .../Create/Hooks/useMonitorsFetch.jsx | 2 +- client/src/Pages/StatusPage/Create/index.jsx | 5 +- .../Status/Components/MonitorsList/index.jsx | 4 +- client/src/Utils/NetworkService.js | 9 +-- server/src/controllers/monitorController.ts | 7 +- server/src/db/modules/monitorModule.js | 66 ++++++++----------- server/src/service/business/monitorService.ts | 28 +------- server/src/validation/joi.js | 25 +++++-- 18 files changed, 109 insertions(+), 151 deletions(-) diff --git a/client/src/Components/v1/Inputs/Search/index.jsx b/client/src/Components/v1/Inputs/Search/index.jsx index 7a3771d47..347223bf3 100644 --- a/client/src/Components/v1/Inputs/Search/index.jsx +++ b/client/src/Components/v1/Inputs/Search/index.jsx @@ -85,7 +85,7 @@ const Search = ({ const enhancedOptions = React.useMemo(() => { return multiple && isAdorned ? [ - { [filteredBy]: t("selectAll"), isSelectAll: true, _id: "select_all" }, + { [filteredBy]: t("selectAll"), isSelectAll: true, _id: "select_all", id: "select_all" }, ...options, ] : options; @@ -93,7 +93,7 @@ const Search = ({ const isOptionSelected = (option) => { if (!multiple && !isAdorned) return false; if (Array.isArray(value)) { - return value.some((item) => item._id === option._id); + return value.some((item) => (item._id ?? item.id) === (option._id ?? option.id)); } return false; }; @@ -145,7 +145,7 @@ const Search = ({ disableClearable options={enhancedOptions} getOptionLabel={(option) => option[filteredBy]} - isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier + isOptionEqualToValue={(option, value) => (option._id ?? option.id) === (value?._id ?? value?.id)} // Compare by unique identifier renderInput={(params) => ( { - return option._id; + return option._id ?? option.id; }} renderOption={(props, option) => { const { key, ...optionProps } = props; diff --git a/client/src/Components/v1/Inputs/Select/index.jsx b/client/src/Components/v1/Inputs/Select/index.jsx index 37ce30dc1..f952b6818 100644 --- a/client/src/Components/v1/Inputs/Select/index.jsx +++ b/client/src/Components/v1/Inputs/Select/index.jsx @@ -62,6 +62,7 @@ const Select = ({ fieldWrapperSx = {}, }) => { const theme = useTheme(); + const getItemValue = (item) => item?._id ?? item?.id; const itemStyles = { fontSize: "var(--env-var-font-size-medium)", color: theme.palette.primary.contrastTextTertiary, @@ -123,7 +124,7 @@ const Select = ({ ...sx, }} renderValue={(selected) => { - const selectedItem = items.find((item) => item._id === selected); + const selectedItem = items.find((item) => getItemValue(item) === selected); const displayName = selectedItem ? selectedItem.name : placeholder; return ( )} - {items.map((item) => ( - - {item.name} - - ))} + {items + .map((item) => { + const itemValue = getItemValue(item); + if (itemValue === undefined || itemValue === null) { + return null; + } + return ( + + {item.name} + + ); + }) + .filter(Boolean)} ); @@ -179,8 +188,8 @@ Select.propTypes = { .isRequired, items: PropTypes.arrayOf( PropTypes.shape({ - _id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]) - .isRequired, + _id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]), + id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]), name: PropTypes.string.isRequired, }) diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index ffc356320..197779b53 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -100,22 +100,9 @@ const useFetchMonitorsWithChecks = ({ return [monitors, count, isLoading, networkError]; }; -const useFetchMonitorsByTeamId = ({ - types, - limit, - page, - rowsPerPage, - filter, - field, - order, - checkOrder, - normalize, - status, - updateTrigger, -}) => { +const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => { const [isLoading, setIsLoading] = useState(false); const [monitors, setMonitors] = useState(undefined); - const [summary, setSummary] = useState(undefined); const [networkError, setNetworkError] = useState(false); useEffect(() => { @@ -123,20 +110,11 @@ const useFetchMonitorsByTeamId = ({ try { setIsLoading(true); const res = await networkService.getMonitorsByTeamId({ - limit, types, - page, - rowsPerPage, filter, - field, - order, - checkOrder, - status, - normalize, }); - if (res?.data?.data?.filteredMonitors) { - setMonitors(res.data.data.filteredMonitors); - setSummary(res.data.data.summary); + if (res?.data?.data) { + setMonitors(res.data.data); } } catch (error) { setNetworkError(true); @@ -150,18 +128,10 @@ const useFetchMonitorsByTeamId = ({ fetchMonitors(); }, [ types, - limit, - page, - rowsPerPage, filter, - field, - order, updateTrigger, - checkOrder, - normalize, - status, ]); - return [monitors, summary, isLoading, networkError]; + return [monitors, isLoading, networkError]; }; const useFetchStatsByMonitorId = ({ diff --git a/client/src/Pages/Incidents2/index.jsx b/client/src/Pages/Incidents2/index.jsx index eda68d976..966388b51 100644 --- a/client/src/Pages/Incidents2/index.jsx +++ b/client/src/Pages/Incidents2/index.jsx @@ -34,7 +34,7 @@ const Incidents2 = () => { setUpdateTrigger((prev) => !prev); }; - const [monitors, , isLoadingMonitors, monitorsNetworkError] = useFetchMonitorsByTeamId( + const [monitors, isLoadingMonitors, monitorsNetworkError] = useFetchMonitorsByTeamId( {} ); diff --git a/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx b/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx index c3a4681a1..c70912582 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx +++ b/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx @@ -24,7 +24,7 @@ const MonitorListItem = ({ monitor, onDelete }) => { MonitorListItem.propTypes = { monitor: PropTypes.shape({ - _id: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, }).isRequired, onDelete: PropTypes.func.isRequired, @@ -33,7 +33,7 @@ MonitorListItem.propTypes = { const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => { const onDelete = (monitorToDelete) => { const newMonitors = selectedMonitors.filter( - (monitor) => monitor._id !== monitorToDelete._id + (monitor) => monitor.id !== monitorToDelete.id ); setSelectedMonitors(newMonitors); }; @@ -47,7 +47,7 @@ const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => { > {selectedMonitors?.map((monitor) => ( @@ -59,7 +59,7 @@ const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => { MonitorList.propTypes = { selectedMonitors: PropTypes.arrayOf( PropTypes.shape({ - _id: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, name: PropTypes.string.isRequired, }) ).isRequired, diff --git a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx index 6f180c880..22a61084d 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx +++ b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx @@ -22,6 +22,12 @@ const useMaintenanceActions = () => { weekly: MS_PER_DAY * 7, }; const handleSubmitForm = async (maintenanceWindowId, form) => { + const toStringId = (monitor) => { + if (!monitor) { + return ""; + } + return monitor._id ?? monitor.id ?? ""; + }; const start = dayjs(form.startDate) .set("hour", form.startTime.hour()) .set("minute", form.startTime.minute()); @@ -33,7 +39,7 @@ const useMaintenanceActions = () => { const repeat = REPEAT_LOOKUP[form.repeat]; const submit = { - monitors: form.monitors.map((monitor) => monitor._id), + monitors: form.monitors.map((monitor) => toStringId(monitor)).filter(Boolean), name: form.name, start: start.toISOString(), end: end.toISOString(), diff --git a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx index c3be0091a..96f37fd61 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx +++ b/client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx @@ -41,7 +41,7 @@ const useMaintenanceData = () => { limit: null, types: ["http", "ping", "pagespeed", "port"], }); - const fetchedMonitors = response.data.data.monitors; + const fetchedMonitors = response.data.data; return fetchedMonitors; }; @@ -55,7 +55,7 @@ const useMaintenanceData = () => { const endTime = dayjs(end); const durationInMs = endTime.diff(startTime, "milliseconds").toString(); const { duration, durationUnit } = getDurationAndUnit(durationInMs); - const monitor = monitorList.find((monitor) => monitor._id === monitorId); + const monitor = monitorList.find((monitor) => (monitor._id ?? monitor.id) === monitorId); const maintenanceWindowInformation = { name, repeat: REVERSE_REPEAT_LOOKUP[repeat], diff --git a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx b/client/src/Pages/Maintenance/CreateMaintenance/index.jsx index 384805f3a..759679a2d 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx +++ b/client/src/Pages/Maintenance/CreateMaintenance/index.jsx @@ -108,6 +108,7 @@ const CreateMaintenance = () => { const handleSubmit = async () => { if (hasValidationErrors(form, maintenanceWindowValidation, setErrors)) return; + const request = handleSubmitForm(maintenanceWindowId, form); try { setIsLoading(true); diff --git a/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx b/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx index 5484202d5..d2a13b2eb 100644 --- a/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx +++ b/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx @@ -43,7 +43,7 @@ const MonitorListItem = ({ const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => { const onDelete = (monitorToDelete) => { const newMonitors = selectedMonitors.filter( - (monitor) => monitor._id !== monitorToDelete._id + (monitor) => monitor.id !== monitorToDelete.id ); setSelectedMonitors(newMonitors); }; @@ -80,8 +80,8 @@ const MonitorList = ({ selectedMonitors, setSelectedMonitors }) => { > {selectedMonitors?.map((monitor, index) => ( {(provided, snapshot) => ( diff --git a/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx b/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx index d11825e69..e84de16dc 100644 --- a/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx +++ b/client/src/Pages/StatusPage/Create/Components/Tabs/Content.jsx @@ -24,7 +24,7 @@ const Content = ({ // Handlers const handleMonitorsChange = (selectedMonitors) => { handleFormChange({ - target: { name: "monitors", value: selectedMonitors.map((monitor) => monitor._id) }, + target: { name: "monitors", value: selectedMonitors.map((monitor) => monitor.id) }, }); setSelectedMonitors(selectedMonitors); }; diff --git a/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx b/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx index 95ce2b8c0..03ebb07bf 100644 --- a/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx +++ b/client/src/Pages/StatusPage/Create/Hooks/useMonitorsFetch.jsx @@ -16,7 +16,7 @@ const useMonitorsFetch = () => { limit: null, // donot return any checks for the monitors types: ["http", "ping", "port", "game"], // include game servers in status page monitor selection }); - setMonitors(response.data.data.monitors); + setMonitors(response.data.data); } catch (error) { setNetworkError(true); createToast({ body: error.message }); diff --git a/client/src/Pages/StatusPage/Create/index.jsx b/client/src/Pages/StatusPage/Create/index.jsx index 7fbe6a8dc..f8fcf0e91 100644 --- a/client/src/Pages/StatusPage/Create/index.jsx +++ b/client/src/Pages/StatusPage/Create/index.jsx @@ -62,7 +62,6 @@ const CreateStatusPage = () => { useStatusPageFetch(isCreate, url); const [deleteStatusPage, isDeleting] = useStatusPageDelete(fetchStatusPage, url); - console.log(JSON.stringify(form, null, 2)); // Handlers const handleFormChange = (e) => { let { type, name, value, checked } = e.target; @@ -144,6 +143,8 @@ const CreateStatusPage = () => { ...form, logo: { type: form.logo?.type ?? null, size: form.logo?.size ?? null }, }; + + console.log(toSubmit); const { error } = statusPageValidation.validate(toSubmit, { abortEarly: false, }); @@ -205,7 +206,7 @@ const CreateStatusPage = () => { companyName: statusPage?.companyName, isPublished: statusPage?.isPublished, timezone: statusPage?.timezone, - monitors: statusPageMonitors.map((monitor) => monitor._id), + monitors: statusPageMonitors.map((monitor) => monitor.id), color: statusPage?.color, logo: newLogo, showCharts: statusPage?.showCharts ?? true, diff --git a/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx b/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx index 89e32e5d5..33f771233 100644 --- a/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx +++ b/client/src/Pages/StatusPage/Status/Components/MonitorsList/index.jsx @@ -28,14 +28,14 @@ const MonitorsList = ({ const status = determineState(monitor); return ( { params.append("type", type); }); } - if (page) params.append("page", page); - if (rowsPerPage) params.append("rowsPerPage", rowsPerPage); - if (filter) params.append("filter", filter); - if (field) params.append("field", field); - if (order) params.append("order", order); + if (filter !== undefined && filter !== null && filter !== "") params.append("filter", filter); return this.axiosInstance.get(`/monitors/team?${params.toString()}`, { headers: { diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index ce0347512..9124abd06 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -5,6 +5,7 @@ import { getMonitorByIdQueryValidation, getMonitorsByTeamIdParamValidation, getMonitorsByTeamIdQueryValidation, + getMonitorsWithChecksQueryValidation, createMonitorBodyValidation, editMonitorBodyValidation, pauseMonitorParamValidation, @@ -317,10 +318,10 @@ class MonitorController { await getMonitorsByTeamIdParamValidation.validateAsync(req.params); await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); - let { limit, type, page, rowsPerPage, filter, field, order } = req.query; + const { type, filter } = req.query; const teamId = req?.user?.teamId; - const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, limit, type, page, rowsPerPage, filter, field, order }); + const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter }); return res.status(200).json({ success: true, @@ -358,7 +359,7 @@ class MonitorController { getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { await getMonitorsByTeamIdParamValidation.validateAsync(req.params); - await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); + await getMonitorsWithChecksQueryValidation.validateAsync(req.query); const explain = req?.query?.explain; let { limit, type, page, rowsPerPage, filter, field, order } = req.query; diff --git a/server/src/db/modules/monitorModule.js b/server/src/db/modules/monitorModule.js index 87237632a..84ff86996 100755 --- a/server/src/db/modules/monitorModule.js +++ b/server/src/db/modules/monitorModule.js @@ -289,47 +289,33 @@ class MonitorModule { } }; - getMonitorsByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId }) => { - limit = parseInt(limit); - page = parseInt(page); - rowsPerPage = parseInt(rowsPerPage); - if (field === undefined) { - field = "name"; - order = "asc"; - } - // Build match stage - const matchStage = { teamId: new this.ObjectId(teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - - const summaryResult = await this.Monitor.aggregate(buildMonitorSummaryByTeamIdPipeline({ matchStage })); - const summary = summaryResult[0]; - - const monitors = await this.Monitor.aggregate(buildMonitorsByTeamIdPipeline({ matchStage, field, order })); - - const filteredMonitors = await this.Monitor.aggregate( - buildFilteredMonitorsByTeamIdPipeline({ - matchStage, - filter, - page, - rowsPerPage, - field, - order, - limit, - type, - }) - ); - - const normalizedFilteredMonitors = filteredMonitors.map((monitor) => { - if (!monitor.checks) { - return monitor; + getMonitorsByTeamId = async ({ teamId, type, filter }) => { + try { + const matchStage = { teamId: new this.ObjectId(teamId) }; + if (type !== undefined) { + matchStage.type = Array.isArray(type) ? { $in: type } : type; } - monitor.checks = this.NormalizeData(monitor.checks, 10, 100); - return monitor; - }); - - return { summary, monitors, filteredMonitors: normalizedFilteredMonitors }; + if (filter !== undefined && filter !== null && filter !== "") { + matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }]; + } + const monitors = await this.Monitor.find(matchStage) + .sort({ name: 1 }) + .select({ + _id: 1, + name: 1, + type: 1, + url: 1, + status: 1, + isActive: 1, + teamId: 1, + }) + .lean(); + return monitors; + } catch (error) { + error.service = SERVICE_NAME; + error.method = "getMonitorsByTeamId"; + throw error; + } }; getMonitorsAndSummaryByTeamId = async ({ type, explain, teamId }) => { diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 0296ce9ca..46731446f 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -323,34 +323,10 @@ export class MonitorService implements IMonitorService { return monitor; }; - getMonitorsByTeamId = async ({ - teamId, - limit, - type, - page, - rowsPerPage, - filter, - field, - order, - }: { - teamId: string; - limit?: number; - type?: string | string[]; - page?: number; - rowsPerPage?: number; - filter?: string; - field?: string; - order?: "asc" | "desc"; - }): Promise => { - const monitors = await this.db.monitorModule.getMonitorsByTeamId({ - limit, + getMonitorsByTeamId = async ({ teamId, type, filter }: { teamId: string; type?: MonitorType | MonitorType[]; filter?: string }): Promise => { + const monitors = await this.monitorsRepository.findByTeamId(teamId, { type, - page, - rowsPerPage, filter, - field, - order, - teamId, }); return monitors; }; diff --git a/server/src/validation/joi.js b/server/src/validation/joi.js index e1d04047d..c09ac88b9 100755 --- a/server/src/validation/joi.js +++ b/server/src/validation/joi.js @@ -120,18 +120,30 @@ const getMonitorByIdQueryValidation = joi.object({ const getMonitorsByTeamIdParamValidation = joi.object({}); const getMonitorsByTeamIdQueryValidation = joi.object({ - limit: joi.number(), type: joi .alternatives() .try( joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game"), joi.array().items(joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game")) ), - page: joi.number(), - rowsPerPage: joi.number(), - filter: joi.string(), - field: joi.string(), - order: joi.string().valid("asc", "desc"), + filter: joi.string().allow("", null), +}); + +const getMonitorsWithChecksQueryValidation = joi.object({ + limit: joi.number().integer().min(1).max(100).optional(), + page: joi.number().integer().min(0).optional(), + rowsPerPage: joi.number().integer().min(1).max(100).optional(), + filter: joi.string().allow("", null).optional(), + field: joi.string().optional(), + order: joi.string().valid("asc", "desc").optional(), + type: joi + .alternatives() + .try( + joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game"), + joi.array().items(joi.string().valid("http", "ping", "pagespeed", "docker", "hardware", "port", "game")) + ) + .optional(), + explain: joi.boolean().optional(), }); const getCertificateParamValidation = joi.object({ @@ -713,6 +725,7 @@ export { getMonitorByIdQueryValidation, getMonitorsByTeamIdParamValidation, getMonitorsByTeamIdQueryValidation, + getMonitorsWithChecksQueryValidation, getHardwareDetailsByIdParamValidation, getHardwareDetailsByIdQueryValidation, getCertificateParamValidation, From d4af86f26c3466b7616f8a43c7d7ee6690fb7f48 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 14 Jan 2026 22:44:23 +0000 Subject: [PATCH 210/400] pagespeed details --- client/src/Hooks/monitorHooks.js | 92 +++++------ client/src/Pages/PageSpeed/Details/index.jsx | 11 +- client/src/Utils/NetworkService.js | 11 +- server/src/controllers/controllerUtils.ts | 101 +++++++++++- server/src/controllers/monitorController.ts | 152 +++++++++--------- .../repositories/checks/IChecksRepository.ts | 102 ++++++------ .../checks/MongoChecksRepistory.ts | 36 ++++- server/src/routes/v1/monitorRoute.ts | 2 + server/src/service/business/monitorService.ts | 33 +++- 9 files changed, 354 insertions(+), 186 deletions(-) diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index 197779b53..bc391ebf6 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -6,7 +6,7 @@ import { useMonitorUtils } from "./useMonitorUtils.js"; import { useNavigate } from "react-router-dom"; import { useTranslation } from "react-i18next"; -const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => { +export const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => { const [isLoading, setIsLoading] = useState(false); const [monitors, setMonitors] = useState(undefined); const [monitorsSummary, setMonitorsSummary] = useState(undefined); @@ -37,7 +37,7 @@ const useFetchMonitorsWithSummary = ({ types, monitorUpdateTrigger }) => { return [monitors, monitorsSummary, isLoading, networkError]; }; -const useFetchMonitorsWithChecks = ({ +export const useFetchMonitorsWithChecks = ({ types, limit, page, @@ -100,7 +100,7 @@ const useFetchMonitorsWithChecks = ({ return [monitors, count, isLoading, networkError]; }; -const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => { +export const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => { const [isLoading, setIsLoading] = useState(false); const [monitors, setMonitors] = useState(undefined); const [networkError, setNetworkError] = useState(false); @@ -126,15 +126,11 @@ const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => { } }; fetchMonitors(); - }, [ - types, - filter, - updateTrigger, - ]); + }, [types, filter, updateTrigger]); return [monitors, isLoading, networkError]; }; -const useFetchStatsByMonitorId = ({ +export const useFetchStatsByMonitorId = ({ monitorId, sortOrder, limit, @@ -173,7 +169,7 @@ const useFetchStatsByMonitorId = ({ return [monitor, audits, isLoading, networkError]; }; -const useFetchMonitorGames = ({ setGames, updateTrigger }) => { +export const useFetchMonitorGames = ({ setGames, updateTrigger }) => { const [isLoading, setIsLoading] = useState(true); useEffect(() => { const fetchGames = async () => { @@ -192,7 +188,7 @@ const useFetchMonitorGames = ({ setGames, updateTrigger }) => { return [isLoading]; }; -const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => { +export const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => { const [isLoading, setIsLoading] = useState(true); useEffect(() => { if (typeof monitorId === "undefined") { @@ -215,7 +211,7 @@ const useFetchMonitorById = ({ monitorId, setMonitor, updateTrigger }) => { return [isLoading]; }; -const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) => { +export const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) => { const [isLoading, setIsLoading] = useState(true); const [networkError, setNetworkError] = useState(false); const [monitor, setMonitor] = useState(undefined); @@ -241,8 +237,34 @@ const useFetchHardwareMonitorById = ({ monitorId, dateRange, updateTrigger }) => }, [monitorId, dateRange, updateTrigger]); return [monitor, isLoading, networkError]; }; +export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigger }) => { + const [isLoading, setIsLoading] = useState(true); + const [networkError, setNetworkError] = useState(false); + const [monitor, setMonitor] = useState(undefined); -const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => { + useEffect(() => { + const fetchMonitor = async () => { + try { + if (!monitorId) { + return { monitor: undefined, isLoading: false, networkError: undefined }; + } + const response = await networkService.getPageSpeedDetailsByMonitorId({ + monitorId: monitorId, + dateRange: dateRange, + }); + setMonitor(response.data.data); + } catch (error) { + setNetworkError(true); + } finally { + setIsLoading(false); + } + }; + fetchMonitor(); + }, [monitorId, dateRange, updateTrigger]); + return [monitor, isLoading, networkError]; +}; + +export const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => { const [networkError, setNetworkError] = useState(false); const [isLoading, setIsLoading] = useState(true); const [monitor, setMonitor] = useState(undefined); @@ -270,7 +292,7 @@ const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => { return [monitor, monitorStats, isLoading, networkError]; }; -const useCreateMonitor = () => { +export const useCreateMonitor = () => { const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); const createMonitor = async ({ monitor, redirect }) => { @@ -290,7 +312,7 @@ const useCreateMonitor = () => { return [createMonitor, isLoading]; }; -const useFetchGlobalSettings = () => { +export const useFetchGlobalSettings = () => { const [isLoading, setIsLoading] = useState(true); const [globalSettings, setGlobalSettings] = useState(undefined); useEffect(() => { @@ -312,7 +334,7 @@ const useFetchGlobalSettings = () => { return [globalSettings, isLoading]; }; -const useDeleteMonitor = () => { +export const useDeleteMonitor = () => { const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); const deleteMonitor = async ({ monitor, redirect }) => { @@ -333,7 +355,7 @@ const useDeleteMonitor = () => { return [deleteMonitor, isLoading]; }; -const useUpdateMonitor = () => { +export const useUpdateMonitor = () => { const [isLoading, setIsLoading] = useState(false); const navigate = useNavigate(); const updateMonitor = async ({ monitor, redirect }) => { @@ -380,7 +402,7 @@ const useUpdateMonitor = () => { return [updateMonitor, isLoading]; }; -const usePauseMonitor = () => { +export const usePauseMonitor = () => { const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(undefined); const pauseMonitor = async ({ monitorId, triggerUpdate }) => { @@ -403,7 +425,7 @@ const usePauseMonitor = () => { return [pauseMonitor, isLoading, error]; }; -const useAddDemoMonitors = () => { +export const useAddDemoMonitors = () => { const [isLoading, setIsLoading] = useState(false); const { t } = useTranslation(); const addDemoMonitors = async () => { @@ -420,7 +442,7 @@ const useAddDemoMonitors = () => { return [addDemoMonitors, isLoading]; }; -const useDeleteAllMonitors = () => { +export const useDeleteAllMonitors = () => { const [isLoading, setIsLoading] = useState(false); const { t } = useTranslation(); const deleteAllMonitors = async () => { @@ -437,7 +459,7 @@ const useDeleteAllMonitors = () => { return [deleteAllMonitors, isLoading]; }; -const useDeleteMonitorStats = () => { +export const useDeleteMonitorStats = () => { const { t } = useTranslation(); const [isLoading, setIsLoading] = useState(false); const deleteMonitorStats = async () => { @@ -455,7 +477,7 @@ const useDeleteMonitorStats = () => { return [deleteMonitorStats, isLoading]; }; -const useCreateBulkMonitors = () => { +export const useCreateBulkMonitors = () => { const [isLoading, setIsLoading] = useState(false); const createBulkMonitors = async (file, user) => { @@ -478,7 +500,7 @@ const useCreateBulkMonitors = () => { return [createBulkMonitors, isLoading]; }; -const useExportMonitors = () => { +export const useExportMonitors = () => { const [isLoading, setIsLoading] = useState(false); const { t } = useTranslation(); @@ -511,7 +533,7 @@ const useExportMonitors = () => { return [exportMonitors, isLoading]; }; -const useFetchJson = () => { +export const useFetchJson = () => { const [isLoading, setIsLoading] = useState(false); const fetchJson = async () => { try { @@ -527,25 +549,3 @@ const useFetchJson = () => { }; return [fetchJson, isLoading]; }; - -export { - useFetchMonitorsWithSummary, - useFetchMonitorsWithChecks, - useFetchMonitorsByTeamId, - useFetchStatsByMonitorId, - useFetchMonitorById, - useFetchUptimeMonitorById, - useFetchHardwareMonitorById, - useCreateMonitor, - useFetchGlobalSettings, - useDeleteMonitor, - useUpdateMonitor, - usePauseMonitor, - useAddDemoMonitors, - useDeleteAllMonitors, - useDeleteMonitorStats, - useCreateBulkMonitors, - useExportMonitors, - useFetchMonitorGames, - useFetchJson, -}; diff --git a/client/src/Pages/PageSpeed/Details/index.jsx b/client/src/Pages/PageSpeed/Details/index.jsx index 8a87133df..df411048a 100644 --- a/client/src/Pages/PageSpeed/Details/index.jsx +++ b/client/src/Pages/PageSpeed/Details/index.jsx @@ -11,14 +11,13 @@ import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; import { useTheme } from "@emotion/react"; import { useIsAdmin } from "@/Hooks/useIsAdmin.js"; import { useParams } from "react-router-dom"; -import { useFetchStatsByMonitorId } from "../../../Hooks/monitorHooks.js"; +import { useFetchPageSpeedMonitorById } from "../../../Hooks/monitorHooks.js"; import { useState } from "react"; import { useTranslation } from "react-i18next"; // Constants const BREADCRUMBS = [ { name: "pagespeed", path: "/pagespeed" }, { name: "details", path: `` }, - // { name: "details", path: `/pagespeed/${monitorId}` }, // Not needed? ]; const PageSpeedDetails = () => { @@ -36,13 +35,9 @@ const PageSpeedDetails = () => { }); const [trigger, setTrigger] = useState(false); // Network - const [monitor, audits, isLoading, networkError] = useFetchStatsByMonitorId({ + const [monitor, isLoading, networkError] = useFetchPageSpeedMonitorById({ monitorId, - sortOrder: "desc", - limit: 50, dateRange: "day", - numToDisplay: null, - normalize: null, updateTrigger: trigger, }); @@ -116,7 +111,7 @@ const PageSpeedDetails = () => { /> ); diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js index a736a1c3b..bd0e37b30 100644 --- a/client/src/Utils/NetworkService.js +++ b/client/src/Utils/NetworkService.js @@ -152,7 +152,8 @@ class NetworkService { params.append("type", type); }); } - if (filter !== undefined && filter !== null && filter !== "") params.append("filter", filter); + if (filter !== undefined && filter !== null && filter !== "") + params.append("filter", filter); return this.axiosInstance.get(`/monitors/team?${params.toString()}`, { headers: { @@ -197,6 +198,14 @@ class NetworkService { `/monitors/hardware/details/${config.monitorId}?${params.toString()}` ); } + async getPageSpeedDetailsByMonitorId(config) { + const params = new URLSearchParams(); + if (config.dateRange) params.append("dateRange", config.dateRange); + + return this.axiosInstance.get( + `/monitors/pagespeed/details/${config.monitorId}?${params.toString()}` + ); + } async getUptimeDetailsById(config) { const params = new URLSearchParams(); if (config.dateRange) params.append("dateRange", config.dateRange); diff --git a/server/src/controllers/controllerUtils.ts b/server/src/controllers/controllerUtils.ts index 4d9f9a279..af3eddd44 100755 --- a/server/src/controllers/controllerUtils.ts +++ b/server/src/controllers/controllerUtils.ts @@ -1,3 +1,6 @@ +import { AppError } from "@/utils/AppError.js"; +import { type MonitorType, MonitorTypes } from "@/types/index.js"; + const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise => { const monitorUrl = new URL(monitor.url); const hostname = monitorUrl.hostname; @@ -8,5 +11,101 @@ const fetchMonitorCertificate = async (sslChecker: any, monitor: any): Promise { + if (typeof value === "string" && value.trim().length > 0) { + return value; + } + throw new AppError({ message: `${fieldName} is required`, status: 400 }); +}; -export { fetchMonitorCertificate }; +const optionalString = (value: unknown, fieldName: string): string | undefined => { + if (value === undefined) { + return undefined; + } + if (typeof value === "string") { + return value; + } + throw new AppError({ message: `${fieldName} must be a string`, status: 400 }); +}; + +const optionalNumber = (value: unknown, fieldName: string): number | undefined => { + if (value === undefined) { + return undefined; + } + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + if (typeof value === "string" && value.trim() !== "") { + const parsed = Number(value); + if (!Number.isNaN(parsed)) { + return parsed; + } + } + throw new AppError({ message: `${fieldName} must be a number`, status: 400 }); +}; + +const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => { + if (value === undefined) { + return undefined; + } + if (typeof value === "boolean") { + return value; + } + if (typeof value === "string") { + if (value === "true") { + return true; + } + if (value === "false") { + return false; + } + } + throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 }); +}; + +const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => { + const parseSingle = (input: unknown): MonitorType => { + if (typeof input !== "string") { + throw new AppError({ message: "Monitor type must be a string", status: 400 }); + } + if (!MonitorTypes.includes(input as MonitorType)) { + throw new AppError({ message: `Invalid monitor type: ${input}`, status: 400 }); + } + return input as MonitorType; + }; + + if (value === undefined) { + return undefined; + } + if (Array.isArray(value)) { + return value.map((entry) => parseSingle(entry)); + } + return parseSingle(value); +}; + +const parseSortOrder = (value: unknown): "asc" | "desc" | undefined => { + if (value === undefined) { + return undefined; + } + if (value === "asc" || value === "desc") { + return value; + } + throw new AppError({ message: "order must be either 'asc' or 'desc'", status: 400 }); +}; + +const requireTeamId = (teamId?: string): string => { + if (!teamId) { + throw new AppError({ message: "Team ID is required", status: 400 }); + } + return teamId; +}; + +export { + fetchMonitorCertificate, + requireString, + optionalString, + optionalNumber, + optionalBoolean, + parseMonitorTypeFilter, + parseSortOrder, + requireTeamId, +}; diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 9124abd06..90ab5e7a5 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -14,16 +14,26 @@ import { getHardwareDetailsByIdQueryValidation, } from "@/validation/joi.js"; import sslChecker from "ssl-checker"; -import { fetchMonitorCertificate } from "./controllerUtils.js"; +import { + fetchMonitorCertificate, + requireString, + optionalString, + optionalNumber, + optionalBoolean, + parseMonitorTypeFilter, + parseSortOrder, + requireTeamId, +} from "./controllerUtils.js"; import { AppError } from "@/utils/AppError.js"; +import { IMonitorService } from "@/service/index.js"; const SERVICE_NAME = "monitorController"; class MonitorController { static SERVICE_NAME = SERVICE_NAME; - private monitorService: any; + private monitorService: IMonitorService; - constructor(monitorService: any) { + constructor(monitorService: IMonitorService) { this.monitorService = monitorService; } @@ -32,8 +42,8 @@ class MonitorController { } async verifyTeamAccess(teamId: string, monitorId: string) { - const monitor = await this.monitorService.getMonitorById(monitorId); - if (!monitor.teamId.equals(teamId)) { + const monitor = await this.monitorService.getMonitorById({ teamId, monitorId }); + if (monitor.teamId !== teamId) { throw new AppError({ message: "Access denied", status: 403 }); } } @@ -41,11 +51,8 @@ class MonitorController { getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => { try { await getCertificateParamValidation.validateAsync(req.params); - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } - const { monitorId } = req.params; + const teamId = requireTeamId(req?.user?.teamId); + const monitorId = requireString(req.params?.monitorId, "Monitor ID"); const monitor = await this.monitorService.getMonitorById({ teamId, monitorId }); const certificate = await fetchMonitorCertificate(sslChecker, monitor); @@ -63,15 +70,10 @@ class MonitorController { getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => { try { - const monitorId = req?.params?.monitorId; - const dateRange = req?.query?.dateRange; - const normalize = req?.query?.normalize; - - const teamId = req?.user?.teamId; - - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } + const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const dateRange = requireString(req?.query?.dateRange, "dateRange"); + const normalize = optionalBoolean(req?.query?.normalize, "normalize"); + const teamId = requireTeamId(req?.user?.teamId); const data = await this.monitorService.getUptimeDetailsById({ teamId, @@ -94,12 +96,9 @@ class MonitorController { await getHardwareDetailsByIdParamValidation.validateAsync(req.params); await getHardwareDetailsByIdQueryValidation.validateAsync(req.query); - const monitorId = req?.params?.monitorId; - const dateRange = req?.query?.dateRange; - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } + const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const dateRange = requireString(req?.query?.dateRange, "dateRange"); + const teamId = requireTeamId(req?.user?.teamId); const monitor = await this.monitorService.getHardwareDetailsById({ teamId, @@ -116,18 +115,40 @@ class MonitorController { next(error); } }; + getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => { + try { + await getHardwareDetailsByIdParamValidation.validateAsync(req.params); + await getHardwareDetailsByIdQueryValidation.validateAsync(req.query); + + const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const dateRange = requireString(req?.query?.dateRange, "dateRange"); + const teamId = requireTeamId(req?.user?.teamId); + + const monitor = await this.monitorService.getPageSpeedDetailsById({ + teamId, + monitorId, + dateRange, + }); + + return res.status(200).json({ + success: true, + msg: "Page speed details retrieved successfully", + data: monitor, + }); + } catch (error) { + next(error); + } + }; getMonitorById = async (req: Request, res: Response, next: NextFunction) => { try { await getMonitorByIdParamValidation.validateAsync(req.params); await getMonitorByIdQueryValidation.validateAsync(req.query); - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } + const teamId = requireTeamId(req?.user?.teamId); + const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); - const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: req?.params?.monitorId }); + const monitor = await this.monitorService.getMonitorById({ teamId, monitorId }); return res.status(200).json({ success: true, @@ -143,8 +164,8 @@ class MonitorController { try { await createMonitorBodyValidation.validateAsync(req.body); - const userId = req?.user?._id; - const teamId = req?.user?.teamId; + const userId = requireString(req?.user?._id, "User ID"); + const teamId = requireTeamId(req?.user?.teamId); const monitor = await this.monitorService.createMonitor(teamId, userId, req.body); @@ -172,12 +193,8 @@ class MonitorController { throw new AppError({ message: "File is empty", status: 400 }); } - const userId = req?.user?._id; - const teamId = req?.user?.teamId; - - if (!userId || !teamId) { - throw new AppError({ message: "Missing userId or teamId", status: 400 }); - } + const userId = requireString(req?.user?._id, "User ID"); + const teamId = requireTeamId(req?.user?.teamId); const fileData = req?.file?.buffer?.toString("utf-8"); if (!fileData) { @@ -199,11 +216,8 @@ class MonitorController { deleteMonitor = async (req: Request, res: Response, next: NextFunction) => { try { await getMonitorByIdParamValidation.validateAsync(req.params); - const monitorId = req.params.monitorId; - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } + const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const teamId = requireTeamId(req?.user?.teamId); const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId }); @@ -219,10 +233,7 @@ class MonitorController { deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } + const teamId = requireTeamId(req?.user?.teamId); const deletedCount = await this.monitorService.deleteAllMonitors({ teamId }); @@ -239,12 +250,8 @@ class MonitorController { try { await getMonitorByIdParamValidation.validateAsync(req.params); await editMonitorBodyValidation.validateAsync(req.body); - const monitorId = req?.params?.monitorId; - - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } + const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const teamId = requireTeamId(req?.user?.teamId); const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: req.body }); @@ -262,11 +269,8 @@ class MonitorController { try { await pauseMonitorParamValidation.validateAsync(req.params); - const monitorId = req.params.monitorId; - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } + const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); + const teamId = requireTeamId(req?.user?.teamId); const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId }); @@ -282,7 +286,8 @@ class MonitorController { addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => { try { - const { _id, teamId } = req.user; + const _id = requireString(req?.user?._id, "User ID"); + const teamId = requireTeamId(req?.user?.teamId); const demoMonitors = await this.monitorService.addDemoMonitors({ userId: _id, teamId }); return res.status(200).json({ @@ -318,8 +323,9 @@ class MonitorController { await getMonitorsByTeamIdParamValidation.validateAsync(req.params); await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); - const { type, filter } = req.query; - const teamId = req?.user?.teamId; + const teamId = requireTeamId(req?.user?.teamId); + const type = parseMonitorTypeFilter(req.query?.type); + const filter = optionalString(req.query?.filter, "filter"); const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, filter }); @@ -338,12 +344,9 @@ class MonitorController { await getMonitorsByTeamIdParamValidation.validateAsync(req.params); await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); - const explain = req?.query?.explain; - const type = req?.query?.type; - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } + const explain = optionalBoolean(req?.query?.explain, "explain"); + const type = parseMonitorTypeFilter(req?.query?.type); + const teamId = requireTeamId(req?.user?.teamId); const result = await this.monitorService.getMonitorsAndSummaryByTeamId({ teamId, type, explain }); @@ -361,12 +364,15 @@ class MonitorController { await getMonitorsByTeamIdParamValidation.validateAsync(req.params); await getMonitorsWithChecksQueryValidation.validateAsync(req.query); - const explain = req?.query?.explain; - let { limit, type, page, rowsPerPage, filter, field, order } = req.query; - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } + const explain = optionalBoolean(req?.query?.explain, "explain"); + const limit = optionalNumber(req?.query?.limit, "limit"); + const page = optionalNumber(req?.query?.page, "page"); + const rowsPerPage = optionalNumber(req?.query?.rowsPerPage, "rowsPerPage"); + const filter = optionalString(req?.query?.filter, "filter"); + const field = optionalString(req?.query?.field, "field"); + const order = parseSortOrder(req?.query?.order); + const type = parseMonitorTypeFilter(req?.query?.type); + const teamId = requireTeamId(req?.user?.teamId); const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({ teamId, diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index 507389e6a..cedea8d86 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -1,6 +1,58 @@ -import type { Check, MonitorType } from "@/types/index.js"; +import type { Check, CheckAudits, MonitorType } from "@/types/index.js"; import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js"; +export interface PageSpeedChecksResult { + monitorType: "pagespeed"; + checks: Check[]; +} + +export interface HardwareChecksResult { + monitorType: "hardware"; + aggregateData: { + latestCheck: Check | null; + totalChecks: number; + }; + upChecks: { + totalChecks: number; + }; + checks: Array<{ + _id: string; + avgCpuUsage: number; + avgMemoryUsage: number; + avgTemperature: number[]; + disks: Array<{ + name: string; + readSpeed: number; + writeSpeed: number; + totalBytes: number; + freeBytes: number; + usagePercent: number; + }>; + net: Array<{ + name: string; + bytesSentPerSecond: number; + deltaBytesRecv: number; + deltaPacketsSent: number; + deltaPacketsRecv: number; + deltaErrIn: number; + deltaErrOut: number; + deltaDropIn: number; + deltaDropOut: number; + deltaFifoIn: number; + deltaFifoOut: number; + }>; + }>; +} + +export interface UptimeChecksResult { + monitorType: Exclude; + groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>; + groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; + groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; + uptimePercentage: number; + avgResponseTime: number; +} + export interface IChecksRepository { findLatestChecksByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise; findDateRangeChecksByMonitor( @@ -9,51 +61,5 @@ export interface IChecksRepository { endDate: Date, dateString: string, options?: { type?: MonitorType } - ): Promise< - | { - monitorType: "uptime"; - groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>; - groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; - groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; - uptimePercentage: number; - avgResponseTime: number; - } - | { - monitorType: "hardware"; - aggregateData: { - latestCheck: Check | null; - totalChecks: number; - }; - upChecks: { - totalChecks: number; - }; - checks: Array<{ - _id: string; - avgCpuUsage: number; - avgMemoryUsage: number; - avgTemperature: number[]; - disks: Array<{ - name: string; - readSpeed: number; - writeSpeed: number; - totalBytes: number; - freeBytes: number; - usagePercent: number; - }>; - net: Array<{ - name: string; - bytesSentPerSecond: number; - deltaBytesRecv: number; - deltaPacketsSent: number; - deltaPacketsRecv: number; - deltaErrIn: number; - deltaErrOut: number; - deltaDropIn: number; - deltaDropOut: number; - deltaFifoIn: number; - deltaFifoOut: number; - }>; - }>; - } - >; + ): Promise; } diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 2aeb32b2d..d9466b4d8 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -11,6 +11,7 @@ import type { CheckMetadata, CheckNetworkInterfaceInfo, CheckTimings, + MonitorType, } from "@/types/index.js"; import { CheckModel, type CheckDocument } from "@/db/models/index.js"; import mongoose from "mongoose"; @@ -177,10 +178,7 @@ class MongoChecksRepistory implements IChecksRepository { }; }; - findLatestChecksByMonitorIds = async ( - monitorIds: string[], - options?: { limitPerMonitor?: number } - ): Promise => { + findLatestChecksByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise => { if (monitorIds.length === 0) { return {}; } @@ -218,15 +216,24 @@ class MongoChecksRepistory implements IChecksRepository { }, {}); }; - findDateRangeChecksByMonitor = async (monitorId: string, startDate: Date, endDate: Date, dateString: string, options?: { type?: string }) => { + findDateRangeChecksByMonitor = async (monitorId: string, startDate: Date, endDate: Date, dateString: string, options?: { type?: MonitorType }) => { const monitorObjectId = new mongoose.Types.ObjectId(monitorId); if (options?.type === "hardware") { return this.findHardwareDateRangeChecks(monitorObjectId, startDate, endDate, dateString); } - return this.findUptimeDateRangeChecks(monitorObjectId, startDate, endDate, dateString); + if (options?.type === "pagespeed") { + return this.findPageSpeedDateRangeChecks(monitorObjectId, startDate, endDate); + } + return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, startDate, endDate, dateString); }; - private findUptimeDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date, dateString: string) => { + private findUptimeDateRangeChecks = async ( + monitorType: Exclude, + monitorObjectId: mongoose.Types.ObjectId, + startDate: Date, + endDate: Date, + dateString: string + ) => { const matchStage = { "metadata.monitorId": monitorObjectId, updatedAt: { $gte: startDate, $lte: endDate }, @@ -307,7 +314,7 @@ class MongoChecksRepistory implements IChecksRepository { const avgResponseTime = result?.groupedAvgResponseTime?.[0]?.avgResponseTime ?? 0; return { - monitorType: "uptime" as const, + monitorType, groupedChecks: result?.groupedChecks ?? [], groupedUpChecks: result?.groupedUpChecks ?? [], groupedDownChecks: result?.groupedDownChecks ?? [], @@ -369,6 +376,19 @@ class MongoChecksRepistory implements IChecksRepository { checks, }; }; + + private findPageSpeedDateRangeChecks = async (monitorObjectId: mongoose.Types.ObjectId, startDate: Date, endDate: Date) => { + const matchStage = { + "metadata.monitorId": monitorObjectId, + createdAt: { $gte: startDate, $lte: endDate }, + }; + + const checks = await CheckModel.find(matchStage).sort({ createdAt: -1 }).limit(25).lean(); + return { + monitorType: "pagespeed" as const, + checks: checks.map((doc) => this.toEntity(doc)), + }; + }; } export default MongoChecksRepistory; diff --git a/server/src/routes/v1/monitorRoute.ts b/server/src/routes/v1/monitorRoute.ts index bcbf5a1c6..24bc64ec8 100755 --- a/server/src/routes/v1/monitorRoute.ts +++ b/server/src/routes/v1/monitorRoute.ts @@ -27,6 +27,8 @@ class MonitorRoutes { // Hardware routes this.router.get("/hardware/details/:monitorId", this.monitorController.getHardwareDetailsById); + // PageSpeed routes + this.router.get("/pagespeed/details/:monitorId", this.monitorController.getPageSpeedDetailsById); // General monitor routes this.router.post("/pause/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.pauseMonitor); diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 46731446f..3793ac35e 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -24,6 +24,7 @@ export interface IMonitorService { // read getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise; getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; + getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; getMonitorById(args: { teamId: string; monitorId: string }): Promise; getMonitorsByTeamId(args: { teamId: string; @@ -268,7 +269,13 @@ export class MonitorService implements IMonitorService { }); const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id); - if (checksData.monitorType !== "uptime") { + if ( + checksData.monitorType !== "http" && + checksData.monitorType !== "ping" && + checksData.monitorType !== "docker" && + checksData.monitorType !== "port" && + checksData.monitorType !== "game" + ) { throw new AppError({ message: `${monitor.type} monitors are not supported for uptime details`, status: 400 }); } @@ -317,6 +324,30 @@ export class MonitorService implements IMonitorService { }; }; + getPageSpeedDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise => { + await this.verifyTeamAccess({ teamId, monitorId }); + const monitor = await this.monitorsRepository.findById(monitorId, teamId); + if (!monitor) { + throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); + } + if (monitor.type !== "pagespeed") { + throw new AppError({ message: `${monitor.type} monitors are not supported for pagespeed details`, status: 400 }); + } + + const rangeKey = (dateRange as DateRangeKey) ?? "recent"; + const { start, end } = this.getDateRange(rangeKey); + const checksData = await this.checksRepository.findDateRangeChecksByMonitor(monitor.id, start, end, this.getDateFormat(rangeKey), { + type: monitor.type, + }); + + if (checksData.monitorType !== "pagespeed") { + throw new AppError({ message: "Unable to load pagespeed stats for this monitor", status: 500 }); + } + return { + ...monitor, + checks: checksData.checks, + }; + }; getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { await this.verifyTeamAccess({ teamId, monitorId }); const monitor = await this.monitorsRepository.findById(monitorId, teamId); From aea0b3453304a024387fab503810447ed038c212 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 14 Jan 2026 22:45:39 +0000 Subject: [PATCH 211/400] format --- server/src/db/models/index.ts | 2 +- server/test/monitorService.test.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index 29f3539c9..3ac99a390 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -5,4 +5,4 @@ export * from "@/db/models/Check.js"; export { default as CheckModel } from "@/db/models/Check.js"; export * from "@/db/models/MonitorStats.js"; -export { default as MonitorStatsModel } from "@/db/models/MonitorStats.js"; \ No newline at end of file +export { default as MonitorStatsModel } from "@/db/models/MonitorStats.js"; diff --git a/server/test/monitorService.test.ts b/server/test/monitorService.test.ts index 4fdcbeb49..7a3bbe12f 100644 --- a/server/test/monitorService.test.ts +++ b/server/test/monitorService.test.ts @@ -213,6 +213,5 @@ describe("MonitorService", () => { }) ); }); - }); }); From 1326a6222b347996a42170acbcf58b257a04d3a8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 14 Jan 2026 22:45:47 +0000 Subject: [PATCH 212/400] format --- client/src/Components/v1/Inputs/Search/index.jsx | 11 +++++++++-- .../CreateMaintenance/hooks/useMaintenanceData.jsx | 4 +++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/client/src/Components/v1/Inputs/Search/index.jsx b/client/src/Components/v1/Inputs/Search/index.jsx index 347223bf3..8138ee6f4 100644 --- a/client/src/Components/v1/Inputs/Search/index.jsx +++ b/client/src/Components/v1/Inputs/Search/index.jsx @@ -85,7 +85,12 @@ const Search = ({ const enhancedOptions = React.useMemo(() => { return multiple && isAdorned ? [ - { [filteredBy]: t("selectAll"), isSelectAll: true, _id: "select_all", id: "select_all" }, + { + [filteredBy]: t("selectAll"), + isSelectAll: true, + _id: "select_all", + id: "select_all", + }, ...options, ] : options; @@ -145,7 +150,9 @@ const Search = ({ disableClearable options={enhancedOptions} getOptionLabel={(option) => option[filteredBy]} - isOptionEqualToValue={(option, value) => (option._id ?? option.id) === (value?._id ?? value?.id)} // Compare by unique identifier + isOptionEqualToValue={(option, value) => + (option._id ?? option.id) === (value?._id ?? value?.id) + } // Compare by unique identifier renderInput={(params) => ( { const endTime = dayjs(end); const durationInMs = endTime.diff(startTime, "milliseconds").toString(); const { duration, durationUnit } = getDurationAndUnit(durationInMs); - const monitor = monitorList.find((monitor) => (monitor._id ?? monitor.id) === monitorId); + const monitor = monitorList.find( + (monitor) => (monitor._id ?? monitor.id) === monitorId + ); const maintenanceWindowInformation = { name, repeat: REVERSE_REPEAT_LOOKUP[repeat], From cdbaad5bd90616a40cb167d16fe2a99c56f396a2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 14 Jan 2026 23:34:05 +0000 Subject: [PATCH 213/400] initial --- client/src/Hooks/monitorHooks.js | 6 ++- client/src/Pages/Uptime/Monitors/index.jsx | 28 +++++------- server/src/db/modules/monitorModule.js | 13 +++--- .../monitors/IMonitorsRepository.ts | 7 +++ .../monitors/MongoMonitorsRepository.ts | 44 +++++++++++++++++-- server/src/service/business/monitorService.ts | 7 +-- .../service/infrastructure/statusService.js | 9 ---- 7 files changed, 72 insertions(+), 42 deletions(-) diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index bc391ebf6..92844ad16 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -50,6 +50,7 @@ export const useFetchMonitorsWithChecks = ({ const [isLoading, setIsLoading] = useState(false); const [count, setCount] = useState(undefined); const [monitors, setMonitors] = useState(undefined); + const [summary, setSummary] = useState(undefined); const [networkError, setNetworkError] = useState(false); const theme = useTheme(); @@ -68,10 +69,11 @@ export const useFetchMonitorsWithChecks = ({ order, }); - const { count, monitors } = res?.data?.data ?? {}; + const { count, monitors, summary } = res?.data?.data ?? {}; const mappedMonitors = monitors.map((monitor) => getMonitorWithPercentage(monitor, theme) ); + setSummary(summary); setMonitors(mappedMonitors); setCount(count || 0); } catch (error) { @@ -97,7 +99,7 @@ export const useFetchMonitorsWithChecks = ({ types, monitorUpdateTrigger, ]); - return [monitors, count, isLoading, networkError]; + return [summary, monitors, count, isLoading, networkError]; }; export const useFetchMonitorsByTeamId = ({ types, filter, updateTrigger }) => { diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/index.jsx index fb8d6246b..e5bcf45fb 100644 --- a/client/src/Pages/Uptime/Monitors/index.jsx +++ b/client/src/Pages/Uptime/Monitors/index.jsx @@ -26,10 +26,7 @@ import { useNavigate } from "react-router-dom"; import { useSelector, useDispatch } from "react-redux"; import { setRowsPerPage } from "../../../Features/UI/uiSlice.js"; import PropTypes from "prop-types"; -import { - useFetchMonitorsWithSummary, - useFetchMonitorsWithChecks, -} from "@/Hooks/monitorHooks.js"; +import { useFetchMonitorsWithChecks } from "@/Hooks/monitorHooks.js"; import { useTranslation } from "react-i18next"; const TYPES = ["http", "ping", "docker", "port", "game"]; @@ -103,12 +100,6 @@ const UptimeMonitors = () => { setMonitorUpdateTrigger((prev) => !prev); }, []); - const [monitors, monitorsSummary, monitorsWithSummaryIsLoading, networkError] = - useFetchMonitorsWithSummary({ - types: TYPES, - monitorUpdateTrigger, - }); - const handleReset = () => { setSelectedState(undefined); setSelectedTypes(undefined); @@ -129,10 +120,11 @@ const UptimeMonitors = () => { const effectiveTypes = selectedTypes?.length ? selectedTypes : TYPES; const [ + summary, monitorsWithChecks, monitorsWithChecksCount, monitorsWithChecksIsLoading, - monitorsWithChecksNetworkError, + networkError, ] = useFetchMonitorsWithChecks({ types: effectiveTypes, limit: 25, @@ -150,14 +142,14 @@ const UptimeMonitors = () => { } }, [isSearching]); - const isLoading = monitorsWithSummaryIsLoading || monitorsWithChecksIsLoading; + const isLoading = monitorsWithChecksIsLoading; return ( <> @@ -174,14 +166,14 @@ const UptimeMonitors = () => { /> { handleReset={handleReset} /> diff --git a/server/src/db/modules/monitorModule.js b/server/src/db/modules/monitorModule.js index 84ff86996..e13da4b59 100755 --- a/server/src/db/modules/monitorModule.js +++ b/server/src/db/modules/monitorModule.js @@ -318,23 +318,22 @@ class MonitorModule { } }; - getMonitorsAndSummaryByTeamId = async ({ type, explain, teamId }) => { + findMonitorsSummaryByTeamId = async ({ type, teamId, explain }) => { try { const matchStage = { teamId: new this.ObjectId(teamId) }; if (type !== undefined) { matchStage.type = Array.isArray(type) ? { $in: type } : type; } + const pipeline = buildMonitorsSummaryByTeamIdPipeline({ matchStage }); if (explain === true) { - return this.Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })).explain("executionStats"); + return this.Monitor.aggregate(pipeline).explain("executionStats"); } - - const queryResult = await this.Monitor.aggregate(buildMonitorsAndSummaryByTeamIdPipeline({ matchStage })); - const { monitors, summary } = queryResult?.[0] ?? {}; - return { monitors, summary }; + const [summary] = await this.Monitor.aggregate(pipeline); + return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0 }; } catch (error) { error.service = SERVICE_NAME; - error.method = "getMonitorsAndSummaryByTeamId"; + error.method = "findMonitorsSummaryByTeamId"; throw error; } }; diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 0da9c7279..50fc79b2b 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -10,6 +10,10 @@ export interface TeamQueryConfig { order?: "asc" | "desc"; } +export interface SummaryConfig { + type?: MonitorType | MonitorType[]; +} + export interface IMonitorsRepository { // create create(monitor: Monitor, teamId: string, userId: string): Promise; @@ -27,4 +31,7 @@ export interface IMonitorsRepository { // counts findMonitorCountByTeamIdAndType(teamId: string, config: TeamQueryConfig): Promise; + + // other + findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise; } diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index c9af6668d..d839b1a78 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -1,8 +1,8 @@ import { MonitorModel } from "@/db/models/index.js"; -import type { MonitorDocument } from "@/db/models/Monitor.js"; -import type { Monitor, MonitorType } from "@/types/monitor.js"; +import type { MonitorDocument } from "@/db/models/index.js"; +import type { Monitor, MonitorType } from "@/types/index.js"; import mongoose, { type FilterQuery } from "mongoose"; -import type { IMonitorsRepository, TeamQueryConfig } from "./IMonitorsRepository.js"; +import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js"; import { AppError } from "@/utils/AppError.js"; class MongoMonitorsRepository implements IMonitorsRepository { @@ -120,6 +120,44 @@ class MongoMonitorsRepository implements IMonitorsRepository { return { monitors: this.mapDocuments(monitors), deletedCount }; }; + findMonitorsSummaryByTeamId = async ( + teamId: string, + config?: SummaryConfig + ): Promise<{ totalMonitors: number; upMonitors: number; downMonitors: number; pausedMonitors: number }> => { + const match: FilterQuery = { teamId: new mongoose.Types.ObjectId(teamId) }; + if (config?.type !== undefined) { + match.type = Array.isArray(config.type) ? { $in: config.type } : config.type; + } + const pipeline = [ + { $match: match }, + { + $group: { + _id: null, + totalMonitors: { $sum: 1 }, + upMonitors: { + $sum: { + $cond: [{ $eq: ["$status", true] }, 1, 0], + }, + }, + downMonitors: { + $sum: { + $cond: [{ $eq: ["$status", false] }, 1, 0], + }, + }, + pausedMonitors: { + $sum: { + $cond: [{ $eq: ["$isActive", false] }, 1, 0], + }, + }, + }, + }, + { $project: { _id: 0 } }, + ]; + + const [summary] = await MonitorModel.aggregate(pipeline); + return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0 }; + }; + private mapDocuments = (documents: MonitorDocument[]): Monitor[] => { if (!documents?.length) { return []; diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 3793ac35e..e88fdbea3 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -47,7 +47,7 @@ export interface IMonitorService { field?: string; order?: "asc" | "desc"; explain?: boolean; - }): Promise<{ count: number; monitors: any[] }>; + }): Promise<{ summary: any; count: number; monitors: any[] }>; getAllGames(): any; getGroupsByTeamId(args: { teamId: string }): Promise; @@ -399,7 +399,8 @@ export class MonitorService implements IMonitorService { field?: string; order?: "asc" | "desc"; explain?: boolean; - }): Promise<{ count: number; monitors: any[] }> => { + }): Promise<{ summary: any; count: number; monitors: any[] }> => { + const summary = await this.monitorsRepository.findMonitorsSummaryByTeamId(teamId); const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter }); const monitors = await this.monitorsRepository.findByTeamId(teamId, { limit, @@ -433,7 +434,7 @@ export class MonitorService implements IMonitorService { }; }); - return { count, monitors: monitorsWithChecks }; + return { summary: summary ?? null, count, monitors: monitorsWithChecks }; }; getAllGames = (): any => { diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index e849bbb83..105f89c44 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -5,15 +5,6 @@ const SERVICE_NAME = "StatusService"; class StatusService { static SERVICE_NAME = SERVICE_NAME; - /** - * @param {{ - * db: any - * logger: any - * buffer: import("./bufferService.js").BufferService - * incidentService: import("../business/incidentService.js").IncidentService - * monitorsRepository: any - * }} - */ constructor({ db, logger, buffer, incidentService, monitorsRepository }) { this.db = db; this.logger = logger; From 93c24d2f5ce232bdf85435ea8dce724b4f069d63 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 14 Jan 2026 15:44:52 -0800 Subject: [PATCH 214/400] pagination and filter fix --- client/src/Pages/Uptime/Monitors/index.jsx | 2 +- server/src/controllers/monitorController.ts | 20 ------------------- .../checks/MongoChecksRepistory.ts | 4 ++-- .../monitors/MongoMonitorsRepository.ts | 5 ++--- server/src/routes/v1/monitorRoute.ts | 1 - server/src/service/business/monitorService.ts | 18 ----------------- 6 files changed, 5 insertions(+), 45 deletions(-) diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/index.jsx index e5bcf45fb..4328715f2 100644 --- a/client/src/Pages/Uptime/Monitors/index.jsx +++ b/client/src/Pages/Uptime/Monitors/index.jsx @@ -114,7 +114,7 @@ const UptimeMonitors = () => { ]); const activeFilter = [...filterLookup].find(([key]) => key !== undefined); - const field = activeFilter?.[1] || sort?.field; + const field = activeFilter?.[1] || (search ? "name" : sort?.field); const filter = activeFilter?.[0] || search; const effectiveTypes = selectedTypes?.length ? selectedTypes : TYPES; diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 90ab5e7a5..859a8fa4b 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -339,26 +339,6 @@ class MonitorController { } }; - getMonitorsAndSummaryByTeamId = async (req: Request, res: Response, next: NextFunction) => { - try { - await getMonitorsByTeamIdParamValidation.validateAsync(req.params); - await getMonitorsByTeamIdQueryValidation.validateAsync(req.query); - - const explain = optionalBoolean(req?.query?.explain, "explain"); - const type = parseMonitorTypeFilter(req?.query?.type); - const teamId = requireTeamId(req?.user?.teamId); - - const result = await this.monitorService.getMonitorsAndSummaryByTeamId({ teamId, type, explain }); - - return res.status(200).json({ - msg: "Monitors and summary retrieved successfully", - data: result, - }); - } catch (error) { - next(error); - } - }; - getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { await getMonitorsByTeamIdParamValidation.validateAsync(req.params); diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 7ede5587e..a6a049840 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -23,7 +23,7 @@ import { export type LatestChecksMap = Record; -class MongoChecksRepistory implements IChecksRepository { +class MongoChecksRepository implements IChecksRepository { private toEntity = (doc: CheckDocument): Check => { const toStringId = (value: mongoose.Types.ObjectId | string | undefined | null): string => { if (!value) { @@ -392,4 +392,4 @@ class MongoChecksRepistory implements IChecksRepository { }; } -export default MongoChecksRepistory; +export default MongoChecksRepository; diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index d839b1a78..970371b17 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -44,7 +44,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { }; findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise => { - const { page = 0, rowsPerPage = 25, filter, field = "createdAt", order = "desc", type, limit } = config ?? {}; + const { page = 0, rowsPerPage = 5, filter, field = "createdAt", order = "desc", type, limit } = config ?? {}; const query: Record = { teamId: new mongoose.Types.ObjectId(teamId), @@ -75,9 +75,8 @@ class MongoMonitorsRepository implements IMonitorsRepository { const sort = { [field]: order === "asc" ? 1 : -1 } as const; const skip = Math.max(page, 0) * rowsPerPage; - const limitValue = limit ?? rowsPerPage; - const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(limitValue).exec(); + const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(rowsPerPage); return this.mapDocuments(documents); }; diff --git a/server/src/routes/v1/monitorRoute.ts b/server/src/routes/v1/monitorRoute.ts index 24bc64ec8..ab6b0c98e 100755 --- a/server/src/routes/v1/monitorRoute.ts +++ b/server/src/routes/v1/monitorRoute.ts @@ -19,7 +19,6 @@ class MonitorRoutes { // Team routes this.router.get("/team", this.monitorController.getMonitorsByTeamId); this.router.get("/team/with-checks", this.monitorController.getMonitorsWithChecksByTeamId); - this.router.get("/team/summary", this.monitorController.getMonitorsAndSummaryByTeamId); this.router.get("/team/groups", this.monitorController.getGroupsByTeamId); // Uptime routes diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index e88fdbea3..0a767779e 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -36,7 +36,6 @@ export interface IMonitorService { field?: string; order?: "asc" | "desc"; }): Promise; - getMonitorsAndSummaryByTeamId(args: { teamId: string; type?: string | string[]; explain?: boolean }): Promise; getMonitorsWithChecksByTeamId(args: { teamId: string; limit?: number; @@ -362,23 +361,6 @@ export class MonitorService implements IMonitorService { return monitors; }; - getMonitorsAndSummaryByTeamId = async ({ - teamId, - type, - explain, - }: { - teamId: string; - type?: string | string[]; - explain?: boolean; - }): Promise => { - const result = await this.db.monitorModule.getMonitorsAndSummaryByTeamId({ - type, - explain, - teamId, - }); - return result; - }; - getMonitorsWithChecksByTeamId = async ({ teamId, limit, From e7e4ce7f57112389c08a834c42ae060be8f8b6ea Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 14 Jan 2026 15:57:17 -0800 Subject: [PATCH 215/400] pagination fix --- client/src/Pages/Infrastructure/Monitors/index.jsx | 2 +- .../Monitors/Components/MonitorGrid/index.jsx | 1 + client/src/Pages/PageSpeed/Monitors/index.jsx | 1 + client/src/Pages/Uptime/Monitors/index.jsx | 14 +++++++++++--- .../monitors/MongoMonitorsRepository.ts | 2 +- 5 files changed, 15 insertions(+), 5 deletions(-) diff --git a/client/src/Pages/Infrastructure/Monitors/index.jsx b/client/src/Pages/Infrastructure/Monitors/index.jsx index b25636fa9..e4add415c 100644 --- a/client/src/Pages/Infrastructure/Monitors/index.jsx +++ b/client/src/Pages/Infrastructure/Monitors/index.jsx @@ -71,7 +71,7 @@ const InfrastructureMonitors = () => { const field = toFilterStatus !== undefined ? "status" : undefined; - const [monitors, count, isLoading, networkError] = useFetchMonitorsWithChecks({ + const [summary, monitors, count, isLoading, networkError] = useFetchMonitorsWithChecks({ types: TYPES, limit: 1, page: page, diff --git a/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx b/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx index 6b0a800fb..8708502cd 100644 --- a/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx +++ b/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx @@ -2,6 +2,7 @@ import { Grid, Grid2 } from "@mui/material"; import Card from "../Card/index.jsx"; const MonitorGrid = ({ shouldRender, monitors }) => { + console.log(monitors); return ( { const isAdmin = useIsAdmin(); const [ + summary, monitorsWithChecks, monitorsWithChecksCount, monitorsWithChecksIsLoading, diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/index.jsx index 4328715f2..101020eea 100644 --- a/client/src/Pages/Uptime/Monitors/index.jsx +++ b/client/src/Pages/Uptime/Monitors/index.jsx @@ -26,7 +26,10 @@ import { useNavigate } from "react-router-dom"; import { useSelector, useDispatch } from "react-redux"; import { setRowsPerPage } from "../../../Features/UI/uiSlice.js"; import PropTypes from "prop-types"; -import { useFetchMonitorsWithChecks } from "@/Hooks/monitorHooks.js"; +import { + useFetchMonitorsWithChecks, + useFetchMonitorsByTeamId, +} from "@/Hooks/monitorHooks.js"; import { useTranslation } from "react-i18next"; const TYPES = ["http", "ping", "docker", "port", "game"]; @@ -136,6 +139,11 @@ const UptimeMonitors = () => { monitorUpdateTrigger, }); + const [monitors, listIsLoading, listNetworkError] = useFetchMonitorsByTeamId({ + type: ["http", "ping", "docker", "port", "game"], + }); + + console.log(monitors); useEffect(() => { if (isSearching) { setPage(undefined); @@ -149,7 +157,7 @@ const UptimeMonitors = () => { @@ -187,7 +195,7 @@ const UptimeMonitors = () => { handleReset={handleReset} /> diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 970371b17..a7b933851 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -44,7 +44,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { }; findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise => { - const { page = 0, rowsPerPage = 5, filter, field = "createdAt", order = "desc", type, limit } = config ?? {}; + const { page = 0, rowsPerPage = 0, filter, field = "createdAt", order = "desc", type, limit } = config ?? {}; const query: Record = { teamId: new mongoose.Types.ObjectId(teamId), From 1c01dcaf31e0fcdc7cfeba5a3f11ff52e3be7b21 Mon Sep 17 00:00:00 2001 From: karenvicent Date: Wed, 14 Jan 2026 22:12:26 -0500 Subject: [PATCH 216/400] create incidents action menu and detail page --- .../IncidentTable/IncidentActionsMenu.jsx | 204 ++++++++++++++++++ .../Components/IncidentTable/index.jsx | 39 +--- .../LatestIncidentsPanel/IncidentItem.jsx | 28 +-- .../src/Pages/v1/Incidents2/Details/index.jsx | 171 +++++++++++++++ .../hooks/useGetIncidentsDuration.js | 25 +++ client/src/Routes/index.jsx | 5 + client/src/locales/en.json | 12 +- 7 files changed, 428 insertions(+), 56 deletions(-) create mode 100644 client/src/Pages/v1/Incidents2/Components/IncidentTable/IncidentActionsMenu.jsx create mode 100644 client/src/Pages/v1/Incidents2/Details/index.jsx create mode 100644 client/src/Pages/v1/Incidents2/hooks/useGetIncidentsDuration.js diff --git a/client/src/Pages/v1/Incidents2/Components/IncidentTable/IncidentActionsMenu.jsx b/client/src/Pages/v1/Incidents2/Components/IncidentTable/IncidentActionsMenu.jsx new file mode 100644 index 000000000..59749fca7 --- /dev/null +++ b/client/src/Pages/v1/Incidents2/Components/IncidentTable/IncidentActionsMenu.jsx @@ -0,0 +1,204 @@ +// Components +import IconButton from "@mui/material/IconButton"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import Settings from "../../../../../assets/icons/settings-bold.svg?react"; +import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; +import { Button, Stack } from "@mui/material"; +import useFetchIncidents from "../../hooks/useFetchIncidents"; + +// Utils +import { useState } from "react"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; +import { TypeToPathMap } from "@/Utils/monitorUtils.js"; +import PropTypes from "prop-types"; + +const IncidentActionsMenu = ({ incident, onResolve }) => { + const [anchorEl, setAnchorEl] = useState(null); + const [isDialogOpen, setIsDialogOpen] = useState(false); + const [comment, setComment] = useState(""); + const theme = useTheme(); + const { t } = useTranslation(); + const navigate = useNavigate(); + const { fetchIncidentById } = useFetchIncidents(); + const openMenu = (event) => { + event.preventDefault(); + event.stopPropagation(); + setAnchorEl(event.currentTarget); + }; + + const closeMenu = (e) => { + if (e) { + e.stopPropagation(); + } + setAnchorEl(null); + }; + + const openResolveDialog = (e) => { + closeMenu(e); + setComment(""); + setIsDialogOpen(true); + }; + + const closeDialog = (e) => { + if (e) { + e.stopPropagation(); + } + setComment(""); + setIsDialogOpen(false); + }; + + const handleConfirmResolve = (e) => { + e.stopPropagation(); + if (onResolve) { + const options = comment.trim() ? { comment: comment.trim() } : {}; + onResolve(incident._id, options); + } + setComment(""); + setIsDialogOpen(false); + }; + + const isActive = incident.status === true; + const handleGoToDetails = (e) => { + e.stopPropagation(); + closeMenu(e); + navigate(`/incidents/details/${incident._id}`); + }; + + const handleGoToMonitor = async (e) => { + e.stopPropagation(); + closeMenu(e); + try { + const incidentData = await fetchIncidentById(incident._id); + console.log(incidentData); + if (incidentData) { + const { type, _id: monitorId } = incidentData.monitorId; + const path = TypeToPathMap[type]; + if (path && monitorId) { + navigate(`/${path}/${monitorId}`); + } else { + console.error(`Monitor type not found`); + } + } else { + console.error("Monitor information not found in incident data"); + } + } catch (error) { + console.error("Error fetching incident details:", error); + } + }; + + return ( + <> + { + event.stopPropagation(); + openMenu(event); + }} + sx={{ + "&:focus": { + outline: "none", + }, + "& svg path": { + stroke: theme.palette.primary.contrastTextTertiary, + }, + }} + > + + + + + {isActive && ( + { + e.stopPropagation(); + openResolveDialog(e); + }} + > + {t("incidentsPage.incidentsTableActionResolveManually")} + + )} + + {t("incidentsPage.incidentsTableActionDetails", "Details")} + + + {t("incidentsPage.incidentsTableActionGoToMonitor", "Go to monitor")} + + + Add comment + + {/* Future options will be added here */} + + + + setComment(e.target.value)} + maxWidth="100%" + /> + + + + + + + + ); +}; + +IncidentActionsMenu.propTypes = { + incident: PropTypes.shape({ + _id: PropTypes.string.isRequired, + status: PropTypes.bool.isRequired, + }).isRequired, + onResolve: PropTypes.func.isRequired, +}; + +export default IncidentActionsMenu; diff --git a/client/src/Pages/v1/Incidents2/Components/IncidentTable/index.jsx b/client/src/Pages/v1/Incidents2/Components/IncidentTable/index.jsx index 8cac4e07b..5456d55f2 100644 --- a/client/src/Pages/v1/Incidents2/Components/IncidentTable/index.jsx +++ b/client/src/Pages/v1/Incidents2/Components/IncidentTable/index.jsx @@ -6,13 +6,14 @@ import { StatusLabel } from "@/Components/v1/Label/index.jsx"; import { HttpStatusLabel } from "@/Components/v1/HttpStatusLabel/index.jsx"; import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; +import IncidentActionsMenu from "./IncidentActionsMenu.jsx"; //Utils import { formatDateWithTz } from "../../../../../Utils/timeUtils.js"; import { useSelector } from "react-redux"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; -import { Button, Typography, useTheme } from "@mui/material"; +import { Typography, useTheme } from "@mui/material"; const IncidentTable = ({ incidents = [], @@ -31,9 +32,9 @@ const IncidentTable = ({ const { t } = useTranslation(); const theme = useTheme(); - const handleResolveIncident = async (incidentId) => { + const handleResolveIncident = async (incidentId, options = {}) => { try { - await resolveIncident(incidentId); + await resolveIncident(incidentId, options); handleUpdateTrigger(); } catch (error) { console.error(t("incidentsPage.errorResolvingIncident"), error); @@ -120,32 +121,12 @@ const IncidentTable = ({ id: "action", content: t("actions"), render: (row) => { - if (row.status === true) { - return ( - - ); - } else { - return ( - - {t("incidentsPage.incidentsTableResolved")} - - ); - } + return ( + + ); }, }, ]; diff --git a/client/src/Pages/v1/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx b/client/src/Pages/v1/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx index c212c8e52..dff710597 100644 --- a/client/src/Pages/v1/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx +++ b/client/src/Pages/v1/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx @@ -2,40 +2,16 @@ import PropTypes from "prop-types"; import { Box, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import { getHumanReadableDuration } from "@/Utils/timeUtils.js"; import Monitors from "@/assets/icons/monitors.svg?react"; import AverageResponseIcon from "@/assets/icons/status-pages.svg?react"; import { useTranslation } from "react-i18next"; +import useGetIncidentsDuration from "../../hooks/useGetIncidentsDuration"; const IncidentItem = ({ incident }) => { const { t } = useTranslation(); const theme = useTheme(); const isActive = incident.status === true; - const calculateDuration = () => { - if (!incident.startTime) { - return "-"; - } - const startTime = new Date(incident.startTime); - const endTime = isActive - ? new Date() - : incident.endTime - ? new Date(incident.endTime) - : null; - - if (!endTime) { - return "-"; - } - - const durationMs = endTime.getTime() - startTime.getTime(); - - if (durationMs < 0) { - return "-"; - } - - return getHumanReadableDuration(durationMs); - }; - - const duration = calculateDuration(); + const duration = useGetIncidentsDuration(incident, isActive); const iconWrapperStyle = { px: theme.spacing(2), display: "flex", diff --git a/client/src/Pages/v1/Incidents2/Details/index.jsx b/client/src/Pages/v1/Incidents2/Details/index.jsx new file mode 100644 index 000000000..67388899d --- /dev/null +++ b/client/src/Pages/v1/Incidents2/Details/index.jsx @@ -0,0 +1,171 @@ +// Components +import { Stack, Typography, Grid, Box } from "@mui/material"; +import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; +import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; +import SummaryCard from "../Components/SummaryCard"; +import useGetIncidentsDuration from "../hooks/useGetIncidentsDuration"; + +// Utils +import { useParams } from "react-router-dom"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import useFetchIncidents from "../hooks/useFetchIncidents"; +import { useEffect, useState } from "react"; + +// Constants +const BREADCRUMBS = [ + { name: "incidents", path: "/incidents" }, + { name: "details", path: "" }, +]; + +const IncidentDetails = () => { + const theme = useTheme(); + const { t } = useTranslation(); + const { incidentId } = useParams(); + const { fetchIncidentById } = useFetchIncidents(); + const [incident, setIncident] = useState(null); + const [isLoading, setIsLoading] = useState(true); + const isActive = incident?.status === true; + const duration = useGetIncidentsDuration(incident || null, isActive); + useEffect(() => { + const loadIncident = async () => { + if (incidentId) { + setIsLoading(true); + try { + const incidentData = await fetchIncidentById(incidentId); + console.log(incidentData); + setIncident(incidentData); + } catch (error) { + console.error("Error fetching incident:", error); + } finally { + setIsLoading(false); + } + } + }; + + loadIncident(); + }, [incidentId, fetchIncidentById]); + + if (isLoading) { + return ( + + + + + ); + } + + if (!incident) { + return ( + + + + + ); + } + + return ( + + + + {t("incidentsPage.incidentDetails", "Incident Details")} + + + + + + {duration} + + + + + {incident?.startTime} + + + {!isActive && ( + + + {incident?.endTime} + + + )} + + + + + {incident?.statusCode} + {incident?.message} + + + + + {incident?.checks.length} + + + {!isActive && ( + + + {incident?.resolutionType} + {incident?.resolutionType === "manual" && ( + + + Resolved by {incident?.resolvedBy?.email} + + {incident?.comment && ( + Comment: {incident?.comment} + )} + + )} + + + )} + + + + ); +}; + +export default IncidentDetails; diff --git a/client/src/Pages/v1/Incidents2/hooks/useGetIncidentsDuration.js b/client/src/Pages/v1/Incidents2/hooks/useGetIncidentsDuration.js new file mode 100644 index 000000000..e9b1da6d1 --- /dev/null +++ b/client/src/Pages/v1/Incidents2/hooks/useGetIncidentsDuration.js @@ -0,0 +1,25 @@ +import { getHumanReadableDuration } from "@/Utils/timeUtils.js"; +const useGetIncidentsDuration = (incident, isActive) => { + if (!incident?.startTime) { + return "-"; + } + const startTime = new Date(incident?.startTime); + const endTime = isActive + ? new Date() + : incident?.endTime + ? new Date(incident.endTime) + : null; + + if (!endTime) { + return "-"; + } + + const durationMs = endTime.getTime() - startTime.getTime(); + + if (durationMs < 0) { + return "-"; + } + + return getHumanReadableDuration(durationMs); +}; +export default useGetIncidentsDuration; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 255d913c1..1af600e09 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -31,6 +31,7 @@ import ServerUnreachable from "../Pages/v1/ServerUnreachable.jsx"; // Incidents import Checks from "../Pages/v1/Incidents"; import Incidents2 from "../Pages/v1/Incidents2"; +import IncidentDetails from "../Pages/v1/Incidents2/Details"; // Status pages import CreateStatus from "../Pages/v1/StatusPage/Create"; @@ -134,6 +135,10 @@ const Routes = () => { path="checks/:monitorId?" element={} /> + } + /> } diff --git a/client/src/locales/en.json b/client/src/locales/en.json index ed07fd8d2..ad68322ff 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1193,6 +1193,16 @@ "incidentsOptionsHeaderFilterResolved": "Resolved", "incidentsTableResolved": "Closed", "incidentsTableActionResolveManually": "Resolve Manually", - "hours": "hours" + "incidentsTableActionDetails": "Details", + "incidentsTableActionGoToMonitor": "Go to monitor", + "resolveIncidentDialogTitle": "Resolve Incident", + "resolveIncidentDialogDescription": "Are you sure you want to resolve this incident manually?", + "resolveIncidentDialogCommentLabel": "Comment (optional)", + "resolveIncidentDialogCommentPlaceholder": "Add a comment about the resolution...", + "resolveIncidentDialogConfirm": "Resolve", + "hours": "hours", + "loading": "Loading incident details...", + "incidentNotFound": "Incident not found", + "incidentDetails": "Incident Details" } } From 1472a0971837d1ab425d495a63db3f96e08853dd Mon Sep 17 00:00:00 2001 From: karenvicent Date: Wed, 14 Jan 2026 22:43:25 -0500 Subject: [PATCH 217/400] add new uptime intervals --- client/src/Pages/v1/Uptime/Create/index.jsx | 5 +++++ client/src/locales/en.json | 8 +++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/client/src/Pages/v1/Uptime/Create/index.jsx b/client/src/Pages/v1/Uptime/Create/index.jsx index 202f64d85..003869b10 100644 --- a/client/src/Pages/v1/Uptime/Create/index.jsx +++ b/client/src/Pages/v1/Uptime/Create/index.jsx @@ -107,11 +107,16 @@ const UptimeCreate = ({ isClone = false }) => { // Constants const MS_PER_MINUTE = 60000; const FREQUENCIES = [ + { _id: 0.25, name: t("time.fifteenSeconds") }, + { _id: 0.5, name: t("time.thirtySeconds") }, { _id: 1, name: t("time.oneMinute") }, { _id: 2, name: t("time.twoMinutes") }, { _id: 3, name: t("time.threeMinutes") }, { _id: 4, name: t("time.fourMinutes") }, { _id: 5, name: t("time.fiveMinutes") }, + { _id: 10, name: t("time.tenMinutes") }, + { _id: 15, name: t("time.fifteenMinutes") }, + { _id: 30, name: t("time.thirtyMinutes") }, ]; const GAMELIST = Object.entries(games).map(([key, value]) => ({ diff --git a/client/src/locales/en.json b/client/src/locales/en.json index ed07fd8d2..a658d6d66 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -985,6 +985,10 @@ }, "pageSpeedLighthouseAPI": "Use the Lighthouse PageSpeed API to monitor your website", "time": { + "fifteenSeconds": "15 seconds", + "thirtySeconds": "30 seconds", + "fifteenMinutes": "15 minutes", + "thirtyMinutes": "30 minutes", "threeMinutes": "3 minutes", "fiveMinutes": "5 minutes", "tenMinutes": "10 minutes", @@ -994,9 +998,7 @@ "oneWeek": "1 week", "fourMinutes": "4 minutes", "oneMinute": "1 minute", - "twoMinutes": "2 minutes", - "fifteenSeconds": "15 seconds", - "thirtySeconds": "30 seconds" + "twoMinutes": "2 minutes" }, "general": { "noOptionsFound": "No {{unit}} found" From 420164d616afe7d4e9c247fcbfe70196a2ad0e4b Mon Sep 17 00:00:00 2001 From: Craig Lawson Date: Thu, 15 Jan 2026 12:52:11 +0000 Subject: [PATCH 218/400] conditional simplification as reqeusted --- server/src/db/modules/statusPageModule.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/db/modules/statusPageModule.js b/server/src/db/modules/statusPageModule.js index 60fd698de..e1b73a691 100755 --- a/server/src/db/modules/statusPageModule.js +++ b/server/src/db/modules/statusPageModule.js @@ -265,7 +265,7 @@ class StatusPageModule { const normalizedMonitors = monitors.map((monitor) => { const normalizedChecks = this.NormalizeData(monitor.checks, 10, 100); - if (showURL !== true) { + if (!showURL) { const { url, port, secret, notifications, ...rest } = monitor; return { ...rest, checks: normalizedChecks }; } From 6b5fd8d17ffb928cdc4c32f3b0c5151aa1863dd0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 18:21:36 +0000 Subject: [PATCH 219/400] update, pause --- .../v1/MonitorDetailsControlHeader/index.jsx | 10 +++---- client/src/Hooks/monitorHooks.js | 2 +- client/src/Pages/Uptime/Create/index.jsx | 4 +-- client/src/Validation/validation.js | 2 +- .../checks/MongoChecksRepistory.ts | 7 +---- .../monitors/IMonitorsRepository.ts | 5 +++- .../monitors/MongoMonitorsRepository.ts | 29 ++++++++++++++++++- server/src/service/business/monitorService.ts | 17 ++++++----- .../service/infrastructure/statusService.js | 4 +-- 9 files changed, 53 insertions(+), 27 deletions(-) diff --git a/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx b/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx index bb4399b85..a86565f49 100644 --- a/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx +++ b/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx @@ -66,7 +66,7 @@ const MonitorDetailsControlHeader = ({ gap={theme.spacing(2)} > @@ -78,7 +78,7 @@ const MonitorDetailsControlHeader = ({ startIcon={} disabled={isTestNotificationsDisabled} onClick={() => { - testAllNotifications({ monitorId: monitor?._id }); + testAllNotifications({ monitorId: monitor?.id }); }} sx={{ whiteSpace: "nowrap", @@ -92,7 +92,7 @@ const MonitorDetailsControlHeader = ({ variant="contained" color="secondary" onClick={(e) => { - navigate(`/incidents/${monitor?._id}`); + navigate(`/incidents/${monitor?.id}`); }} > {t("menu.incidents")} @@ -107,7 +107,7 @@ const MonitorDetailsControlHeader = ({ } onClick={() => { pauseMonitor({ - monitorId: monitor?._id, + monitorId: monitor?.id, triggerUpdate, }); }} @@ -120,7 +120,7 @@ const MonitorDetailsControlHeader = ({ variant="contained" color="secondary" startIcon={} - onClick={() => navigate(`/${path}/configure/${monitor._id}`)} + onClick={() => navigate(`/${path}/configure/${monitor.id}`)} > Configure diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index 92844ad16..d865ffab8 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -387,7 +387,7 @@ export const useUpdateMonitor = () => { }), }; await networkService.updateMonitor({ - monitorId: monitor._id, + monitorId: monitor.id, updatedFields, }); diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index c3421e875..c54af3d07 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -194,7 +194,7 @@ const UptimeCreate = ({ isClone = false }) => { }; } else { form = { - _id: monitor._id, + id: monitor.id, url: monitor.url, name: monitor.name || monitor.url.substring(0, 50), statusWindowSize: monitor.statusWindowSize, @@ -287,7 +287,7 @@ const UptimeCreate = ({ isClone = false }) => { const handleRemove = async (event) => { event.preventDefault(); - const TEMP_MONITOR = { id: monitor._id }; + const TEMP_MONITOR = { id: monitor.id }; await deleteMonitor({ monitor: TEMP_MONITOR, redirect: "/uptime" }); }; diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index 112bbb349..9a84e2e39 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -112,7 +112,7 @@ const loginCredentials = joi.object({ }); const monitorValidation = joi.object({ - _id: joi.string(), + id: joi.string(), userId: joi.string(), teamId: joi.string(), statusWindowSize: joi.number().min(1).max(20).default(5).messages({ diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index a6a049840..b5489309d 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -184,18 +184,13 @@ class MongoChecksRepository implements IChecksRepository { } const mongoIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); const limitPerMonitor = options?.limitPerMonitor ?? 25; - const maxIntervalMs = Number(10 * 60 * 1000); - const bufferMs = Number(maxIntervalMs); - const lookbackMs = limitPerMonitor * maxIntervalMs + bufferMs; - - const cutoffDate = new Date(Date.now() - lookbackMs); const checkGroups = await CheckModel.aggregate([ { $match: { "metadata.monitorId": { $in: mongoIds }, - createdAt: { $gte: cutoffDate }, }, }, + { $sort: { "metadata.monitorId": 1, createdAt: -1 } }, { $group: { _id: "$metadata.monitorId", diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 50fc79b2b..1007842e7 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -24,8 +24,10 @@ export interface IMonitorsRepository { // collection fetch findAll(): Promise; findByTeamId(teamId: string, config: TeamQueryConfig): Promise; + // update - update(monitorId: string, updates: Partial): Promise; + updateById(monitorId: string, updates: Partial): Promise; + togglePauseById(monitorId: string, teamId: string): Promise; // delete deleteByTeamId(teamId: string): Promise<{ monitors: Monitor[]; deletedCount: number }>; @@ -34,4 +36,5 @@ export interface IMonitorsRepository { // other findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise; + findGroupsByTeamId(teamId: string): Promise; } diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index a7b933851..5e579c703 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -96,7 +96,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { return count; }; - update = async (monitorId: string, patch: Partial) => { + updateById = async (monitorId: string, patch: Partial) => { const updatedMonitor = await MonitorModel.findOneAndUpdate( { _id: monitorId }, { @@ -112,6 +112,25 @@ class MongoMonitorsRepository implements IMonitorsRepository { return this.toEntity(updatedMonitor); }; + togglePauseById = async (monitorId: string, teamId: string) => { + const monitor = await MonitorModel.findOneAndUpdate( + { _id: monitorId, teamId }, + [ + { + $set: { + isActive: { $not: "$isActive" }, + status: "$$REMOVE", + }, + }, + ], + { new: true } + ); + if (!monitor) { + throw new AppError({ message: `Monitor with ID ${monitorId} not found for the given team.`, status: 404 }); + } + return this.toEntity(monitor); + }; + deleteByTeamId = async (teamId: string) => { const monitors = await MonitorModel.find({ teamId }); const { deletedCount } = await MonitorModel.deleteMany({ teamId }); @@ -157,6 +176,14 @@ class MongoMonitorsRepository implements IMonitorsRepository { return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0 }; }; + findGroupsByTeamId = async (teamId: string): Promise => { + const groups = await MonitorModel.distinct("group", { + teamId: new mongoose.Types.ObjectId(teamId), + group: { $nin: [null, ""] }, + }); + return groups.sort(); + }; + private mapDocuments = (documents: MonitorDocument[]): Monitor[] => { if (!documents?.length) { return []; diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 0a767779e..cb439f1cc 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -48,10 +48,10 @@ export interface IMonitorService { explain?: boolean; }): Promise<{ summary: any; count: number; monitors: any[] }>; getAllGames(): any; - getGroupsByTeamId(args: { teamId: string }): Promise; + getGroupsByTeamId(args: { teamId: string }): Promise; // update - editMonitor(args: { teamId: string; monitorId: string; body: any }): Promise; + editMonitor(args: { teamId: string; monitorId: string; body: Monitor }): Promise; pauseMonitor(args: { teamId: string; monitorId: string }): Promise; // delete @@ -423,21 +423,22 @@ export class MonitorService implements IMonitorService { return this.games; }; - getGroupsByTeamId = async ({ teamId }: { teamId: string }): Promise => { - const groups = await this.db.monitorModule.getGroupsByTeamId({ teamId }); + getGroupsByTeamId = async ({ teamId }: { teamId: string }): Promise => { + const groups = await this.monitorsRepository.findGroupsByTeamId(teamId); return groups; }; - editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: any }): Promise => { + editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Monitor }) => { await this.verifyTeamAccess({ teamId, monitorId }); - const editedMonitor = await this.db.monitorModule.editMonitor({ monitorId, body }); + const editedMonitor = await this.monitorsRepository.updateById(monitorId, body); await this.jobQueue.updateJob(editedMonitor); + return editedMonitor; }; pauseMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { await this.verifyTeamAccess({ teamId, monitorId }); - const monitor = await this.db.monitorModule.pauseMonitor({ monitorId }); - monitor.isActive === true ? await this.jobQueue.resumeJob(monitor._id, monitor) : await this.jobQueue.pauseJob(monitor); + const monitor = await this.monitorsRepository.togglePauseById(monitorId, teamId); + monitor.isActive === true ? await this.jobQueue.resumeJob(monitor) : await this.jobQueue.pauseJob(monitor); return monitor; }; diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index 105f89c44..cfbf1b339 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -195,7 +195,7 @@ class StatusService { // Return early if not enough data points if (monitor.statusWindow.length < monitor.statusWindowSize) { - const updated = await this.monitorsRepository.update(monitor.id, monitor); + const updated = await this.monitorsRepository.updateById(monitor.id, monitor); return { monitor: updated, statusChanged: false, @@ -285,7 +285,7 @@ class StatusService { } monitor.status = newStatus; - const updated = await this.monitorsRepository.update(monitor.id, monitor); + const updated = await this.monitorsRepository.updateById(monitor.id, monitor); return { monitor: updated, From 75bcf9df58642168926172a2dc55aaa34a99fb8e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 18:24:58 +0000 Subject: [PATCH 220/400] deleteById --- .../src/repositories/monitors/IMonitorsRepository.ts | 1 + .../repositories/monitors/MongoMonitorsRepository.ts | 10 ++++++++++ server/src/service/business/monitorService.ts | 12 ++++++------ 3 files changed, 17 insertions(+), 6 deletions(-) diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 1007842e7..ba28d58e5 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -29,6 +29,7 @@ export interface IMonitorsRepository { updateById(monitorId: string, updates: Partial): Promise; togglePauseById(monitorId: string, teamId: string): Promise; // delete + deleteById(monitorId: string, teamId: string): Promise; deleteByTeamId(teamId: string): Promise<{ monitors: Monitor[]; deletedCount: number }>; // counts diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 5e579c703..a2aa9de66 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -131,6 +131,16 @@ class MongoMonitorsRepository implements IMonitorsRepository { return this.toEntity(monitor); }; + deleteById = async (monitorId: string, teamId: string) => { + const deletedMonitor = await MonitorModel.findOneAndDelete({ _id: monitorId, teamId }); + + if (!deletedMonitor) { + throw new AppError({ message: `Monitor with ID ${monitorId} not found for the given team.`, status: 404 }); + } + + return this.toEntity(deletedMonitor); + }; + deleteByTeamId = async (teamId: string) => { const monitors = await MonitorModel.find({ teamId }); const { deletedCount } = await MonitorModel.deleteMany({ teamId }); diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index cb439f1cc..bce6a1283 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -52,10 +52,10 @@ export interface IMonitorService { // update editMonitor(args: { teamId: string; monitorId: string; body: Monitor }): Promise; - pauseMonitor(args: { teamId: string; monitorId: string }): Promise; + pauseMonitor(args: { teamId: string; monitorId: string }): Promise; // delete - deleteMonitor(args: { teamId: string; monitorId: string }): Promise; + deleteMonitor(args: { teamId: string; monitorId: string }): Promise; deleteAllMonitors(args: { teamId: string }): Promise; // other @@ -435,18 +435,18 @@ export class MonitorService implements IMonitorService { return editedMonitor; }; - pauseMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { + pauseMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { await this.verifyTeamAccess({ teamId, monitorId }); const monitor = await this.monitorsRepository.togglePauseById(monitorId, teamId); monitor.isActive === true ? await this.jobQueue.resumeJob(monitor) : await this.jobQueue.pauseJob(monitor); return monitor; }; - deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { + deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { await this.verifyTeamAccess({ teamId, monitorId }); - const monitor = await this.db.monitorModule.deleteMonitor({ teamId, monitorId }); + const monitor = await this.monitorsRepository.deleteById(monitorId, teamId); await this.jobQueue.deleteJob(monitor); - await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor._id); + await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor.id); return monitor; }; From 74a7f0a701c8519b963c4bc868955ed62d42778c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 18:33:03 +0000 Subject: [PATCH 221/400] remove verify team access --- .../monitors/IMonitorsRepository.ts | 2 +- .../monitors/MongoMonitorsRepository.ts | 4 +- server/src/service/business/monitorService.ts | 75 +------------------ .../service/infrastructure/statusService.js | 4 +- 4 files changed, 8 insertions(+), 77 deletions(-) diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index ba28d58e5..06c57ddb9 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -26,7 +26,7 @@ export interface IMonitorsRepository { findByTeamId(teamId: string, config: TeamQueryConfig): Promise; // update - updateById(monitorId: string, updates: Partial): Promise; + updateById(monitorId: string, teamId: string, updates: Partial): Promise; togglePauseById(monitorId: string, teamId: string): Promise; // delete deleteById(monitorId: string, teamId: string): Promise; diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index a2aa9de66..fe5958c2a 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -96,9 +96,9 @@ class MongoMonitorsRepository implements IMonitorsRepository { return count; }; - updateById = async (monitorId: string, patch: Partial) => { + updateById = async (monitorId: string, teamId: string, patch: Partial) => { const updatedMonitor = await MonitorModel.findOneAndUpdate( - { _id: monitorId }, + { _id: monitorId, teamId }, { $set: { ...patch, diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index bce6a1283..ff79259eb 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -14,7 +14,6 @@ type DateRangeKey = "recent" | "day" | "week" | "month" | "all"; export interface IMonitorService { readonly serviceName: string; - verifyTeamAccess(args: { teamId: string; monitorId: string }): Promise; // create createMonitor(teamId: string, userId: string, body: Monitor): Promise; @@ -60,7 +59,6 @@ export interface IMonitorService { // other sendTestEmail(args: { to: string }): Promise; - exportMonitorsToCSV(args: { teamId: string }): Promise; exportMonitorsToJSON(args: { teamId: string }): Promise; } @@ -146,13 +144,6 @@ export class MonitorService implements IMonitorService { return formatLookup[dateRange]; }; - verifyTeamAccess = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - if (!monitor?.teamId?.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } - }; - createMonitor = async (teamId: string, userId: string, body: Monitor): Promise => { const monitor = await this.monitorsRepository.create(body, teamId, userId); if (!monitor) { @@ -255,8 +246,6 @@ export class MonitorService implements IMonitorService { dateRange: string; normalize?: boolean; }): Promise => { - await this.verifyTeamAccess({ teamId, monitorId }); - const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); @@ -292,7 +281,6 @@ export class MonitorService implements IMonitorService { }; getHardwareDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise => { - await this.verifyTeamAccess({ teamId, monitorId }); const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); @@ -324,7 +312,6 @@ export class MonitorService implements IMonitorService { }; getPageSpeedDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise => { - await this.verifyTeamAccess({ teamId, monitorId }); const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); @@ -348,7 +335,6 @@ export class MonitorService implements IMonitorService { }; }; getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { - await this.verifyTeamAccess({ teamId, monitorId }); const monitor = await this.monitorsRepository.findById(monitorId, teamId); return monitor; }; @@ -429,21 +415,18 @@ export class MonitorService implements IMonitorService { }; editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Monitor }) => { - await this.verifyTeamAccess({ teamId, monitorId }); - const editedMonitor = await this.monitorsRepository.updateById(monitorId, body); + const editedMonitor = await this.monitorsRepository.updateById(monitorId, teamId, body); await this.jobQueue.updateJob(editedMonitor); return editedMonitor; }; pauseMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { - await this.verifyTeamAccess({ teamId, monitorId }); const monitor = await this.monitorsRepository.togglePauseById(monitorId, teamId); monitor.isActive === true ? await this.jobQueue.resumeJob(monitor) : await this.jobQueue.pauseJob(monitor); return monitor; }; deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { - await this.verifyTeamAccess({ teamId, monitorId }); const monitor = await this.monitorsRepository.deleteById(monitorId, teamId); await this.jobQueue.deleteJob(monitor); await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor.id); @@ -486,65 +469,13 @@ export class MonitorService implements IMonitorService { return messageId; }; - exportMonitorsToCSV = async ({ teamId }: { teamId: string }): Promise => { - const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId }); - - if (!monitors || monitors.length === 0) { - throw this.errorService.createNotFoundError("No monitors to export"); - } - - const csvData = monitors?.filteredMonitors?.map((monitor: any) => ({ - name: monitor.name, - description: monitor.description, - type: monitor.type, - url: monitor.url, - interval: monitor.interval, - port: monitor.port, - ignoreTlsErrors: monitor.ignoreTlsErrors, - isActive: monitor.isActive, - })); - - const csv = this.papaparse.unparse(csvData); - return csv; - }; exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise => { - const monitors = await this.db.monitorModule.getMonitorsByTeamId({ teamId }); + const monitors = await this.monitorsRepository.findByTeamId(teamId, {}); if (!monitors || monitors.length === 0) { throw this.errorService.createNotFoundError("No monitors to export"); } - const json = monitors?.filteredMonitors - ?.map((monitor: any) => { - const initialType = monitor.type; - let parsedType; - - if (initialType === "hardware") { - parsedType = "infrastructure"; - } else if (initialType === "http") { - if (monitor.url.startsWith("https://")) { - parsedType = "https"; - } else { - parsedType = "http"; - } - } else if (initialType === "pagespeed") { - parsedType = initialType; - } else { - // Skip unsupported types - return; - } - - return { - name: monitor.name, - url: monitor.url, - type: parsedType, - interval: monitor.interval, - n: monitor.statusWindowSize, - secret: monitor.secret, - }; - }) - .filter(Boolean); - - return json; + return monitors; }; } diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index cfbf1b339..0b2e9f44c 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -195,7 +195,7 @@ class StatusService { // Return early if not enough data points if (monitor.statusWindow.length < monitor.statusWindowSize) { - const updated = await this.monitorsRepository.updateById(monitor.id, monitor); + const updated = await this.monitorsRepository.updateById(monitor.id, monitor.teamId, monitor); return { monitor: updated, statusChanged: false, @@ -285,7 +285,7 @@ class StatusService { } monitor.status = newStatus; - const updated = await this.monitorsRepository.updateById(monitor.id, monitor); + const updated = await this.monitorsRepository.updateById(monitor.id, monitor.teamId, monitor); return { monitor: updated, From 68559741ba6c4933b7c8edc9368c3f45c01721df Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 19:36:41 +0000 Subject: [PATCH 222/400] remove mongo middleware --- server/src/config/services.ts | 5 +- server/src/controllers/monitorController.ts | 15 --- server/src/db/models/Monitor.ts | 29 ----- server/src/db/models/StatusPage.js | 85 ------------- server/src/db/models/StatusPage.ts | 112 ++++++++++++++++++ server/src/db/models/index.ts | 3 + server/src/db/modules/statusPageModule.js | 1 + .../repositories/checks/IChecksRepository.ts | 6 + .../checks/MongoChecksRepistory.ts | 5 + server/src/repositories/index.ts | 3 + .../monitor-stats/IMonitorStatsRepository.ts | 1 + .../MongoMonitorStatsRepository.ts | 8 ++ .../monitors/MongoMonitorsRepository.ts | 2 +- .../status-pages/IStatusPagesRepository.ts | 11 ++ .../MongoStatusPagesRepository.ts | 52 ++++++++ server/src/routes/v1/monitorRoute.ts | 1 - server/src/service/business/monitorService.ts | 19 +-- server/src/types/index.ts | 1 + server/src/types/statusPage.ts | 29 +++++ 19 files changed, 247 insertions(+), 141 deletions(-) delete mode 100755 server/src/db/models/StatusPage.js create mode 100644 server/src/db/models/StatusPage.ts create mode 100644 server/src/repositories/status-pages/IStatusPagesRepository.ts create mode 100644 server/src/repositories/status-pages/MongoStatusPagesRepository.ts create mode 100644 server/src/types/statusPage.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 74c68c86e..990dc63b3 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -70,7 +70,7 @@ import SettingsModule from "../db/modules/settingsModule.js"; import IncidentModule from "../db/modules/incidentModule.js"; // repositories -import { MongoMonitorsRepository, MongoChecksRepository, MongoMonitorStatsRepository } from "@/repositories/index.js"; +import { MongoMonitorsRepository, MongoChecksRepository, MongoMonitorStatsRepository, MongoStatusPagesRepository } from "@/repositories/index.js"; export const initializeServices = async ({ logger, envSettings, settingsService }: { logger: any; envSettings: any; settingsService: any }) => { const serviceRegistry = new ServiceRegistry({ logger }); @@ -124,6 +124,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService const monitorsRepository = new MongoMonitorsRepository(); const checksRepository = new MongoChecksRepository(); const monitorStatsRepository = new MongoMonitorStatsRepository(); + const statusPagesRepository = new MongoStatusPagesRepository(); const networkService = new NetworkService({ axios, @@ -217,7 +218,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService errorService, }); const monitorService = new MonitorService({ - db, jobQueue: superSimpleQueue, stringService, emailService, @@ -228,6 +228,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService monitorsRepository, checksRepository, monitorStatsRepository, + statusPagesRepository, }); const services = { diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 859a8fa4b..d46845026 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -374,21 +374,6 @@ class MonitorController { next(error); } }; - exportMonitorsToCSV = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } - - const csv = await this.monitorService.exportMonitorsToCSV({ teamId }); - res.setHeader("Content-Type", "text/csv"); - res.setHeader("Content-Disposition", "attachment; filename=monitors.csv"); - return res.send(csv); - } catch (error) { - next(error); - } - }; exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => { try { diff --git a/server/src/db/models/Monitor.ts b/server/src/db/models/Monitor.ts index f6f9d4371..17565a2e4 100644 --- a/server/src/db/models/Monitor.ts +++ b/server/src/db/models/Monitor.ts @@ -171,35 +171,6 @@ const MonitorSchema = new Schema( } ); -MonitorSchema.pre("findOneAndDelete", async function (next) { - try { - const doc = await this.model.findOne(this.getFilter()); - - if (!doc) { - throw new Error("Monitor not found"); - } - - await Check.deleteMany({ monitorId: doc._id }); - await StatusPage.updateMany({ monitors: doc?._id }, { $pull: { monitors: doc?._id } }); - await MonitorStats.deleteMany({ monitorId: doc?._id.toString() }); - next(); - } catch (error) { - next(error as Error); - } -}); - -MonitorSchema.pre("deleteMany", async function (next) { - const filter = this.getFilter(); - const monitors = (await this.model.find(filter).select(["_id", "type"]).lean()) as { _id: Types.ObjectId }[]; - - for (const monitor of monitors) { - await Check.deleteMany({ monitorId: monitor._id }); - await StatusPage.updateMany({ monitors: monitor._id }, { $pull: { monitors: monitor._id } }); - await MonitorStats.deleteMany({ monitorId: monitor._id.toString() }); - } - next(); -}); - MonitorSchema.pre("save", function (next) { if (!this.cpuAlertThreshold || this.isModified("alertThreshold")) { this.cpuAlertThreshold = this.alertThreshold; diff --git a/server/src/db/models/StatusPage.js b/server/src/db/models/StatusPage.js deleted file mode 100755 index 621749fb8..000000000 --- a/server/src/db/models/StatusPage.js +++ /dev/null @@ -1,85 +0,0 @@ -import mongoose from "mongoose"; - -const StatusPageSchema = mongoose.Schema( - { - userId: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - immutable: true, - required: true, - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - required: true, - }, - type: { - type: String, - required: true, - default: "uptime", - enum: ["uptime"], - }, - companyName: { - type: String, - required: true, - default: "", - }, - url: { - type: String, - unique: true, - required: true, - default: "", - }, - timezone: { - type: String, - required: false, - }, - color: { - type: String, - required: false, - default: "#4169E1", - }, - monitors: [ - { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - required: true, - }, - ], - subMonitors: [ - { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - required: true, - }, - ], - logo: { - data: Buffer, - contentType: String, - }, - isPublished: { - type: Boolean, - default: false, - }, - showCharts: { - type: Boolean, - default: true, - }, - showUptimePercentage: { - type: Boolean, - default: true, - }, - showAdminLoginLink: { - type: Boolean, - default: false, - }, - customCSS: { - type: String, - default: "", - }, - }, - { timestamps: true } -); - -export default mongoose.model("StatusPage", StatusPageSchema); diff --git a/server/src/db/models/StatusPage.ts b/server/src/db/models/StatusPage.ts new file mode 100644 index 000000000..6b6e7721c --- /dev/null +++ b/server/src/db/models/StatusPage.ts @@ -0,0 +1,112 @@ +import { Schema, model, type Types } from "mongoose"; +import type { StatusPage, StatusPageLogo } from "@/types/statusPage.js"; +import { StatusPageTypes } from "@/types/statusPage.js"; + +type StatusPageDocumentBase = Omit & { + monitors: Types.ObjectId[]; + subMonitors: Types.ObjectId[]; + originalMonitors?: Types.ObjectId[]; + logo?: StatusPageLogo | null; +}; + +interface StatusPageDocument extends StatusPageDocumentBase { + _id: Types.ObjectId; + userId: Types.ObjectId; + teamId: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const logoSchema = new Schema( + { + data: { type: Buffer }, + contentType: { type: String }, + }, + { _id: false } +); + +const StatusPageSchema = new Schema( + { + userId: { + type: Schema.Types.ObjectId, + ref: "User", + immutable: true, + required: true, + }, + teamId: { + type: Schema.Types.ObjectId, + ref: "Team", + immutable: true, + required: true, + }, + type: { + type: String, + required: true, + default: "uptime", + enum: StatusPageTypes, + }, + companyName: { + type: String, + required: true, + default: "", + }, + url: { + type: String, + unique: true, + required: true, + default: "", + }, + timezone: { + type: String, + }, + color: { + type: String, + default: "#4169E1", + }, + monitors: [ + { + type: Schema.Types.ObjectId, + ref: "Monitor", + required: true, + }, + ], + subMonitors: [ + { + type: Schema.Types.ObjectId, + ref: "Monitor", + required: true, + }, + ], + logo: { + type: logoSchema, + default: null, + }, + isPublished: { + type: Boolean, + default: false, + }, + showCharts: { + type: Boolean, + default: true, + }, + showUptimePercentage: { + type: Boolean, + default: true, + }, + showAdminLoginLink: { + type: Boolean, + default: false, + }, + customCSS: { + type: String, + default: "", + }, + }, + { timestamps: true } +); + +const StatusPageModel = model("StatusPage", StatusPageSchema); + +export type { StatusPageDocument }; +export { StatusPageModel }; +export default StatusPageModel; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index 3ac99a390..b92ae18b7 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -6,3 +6,6 @@ export { default as CheckModel } from "@/db/models/Check.js"; export * from "@/db/models/MonitorStats.js"; export { default as MonitorStatsModel } from "@/db/models/MonitorStats.js"; + +export * from "@/db/models/StatusPage.js"; +export { default as StatusPageModel } from "@/db/models/StatusPage.js"; diff --git a/server/src/db/modules/statusPageModule.js b/server/src/db/modules/statusPageModule.js index 999e0bdc9..081d3bea5 100755 --- a/server/src/db/modules/statusPageModule.js +++ b/server/src/db/modules/statusPageModule.js @@ -287,6 +287,7 @@ class StatusPageModule { deleteStatusPagesByMonitorId = async (monitorId) => { try { await this.StatusPage.deleteMany({ monitors: { $in: [monitorId] } }); + await this.StatusPage.deleteMany({ subMonitors: { $in: [monitorId] } }); } catch (error) { error.service = SERVICE_NAME; error.method = "deleteStatusPageByMonitorId"; diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index cedea8d86..19faa0159 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -54,6 +54,9 @@ export interface UptimeChecksResult { } export interface IChecksRepository { + // create + // single fetch + // collection fetch findLatestChecksByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise; findDateRangeChecksByMonitor( monitorId: string, @@ -62,4 +65,7 @@ export interface IChecksRepository { dateString: string, options?: { type?: MonitorType } ): Promise; + // update + //delete + deleteByMonitorId(monitorId: string): Promise; } diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index b5489309d..615754933 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -385,6 +385,11 @@ class MongoChecksRepository implements IChecksRepository { checks: checks.map((doc) => this.toEntity(doc)), }; }; + + deleteByMonitorId = async (monitorId: string): Promise => { + const result = await CheckModel.deleteMany({ "metadata.monitorId": monitorId }); + return result.deletedCount; + }; } export default MongoChecksRepository; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 44d64f5bb..2c27591f1 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -6,3 +6,6 @@ export { default as MongoChecksRepository } from "@/repositories/checks/MongoChe export * from "@/repositories/monitor-stats/IMonitorStatsRepository.js"; export { default as MongoMonitorStatsRepository } from "@/repositories/monitor-stats/MongoMonitorStatsRepository.js"; + +export * from "@/repositories/status-pages/IStatusPagesRepository.js"; +export { default as MongoStatusPagesRepository } from "@/repositories/status-pages/MongoStatusPagesRepository.js"; diff --git a/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts b/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts index a0720619f..08309b4ee 100644 --- a/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts +++ b/server/src/repositories/monitor-stats/IMonitorStatsRepository.ts @@ -5,5 +5,6 @@ export interface IMonitorStatsRepository { findByMonitorId(monitorId: string): Promise; // update // delete + deleteByMonitorId(monitorId: string): Promise; // other } diff --git a/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts b/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts index d1d6a4825..52dbe8aa5 100644 --- a/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts +++ b/server/src/repositories/monitor-stats/MongoMonitorStatsRepository.ts @@ -39,6 +39,14 @@ class MongoMonitorStatsRepository implements IMonitorStatsRepository { } return this.toEntity(monitorStats); }; + + deleteByMonitorId = async (monitorId: string) => { + const deleted = await MonitorStatsModel.findOneAndDelete({ monitorId: new mongoose.Types.ObjectId(monitorId) }); + if (!deleted) { + throw new AppError({ message: "Monitor stats not found", status: 404 }); + } + return this.toEntity(deleted); + }; } export default MongoMonitorStatsRepository; diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index fe5958c2a..52f423154 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -1,6 +1,6 @@ import { MonitorModel } from "@/db/models/index.js"; import type { MonitorDocument } from "@/db/models/index.js"; -import type { Monitor, MonitorType } from "@/types/index.js"; +import type { Monitor } from "@/types/index.js"; import mongoose, { type FilterQuery } from "mongoose"; import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js"; import { AppError } from "@/utils/AppError.js"; diff --git a/server/src/repositories/status-pages/IStatusPagesRepository.ts b/server/src/repositories/status-pages/IStatusPagesRepository.ts new file mode 100644 index 000000000..1b9f9303b --- /dev/null +++ b/server/src/repositories/status-pages/IStatusPagesRepository.ts @@ -0,0 +1,11 @@ +import type { StatusPage } from "@/types/statusPage.js"; + +export interface IStatusPagesRepository { + // create + // single fetch + // collection fetch + // update + // delete + // other + removeMonitorFromStatusPages(monitorId: string): Promise; +} diff --git a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts new file mode 100644 index 000000000..6beb23f0e --- /dev/null +++ b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts @@ -0,0 +1,52 @@ +import { IStatusPagesRepository } from "@/repositories/index.js"; +import { type StatusPageDocument, StatusPageModel } from "@/db/models/StatusPage.js"; +import type { StatusPage } from "@/types/statusPage.js"; +import mongoose from "mongoose"; + +class MongoStatusPagesRepository implements IStatusPagesRepository { + private toEntity(doc: StatusPageDocument): StatusPage { + const toStringId = (value: unknown): string => { + if (value instanceof mongoose.Types.ObjectId) { + return value.toString(); + } + return value?.toString() ?? ""; + }; + + const toDateString = (value: Date | string): string => { + return value instanceof Date ? value.toISOString() : value; + }; + + const mapIdArray = (values?: mongoose.Types.ObjectId[]): string[] => { + return values?.map((value) => toStringId(value)) ?? []; + }; + + return { + id: toStringId(doc._id), + userId: toStringId(doc.userId), + teamId: toStringId(doc.teamId), + type: doc.type, + companyName: doc.companyName, + url: doc.url, + timezone: doc.timezone ?? undefined, + color: doc.color, + monitors: mapIdArray(doc.monitors), + subMonitors: mapIdArray(doc.subMonitors), + originalMonitors: doc.originalMonitors?.map((value) => toStringId(value)), + logo: doc.logo ?? undefined, + isPublished: doc.isPublished, + showCharts: doc.showCharts, + showUptimePercentage: doc.showUptimePercentage, + showAdminLoginLink: doc.showAdminLoginLink, + customCSS: doc.customCSS, + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), + }; + } + + removeMonitorFromStatusPages = async (monitorId: string): Promise => { + const res = await StatusPageModel.updateMany({ monitors: monitorId }, { $pull: { monitors: monitorId } }); + return res.modifiedCount; + }; +} + +export default MongoStatusPagesRepository; diff --git a/server/src/routes/v1/monitorRoute.ts b/server/src/routes/v1/monitorRoute.ts index ab6b0c98e..2e6db630d 100755 --- a/server/src/routes/v1/monitorRoute.ts +++ b/server/src/routes/v1/monitorRoute.ts @@ -43,7 +43,6 @@ class MonitorRoutes { // Other static routes this.router.post("/demo", isAllowed(["admin", "superadmin"]), this.monitorController.addDemoMonitors); - this.router.get("/export", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToCSV); this.router.get("/export/json", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToJSON); this.router.post("/bulk", isAllowed(["admin", "superadmin"]), upload.single("csvFile"), this.monitorController.createBulkMonitors); this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.monitorController.sendTestEmail); diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index ff79259eb..ecdf8ca02 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -2,7 +2,7 @@ import { createMonitorsBodyValidation } from "@/validation/joi.js"; import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js"; import { type Monitor } from "@/types/index.js"; import type { MonitorType } from "@/types/monitor.js"; -import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository } from "@/repositories/index.js"; +import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository, IStatusPagesRepository } from "@/repositories/index.js"; import fs from "fs"; import { fileURLToPath } from "url"; import path from "path"; @@ -76,9 +76,9 @@ export class MonitorService implements IMonitorService { private monitorsRepository: IMonitorsRepository; private checksRepository: IChecksRepository; private monitorStatsRepository: IMonitorStatsRepository; + private statusPagesRepository: IStatusPagesRepository; constructor({ - db, jobQueue, stringService, emailService, @@ -89,8 +89,8 @@ export class MonitorService implements IMonitorService { monitorsRepository, checksRepository, monitorStatsRepository, + statusPagesRepository, }: { - db: any; jobQueue: any; stringService: any; emailService: any; @@ -101,8 +101,8 @@ export class MonitorService implements IMonitorService { monitorsRepository: IMonitorsRepository; checksRepository: IChecksRepository; monitorStatsRepository: IMonitorStatsRepository; + statusPagesRepository: IStatusPagesRepository; }) { - this.db = db; this.jobQueue = jobQueue; this.stringService = stringService; this.emailService = emailService; @@ -113,6 +113,7 @@ export class MonitorService implements IMonitorService { this.monitorsRepository = monitorsRepository; this.checksRepository = checksRepository; this.monitorStatsRepository = monitorStatsRepository; + this.statusPagesRepository = statusPagesRepository; } get serviceName(): string { @@ -428,8 +429,10 @@ export class MonitorService implements IMonitorService { deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { const monitor = await this.monitorsRepository.deleteById(monitorId, teamId); + await this.monitorStatsRepository.deleteByMonitorId(monitor.id); + await this.checksRepository.deleteByMonitorId(monitor.id); + await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id); await this.jobQueue.deleteJob(monitor); - await this.db.statusPageModule.deleteStatusPagesByMonitorId(monitor.id); return monitor; }; @@ -439,9 +442,9 @@ export class MonitorService implements IMonitorService { monitors.map(async (monitor) => { try { await this.jobQueue.deleteJob(monitor); - await this.db.checkModule.deleteChecks(monitor.id); - await this.db.pageSpeedCheckModule.deletePageSpeedChecksByMonitorId(monitor.id); - await this.db.notificationsModule.deleteNotificationsByMonitorId(monitor.id); + await this.checksRepository.deleteByMonitorId(monitor.id); + await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id); + await this.monitorStatsRepository.deleteByMonitorId(monitor.id); } catch (error: any) { this.logger.warn({ message: `Error deleting associated records for monitor ${monitor.id} with name ${monitor.name}`, diff --git a/server/src/types/index.ts b/server/src/types/index.ts index 2367f1c1c..81620da03 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -1,3 +1,4 @@ export * from "@/types/check.js"; export * from "@/types/monitor.js"; export * from "@/types/monitorStats.js"; +export * from "@/types/statusPage.js"; diff --git a/server/src/types/statusPage.ts b/server/src/types/statusPage.ts new file mode 100644 index 000000000..64a5ac50b --- /dev/null +++ b/server/src/types/statusPage.ts @@ -0,0 +1,29 @@ +export const StatusPageTypes = ["uptime"] as const; +export type StatusPageType = (typeof StatusPageTypes)[number]; + +export interface StatusPageLogo { + data: Buffer; + contentType: string; +} + +export interface StatusPage { + id: string; + userId: string; + teamId: string; + type: StatusPageType; + companyName: string; + url: string; + timezone?: string; + color: string; + monitors: string[]; + subMonitors: string[]; + originalMonitors?: string[]; + logo?: StatusPageLogo | null; + isPublished: boolean; + showCharts: boolean; + showUptimePercentage: boolean; + showAdminLoginLink: boolean; + customCSS: string; + createdAt: string; + updatedAt: string; +} From ec953ff26246ea1cab438d7ec48982740740afe3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 19:40:22 +0000 Subject: [PATCH 223/400] format --- server/src/db/models/StatusPage.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/db/models/StatusPage.ts b/server/src/db/models/StatusPage.ts index 6b6e7721c..42d11bc46 100644 --- a/server/src/db/models/StatusPage.ts +++ b/server/src/db/models/StatusPage.ts @@ -2,7 +2,10 @@ import { Schema, model, type Types } from "mongoose"; import type { StatusPage, StatusPageLogo } from "@/types/statusPage.js"; import { StatusPageTypes } from "@/types/statusPage.js"; -type StatusPageDocumentBase = Omit & { +type StatusPageDocumentBase = Omit< + StatusPage, + "id" | "userId" | "teamId" | "monitors" | "subMonitors" | "originalMonitors" | "createdAt" | "updatedAt" +> & { monitors: Types.ObjectId[]; subMonitors: Types.ObjectId[]; originalMonitors?: Types.ObjectId[]; From 9255500bfcdb824e36ad5f5a866b13e6e2aa43e3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 20:55:01 +0000 Subject: [PATCH 224/400] remove db module --- server/src/config/controllers.ts | 6 +- server/src/config/services.ts | 71 +- .../src/controllers/notificationController.ts | 11 +- server/src/db/MongoDB.js | 2 - server/src/db/modules/monitorModule.js | 539 ------------- server/src/db/modules/monitorModuleQueries.js | 740 ------------------ .../checks/MongoChecksRepistory.ts | 190 ++++- .../monitors/IMonitorsRepository.ts | 3 +- .../monitors/MongoMonitorsRepository.ts | 20 +- .../MongoStatusPagesRepository.ts | 61 +- .../{checkService.js => checkService.ts} | 71 +- ...Service.js => maintenanceWindowService.ts} | 40 +- .../{userService.js => userService.ts} | 73 +- .../service/infrastructure/statusService.js | 5 +- 14 files changed, 415 insertions(+), 1417 deletions(-) delete mode 100755 server/src/db/modules/monitorModule.js delete mode 100755 server/src/db/modules/monitorModuleQueries.js rename server/src/service/business/{checkService.js => checkService.ts} (61%) rename server/src/service/business/{maintenanceWindowService.js => maintenanceWindowService.ts} (55%) rename server/src/service/business/{userService.js => userService.ts} (79%) diff --git a/server/src/config/controllers.ts b/server/src/config/controllers.ts index 9fe834348..44b051580 100644 --- a/server/src/config/controllers.ts +++ b/server/src/config/controllers.ts @@ -10,8 +10,8 @@ import StatusPageController from "../controllers/statusPageController.js"; import NotificationController from "../controllers/notificationController.js"; import DiagnosticController from "../controllers/diagnosticController.js"; import IncidentController from "../controllers/incidentController.js"; - -export const initializeControllers = (services: any) => { +import type { InitializedSerivces } from "@/config/services.js"; +export const initializeControllers = (services: InitializedSerivces) => { const controllers: Record = {}; controllers.authController = new AuthController(services.userService); @@ -26,7 +26,7 @@ export const initializeControllers = (services: any) => { controllers.queueController = new QueueController(services.jobQueue); controllers.logController = new LogController(services.logger); controllers.statusPageController = new StatusPageController(services.db); - controllers.notificationController = new NotificationController(services.notificationService, services.db); + controllers.notificationController = new NotificationController(services.notificationService, services.db, services.monitorsRepository); controllers.diagnosticController = new DiagnosticController(services.diagnosticService); controllers.incidentController = new IncidentController(services.incidentService); diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 990dc63b3..8ebda0e16 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -63,16 +63,61 @@ import CheckModule from "../db/modules/checkModule.js"; import StatusPageModule from "../db/modules/statusPageModule.js"; import UserModule from "../db/modules/userModule.js"; import MaintenanceWindowModule from "../db/modules/maintenanceWindowModule.js"; -import MonitorModule from "../db/modules/monitorModule.js"; import NotificationModule from "../db/modules/notificationModule.js"; import RecoveryModule from "../db/modules/recoveryModule.js"; import SettingsModule from "../db/modules/settingsModule.js"; import IncidentModule from "../db/modules/incidentModule.js"; // repositories -import { MongoMonitorsRepository, MongoChecksRepository, MongoMonitorStatsRepository, MongoStatusPagesRepository } from "@/repositories/index.js"; +import { + MongoMonitorsRepository, + MongoChecksRepository, + MongoMonitorStatsRepository, + MongoStatusPagesRepository, + IMonitorsRepository, + IChecksRepository, + IMonitorStatsRepository, + IStatusPagesRepository, +} from "@/repositories/index.js"; -export const initializeServices = async ({ logger, envSettings, settingsService }: { logger: any; envSettings: any; settingsService: any }) => { +export type InitializedSerivces = { + //v1 + settingsService: any; + translationService: any; + stringService: any; + db: any; + networkService: any; + emailService: any; + bufferService: any; + statusService: any; + notificationService: any; + jobQueue: any; + userService: any; + checkService: any; + diagnosticService: any; + inviteService: any; + maintenanceWindowService: any; + monitorService: any; + incidentService: any; + errorService: any; + logger: any; + + // Repositories + monitorsRepository: IMonitorsRepository; + checksRepository: IChecksRepository; + monitorStatsRepository: IMonitorStatsRepository; + statusPagesRepository: IStatusPagesRepository; +}; + +export const initializeServices = async ({ + logger, + envSettings, + settingsService, +}: { + logger: any; + envSettings: any; + settingsService: any; +}): Promise => { const serviceRegistry = new ServiceRegistry({ logger }); (ServiceRegistry as any).instance = serviceRegistry; @@ -87,17 +132,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService }); const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); - const monitorModule = new MonitorModule({ - Monitor, - MonitorStats, - stringService, - fs, - path, - fileURLToPath, - ObjectId, - NormalizeData, - NormalizeDataUptimeDetails, - }); const notificationModule = new NotificationModule({ Notification, Monitor }); const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService }); const settingsModule = new SettingsModule({ AppSettings }); @@ -111,7 +145,6 @@ export const initializeServices = async ({ logger, envSettings, settingsService statusPageModule, userModule, maintenanceWindowModule, - monitorModule, notificationModule, recoveryModule, settingsModule, @@ -202,6 +235,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService settingsService, stringService, errorService, + monitorsRepository, }); const diagnosticService = new DiagnosticService(); const inviteService = new InviteService({ @@ -216,6 +250,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService settingsService, stringService, errorService, + monitorsRepository, }); const monitorService = new MonitorService({ jobQueue: superSimpleQueue, @@ -252,6 +287,12 @@ export const initializeServices = async ({ logger, envSettings, settingsService incidentService, errorService, logger, + + // Repositories + monitorsRepository, + checksRepository, + monitorStatsRepository, + statusPagesRepository, }; Object.values(services).forEach((service) => { diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index 74849c6aa..defae2fd3 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -2,6 +2,7 @@ import { Request, Response, NextFunction } from "express"; import { createNotificationBodyValidation } from "@/validation/joi.js"; import { AppError } from "@/utils/AppError.js"; +import { IMonitorsRepository } from "@/repositories/index.js"; const SERVICE_NAME = "NotificationController"; @@ -9,9 +10,11 @@ class NotificationController { static SERVICE_NAME = SERVICE_NAME; private db: any; private notificationService: any; - constructor(notificationService: any, db: any) { + private monitorsRepository: IMonitorsRepository; + constructor(notificationService: any, db: any, monitorsRepository: IMonitorsRepository) { this.notificationService = notificationService; this.db = db; + this.monitorsRepository = monitorsRepository; } get serviceName() { @@ -167,11 +170,7 @@ class NotificationController { throw new AppError({ message: "Team ID is required", status: 400 }); } - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - - if (!monitor.teamId.equals(teamId)) { - throw new AppError({ message: "Unauthorized", status: 403 }); - } + const monitor = await this.monitorsRepository.findById(monitorId, teamId); const notifications = monitor.notifications; if (notifications.length === 0) { diff --git a/server/src/db/MongoDB.js b/server/src/db/MongoDB.js index 3db17c78f..f7e58f34e 100755 --- a/server/src/db/MongoDB.js +++ b/server/src/db/MongoDB.js @@ -12,7 +12,6 @@ class MongoDB { statusPageModule, userModule, maintenanceWindowModule, - monitorModule, notificationModule, recoveryModule, settingsModule, @@ -25,7 +24,6 @@ class MongoDB { this.recoveryModule = recoveryModule; this.checkModule = checkModule; this.maintenanceWindowModule = maintenanceWindowModule; - this.monitorModule = monitorModule; this.notificationModule = notificationModule; this.settingsModule = settingsModule; this.statusPageModule = statusPageModule; diff --git a/server/src/db/modules/monitorModule.js b/server/src/db/modules/monitorModule.js deleted file mode 100755 index e13da4b59..000000000 --- a/server/src/db/modules/monitorModule.js +++ /dev/null @@ -1,539 +0,0 @@ -import { - buildUptimeDetailsPipeline, - buildMonitorSummaryByTeamIdPipeline, - buildMonitorsByTeamIdPipeline, - buildMonitorsAndSummaryByTeamIdPipeline, - buildMonitorsWithChecksByTeamIdPipeline, - buildFilteredMonitorsByTeamIdPipeline, - getHardwareStats, - getUpChecks, - getAggregateData, -} from "./monitorModuleQueries.js"; - -import { CheckModel } from "@/db/models/index.js"; - -const SERVICE_NAME = "monitorModule"; - -class MonitorModule { - constructor({ Monitor, MonitorStats, stringService, fs, path, fileURLToPath, ObjectId, NormalizeData, NormalizeDataUptimeDetails }) { - this.Monitor = Monitor; - this.MonitorStats = MonitorStats; - this.stringService = stringService; - this.fs = fs; - this.path = path; - this.fileURLToPath = fileURLToPath; - this.ObjectId = ObjectId; - this.NormalizeData = NormalizeData; - this.NormalizeDataUptimeDetails = NormalizeDataUptimeDetails; - - const __filename = fileURLToPath(import.meta.url); - const __dirname = path.dirname(__filename); - - this.demoMonitorsPath = path.resolve(__dirname, "../../../utils/demoMonitors.json"); - } - - // Helper - calculateUptimeDuration = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - const latestCheck = new Date(checks[0].createdAt); - let latestDownCheck = 0; - - for (let i = checks.length - 1; i >= 0; i--) { - if (checks[i].status === false) { - latestDownCheck = new Date(checks[i].createdAt); - break; - } - } - - // If no down check is found, uptime is from the last check to now - if (latestDownCheck === 0) { - return Date.now() - new Date(checks[checks.length - 1].createdAt); - } - - // Otherwise the uptime is from the last check to the last down check - return latestCheck - latestDownCheck; - }; - - // Helper - getLastChecked = (checks) => { - if (!checks || checks.length === 0) { - return 0; // Handle case when no checks are available - } - // Data is sorted newest->oldest, so last check is the most recent - return new Date() - new Date(checks[0].createdAt); - }; - getLatestResponseTime = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - - return checks[0]?.responseTime ?? 0; - }; - - // Helper - getAverageResponseTime = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - - const validChecks = checks.filter((check) => typeof check.responseTime === "number"); - if (validChecks.length === 0) { - return 0; - } - const aggResponseTime = validChecks.reduce((sum, check) => { - return sum + check.responseTime; - }, 0); - return aggResponseTime / validChecks.length; - }; - - // Helper - getUptimePercentage = (checks) => { - if (!checks || checks.length === 0) { - return 0; - } - const upCount = checks.reduce((count, check) => { - return check.status === true ? count + 1 : count; - }, 0); - return (upCount / checks.length) * 100; - }; - - // Helper - getIncidents = (checks) => { - if (!checks || checks.length === 0) { - return 0; // Handle case when no checks are available - } - return checks.reduce((acc, check) => { - return check.status === false ? (acc += 1) : acc; - }, 0); - }; - - // Helper - getDateRange = (dateRange) => { - const startDates = { - recent: new Date(new Date().setHours(new Date().getHours() - 2)), - day: new Date(new Date().setDate(new Date().getDate() - 1)), - week: new Date(new Date().setDate(new Date().getDate() - 7)), - month: new Date(new Date().setMonth(new Date().getMonth() - 1)), - all: new Date(0), - }; - return { - start: startDates[dateRange], - end: new Date(), - }; - }; - - //Helper - getMonitorChecks = async (monitorId, dateRange, sortOrder) => { - const objectId = new this.ObjectId(monitorId); - const indexSpec = { - "metadata.monitorId": 1, - updatedAt: sortOrder, - }; - - const matchBase = { "metadata.monitorId": objectId }; - - const [checksAll, checksForDateRange] = await Promise.all([ - CheckModel.find(matchBase).sort({ createdAt: sortOrder }).hint(indexSpec).lean(), - CheckModel.find({ - ...matchBase, - createdAt: { $gte: dateRange.start, $lte: dateRange.end }, - }) - .hint(indexSpec) - .lean(), - ]); - - return { checksAll, checksForDateRange }; - }; - - // Helper - processChecksForDisplay = (normalizeData, checks, numToDisplay, normalize) => { - let processedChecks = checks; - if (numToDisplay && checks.length > numToDisplay) { - const n = Math.ceil(checks.length / numToDisplay); - processedChecks = checks.filter((_, index) => index % n === 0); - } - return normalize ? normalizeData(processedChecks, 1, 100) : processedChecks; - }; - - // Helper - groupChecksByTime = (checks, dateRange) => { - return checks.reduce((acc, check) => { - // Validate the date - const checkDate = new Date(check.createdAt); - if (Number.isNaN(checkDate.getTime()) || checkDate.getTime() === 0) { - return acc; - } - - const time = dateRange === "day" ? checkDate.setMinutes(0, 0, 0) : checkDate.toISOString().split("T")[0]; - - if (!acc[time]) { - acc[time] = { time, checks: [] }; - } - acc[time].checks.push(check); - return acc; - }, {}); - }; - - // Helper - calculateGroupStats = (group) => { - const totalChecks = group.checks.length; - - const checksWithResponseTime = group.checks.filter((check) => typeof check.responseTime === "number" && !Number.isNaN(check.responseTime)); - - return { - time: group.time, - uptimePercentage: this.getUptimePercentage(group.checks), - totalChecks, - totalIncidents: group.checks.filter((check) => !check.status).length, - avgResponseTime: - checksWithResponseTime.length > 0 - ? checksWithResponseTime.reduce((sum, check) => sum + check.responseTime, 0) / checksWithResponseTime.length - : 0, - }; - }; - - getMonitorById = async (monitorId) => { - try { - const monitor = await this.Monitor.findById(monitorId); - if (monitor === null || monitor === undefined) { - const error = new Error(this.stringService.getDbFindMonitorById(monitorId)); - error.status = 404; - throw error; - } - - return monitor; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorById"; - throw error; - } - }; - - getMonitorsByIds = async (monitorIds) => { - try { - const objectIds = monitorIds.map((id) => new this.ObjectId(id)); - return await this.Monitor.find({ _id: { $in: objectIds } }, { _id: 1, teamId: 1 }).lean(); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorsByIds"; - throw error; - } - }; - getUptimeDetailsById = async ({ monitorId, dateRange }) => { - try { - const dates = this.getDateRange(dateRange); - const formatLookup = { - recent: "%Y-%m-%dT%H:%M:00Z", - day: "%Y-%m-%dT%H:00:00Z", - week: "%Y-%m-%dT00:00:00Z", - month: "%Y-%m-%dT00:00:00Z", - }; - - const dateString = formatLookup[dateRange]; - - const results = await CheckModel.aggregate(buildUptimeDetailsPipeline(monitorId, dates, dateString)); - - const monitorData = results[0]; - - monitorData.groupedUpChecks = this.NormalizeDataUptimeDetails(monitorData.groupedUpChecks, 10, 100); - - monitorData.groupedDownChecks = this.NormalizeDataUptimeDetails(monitorData.groupedDownChecks, 10, 100); - - const normalizedGroupChecks = this.NormalizeDataUptimeDetails(monitorData.groupedChecks, 10, 100); - - monitorData.groupedChecks = normalizedGroupChecks; - const monitorStats = await this.MonitorStats.findOne({ monitorId }); - return { monitorData, monitorStats }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getUptimeDetailsById"; - throw error; - } - }; - - getHardwareDetailsById = async ({ monitorId, dateRange }) => { - try { - const monitor = await this.Monitor.findById(monitorId); - const dates = this.getDateRange(dateRange); - - const formatLookup = { - recent: "%Y-%m-%dT%H:%M:00Z", - day: "%Y-%m-%dT%H:00:00Z", - week: "%Y-%m-%dT00:00:00Z", - month: "%Y-%m-%dT00:00:00Z", - }; - const dateString = formatLookup[dateRange]; - - const [aggregateData, upChecksCount, metrics] = await Promise.all([ - getAggregateData(monitorId, dates), - getUpChecks(monitorId, dates), - getHardwareStats(monitorId, dates, dateString), - ]); - - const stats = { - aggregateData: aggregateData, - upChecks: upChecksCount, - checks: metrics, - }; - - return { - ...monitor.toObject(), - stats, - }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getHardwareDetailsById"; - throw error; - } - }; - - getMonitorsByTeamId = async ({ teamId, type, filter }) => { - try { - const matchStage = { teamId: new this.ObjectId(teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - if (filter !== undefined && filter !== null && filter !== "") { - matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }]; - } - const monitors = await this.Monitor.find(matchStage) - .sort({ name: 1 }) - .select({ - _id: 1, - name: 1, - type: 1, - url: 1, - status: 1, - isActive: 1, - teamId: 1, - }) - .lean(); - return monitors; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorsByTeamId"; - throw error; - } - }; - - findMonitorsSummaryByTeamId = async ({ type, teamId, explain }) => { - try { - const matchStage = { teamId: new this.ObjectId(teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - - const pipeline = buildMonitorsSummaryByTeamIdPipeline({ matchStage }); - if (explain === true) { - return this.Monitor.aggregate(pipeline).explain("executionStats"); - } - const [summary] = await this.Monitor.aggregate(pipeline); - return summary ?? { totalMonitors: 0, upMonitors: 0, downMonitors: 0, pausedMonitors: 0 }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "findMonitorsSummaryByTeamId"; - throw error; - } - }; - - getMonitorsWithChecksByTeamId = async ({ limit, type, page, rowsPerPage, filter, field, order, teamId, explain }) => { - try { - limit = parseInt(limit); - page = parseInt(page); - rowsPerPage = parseInt(rowsPerPage); - if (field === undefined) { - field = "name"; - order = "asc"; - } - // Build match stage - const matchStage = { teamId: new this.ObjectId(teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - - if (explain === true) { - return this.Monitor.aggregate( - buildMonitorsWithChecksByTeamIdPipeline({ - matchStage, - filter, - page, - rowsPerPage, - field, - order, - limit, - type, - }) - ).explain("executionStats"); - } - - const queryResult = await this.Monitor.aggregate( - buildMonitorsWithChecksByTeamIdPipeline({ - matchStage, - filter, - page, - rowsPerPage, - field, - order, - limit, - type, - }) - ); - const monitors = queryResult[0]?.monitors; - const count = queryResult[0]?.count; - const normalizedFilteredMonitors = monitors.map((monitor) => { - if (!monitor.checks) { - return monitor; - } - monitor.checks = this.NormalizeData(monitor.checks, 10, 100); - return monitor; - }); - return { count, monitors: normalizedFilteredMonitors }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMonitorsWithChecksByTeamId"; - throw error; - } - }; - createMonitor = async ({ body, teamId, userId }) => { - try { - const monitor = new this.Monitor({ ...body, teamId, userId }); - const saved = await monitor.save(); - return saved; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createMonitor"; - throw error; - } - }; - - createBulkMonitors = async (req) => { - try { - const monitors = req.map((item) => new this.Monitor({ ...item, notifications: undefined })); - await this.Monitor.bulkSave(monitors); - return monitors; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createBulkMonitors"; - throw error; - } - }; - - deleteMonitor = async ({ teamId, monitorId }) => { - try { - const deletedMonitor = await this.Monitor.findOneAndDelete({ _id: monitorId, teamId }); - - if (!deletedMonitor) { - throw new Error(this.stringService.getDbFindMonitorById(monitorId)); - } - - return deletedMonitor; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteMonitor"; - throw error; - } - }; - - deleteAllMonitors = async (teamId) => { - try { - const monitors = await this.Monitor.find({ teamId }); - const { deletedCount } = await this.Monitor.deleteMany({ teamId }); - - return { monitors, deletedCount }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteAllMonitors"; - throw error; - } - }; - - deleteMonitorsByUserId = async (userId) => { - try { - const result = await this.Monitor.deleteMany({ userId: userId }); - return result; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteMonitorsByUserId"; - throw error; - } - }; - - editMonitor = async ({ monitorId, body }) => { - try { - const editedMonitor = await this.Monitor.findByIdAndUpdate(monitorId, body, { - new: true, - }); - return editedMonitor; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "editMonitor"; - throw error; - } - }; - - addDemoMonitors = async (userId, teamId) => { - try { - const demoMonitors = JSON.parse(this.fs.readFileSync(this.demoMonitorsPath, "utf8")); - - const demoMonitorsToInsert = demoMonitors.map((monitor) => { - return { - userId, - teamId, - name: monitor.name, - description: monitor.name, - type: "http", - url: monitor.url, - interval: 60000, - }; - }); - const insertedMonitors = await this.Monitor.insertMany(demoMonitorsToInsert); - return insertedMonitors; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "addDemoMonitors"; - throw error; - } - }; - - pauseMonitor = async ({ monitorId }) => { - try { - const monitor = await this.Monitor.findOneAndUpdate( - { _id: monitorId }, - [ - { - $set: { - isActive: { $not: "$isActive" }, - status: "$$REMOVE", - }, - }, - ], - { new: true } - ); - - return monitor; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "pauseMonitor"; - throw error; - } - }; - - getGroupsByTeamId = async ({ teamId }) => { - try { - const groups = await this.Monitor.distinct("group", { - teamId: new this.ObjectId(teamId), - group: { $ne: null, $ne: "" }, - }); - - return groups.filter(Boolean).sort(); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getGroupsByTeamId"; - throw error; - } - }; -} - -export default MonitorModule; diff --git a/server/src/db/modules/monitorModuleQueries.js b/server/src/db/modules/monitorModuleQueries.js deleted file mode 100755 index 596f11791..000000000 --- a/server/src/db/modules/monitorModuleQueries.js +++ /dev/null @@ -1,740 +0,0 @@ -import { ObjectId } from "mongodb"; -import { CheckModel } from "@/db/models/index.js"; - -const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => { - return [ - { - $match: { - "metadata.monitorId": new ObjectId(monitorId), - updatedAt: { $gte: dates.start, $lte: dates.end }, - }, - }, - { - $sort: { - updatedAt: 1, - }, - }, - { - $facet: { - // For the response time chart, should return checks for date window - // Grouped by: {day: hour}, {week: day}, {month: day} - uptimePercentage: [ - { - $group: { - _id: null, - upChecks: { - $sum: { $cond: [{ $eq: ["$status", true] }, 1, 0] }, - }, - totalChecks: { $sum: 1 }, - }, - }, - { - $project: { - _id: 0, - percentage: { - $cond: [{ $eq: ["$totalChecks", 0] }, 0, { $divide: ["$upChecks", "$totalChecks"] }], - }, - }, - }, - ], - groupedAvgResponseTime: [ - { - $group: { - _id: null, - avgResponseTime: { - $avg: "$responseTime", - }, - }, - }, - ], - groupedChecks: [ - { - $group: { - _id: { - $dateToString: { - format: dateString, - date: "$createdAt", - }, - }, - avgResponseTime: { - $avg: "$responseTime", - }, - totalChecks: { - $sum: 1, - }, - }, - }, - { - $sort: { - _id: 1, - }, - }, - ], - // Up checks grouped by: {day: hour}, {week: day}, {month: day} - groupedUpChecks: [ - { - $match: { - status: true, - }, - }, - { - $group: { - _id: { - $dateToString: { - format: dateString, - date: "$createdAt", - }, - }, - totalChecks: { - $sum: 1, - }, - avgResponseTime: { - $avg: "$responseTime", - }, - }, - }, - { - $sort: { _id: 1 }, - }, - ], - // Down checks grouped by: {day: hour}, {week: day}, {month: day} for the date window - groupedDownChecks: [ - { - $match: { - status: false, - }, - }, - { - $group: { - _id: { - $dateToString: { - format: dateString, - date: "$createdAt", - }, - }, - totalChecks: { - $sum: 1, - }, - avgResponseTime: { - $avg: "$responseTime", - }, - }, - }, - { - $sort: { _id: 1 }, - }, - ], - }, - }, - { - $lookup: { - from: "monitors", - let: { monitor_id: { $toObjectId: monitorId } }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$_id", "$$monitor_id"] }, - }, - }, - { - $project: { - _id: 1, - teamId: 1, - name: 1, - status: 1, - interval: 1, - type: 1, - url: 1, - isActive: 1, - notifications: 1, - }, - }, - ], - as: "monitor", - }, - }, - { - $project: { - groupedAvgResponseTime: { - $arrayElemAt: ["$groupedAvgResponseTime.avgResponseTime", 0], - }, - - groupedChecks: "$groupedChecks", - groupedUpChecks: "$groupedUpChecks", - groupedDownChecks: "$groupedDownChecks", - groupedUptimePercentage: { $arrayElemAt: ["$uptimePercentage.percentage", 0] }, - monitor: { $arrayElemAt: ["$monitor", 0] }, - }, - }, - ]; -}; - -const buildMonitorStatsPipeline = (monitor) => { - return [ - { - $match: { - monitorId: monitor._id, - }, - }, - { - $project: { - avgResponseTime: 1, - uptimePercentage: 1, - totalChecks: 1, - timeSinceLastCheck: { - $subtract: [Date.now(), "$lastCheckTimestamp"], - }, - lastCheckTimestamp: 1, - uptBurnt: { $toString: "$uptBurnt" }, - }, - }, - ]; -}; - -const buildMonitorSummaryByTeamIdPipeline = ({ matchStage }) => { - return [ - { $match: matchStage }, - { - $group: { - _id: null, - totalMonitors: { $sum: 1 }, - upMonitors: { - $sum: { - $cond: [{ $eq: ["$status", true] }, 1, 0], - }, - }, - downMonitors: { - $sum: { - $cond: [{ $eq: ["$status", false] }, 1, 0], - }, - }, - pausedMonitors: { - $sum: { - $cond: [{ $eq: ["$isActive", false] }, 1, 0], - }, - }, - }, - }, - { - $project: { - _id: 0, - }, - }, - ]; -}; - -const buildMonitorsByTeamIdPipeline = ({ matchStage, field, order }) => { - const sort = { [field]: order === "asc" ? 1 : -1 }; - - return [ - { $match: matchStage }, - { $sort: sort }, - { - $project: { - _id: 1, - name: 1, - type: 1, - port: 1, - }, - }, - ]; -}; - -const buildMonitorsAndSummaryByTeamIdPipeline = ({ matchStage }) => { - return [ - { $match: matchStage }, - { - $facet: { - summary: [ - { - $group: { - _id: null, - totalMonitors: { $sum: 1 }, - upMonitors: { - $sum: { - $cond: [{ $eq: ["$status", true] }, 1, 0], - }, - }, - downMonitors: { - $sum: { - $cond: [{ $eq: ["$status", false] }, 1, 0], - }, - }, - pausedMonitors: { - $sum: { - $cond: [{ $eq: ["$isActive", false] }, 1, 0], - }, - }, - }, - }, - { - $project: { - _id: 0, - }, - }, - ], - monitors: [ - { $sort: { name: 1 } }, - { - $project: { - _id: 1, - name: 1, - type: 1, - }, - }, - ], - }, - }, - { - $project: { - summary: { $arrayElemAt: ["$summary", 0] }, - monitors: 1, - }, - }, - ]; -}; - -const buildMonitorsWithChecksByTeamIdPipeline = ({ matchStage, filter, page, rowsPerPage, field, order, limit, type }) => { - const skip = page && rowsPerPage ? page * rowsPerPage : 0; - const sort = { [field]: order === "asc" ? 1 : -1 }; - const limitStage = rowsPerPage ? [{ $limit: rowsPerPage }] : []; - - // Match name - if (typeof filter !== "undefined" && field === "name") { - matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }]; - } - - // Match isActive - if (typeof filter !== "undefined" && field === "isActive") { - matchStage.isActive = filter === "true" ? true : false; - } - - if (typeof filter !== "undefined" && field === "status") { - matchStage.status = filter === "true" ? true : false; - } - - // Match type - if (typeof filter !== "undefined" && field === "type") { - matchStage.type = filter; - } - - const monitorsPipeline = [ - { $sort: sort }, - { $skip: skip }, - ...limitStage, - { - $project: { - _id: 1, - name: 1, - description: 1, - type: 1, - url: 1, - isActive: 1, - createdAt: 1, - updatedAt: 1, - uptimePercentage: 1, - status: 1, - }, - }, - ]; - - // Add checks - if (limit) { - const checksCollection = "checks"; - monitorsPipeline.push({ - $lookup: { - from: checksCollection, - let: { monitorId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - }, - }, - { $sort: { updatedAt: -1 } }, - { $limit: limit }, - { - $project: { - _id: 1, - status: 1, - responseTime: 1, - statusCode: 1, - createdAt: 1, - updatedAt: 1, - originalResponseTime: 1, - }, - }, - ], - as: "checks", - }, - }); - } - - const pipeline = [ - { $match: matchStage }, - { - $facet: { - count: [{ $count: "monitorsCount" }], - monitors: monitorsPipeline, - }, - }, - { - $project: { - count: { $arrayElemAt: ["$count", 0] }, - monitors: 1, - }, - }, - ]; - return pipeline; -}; - -const buildFilteredMonitorsByTeamIdPipeline = ({ matchStage, filter, page, rowsPerPage, field, order, limit, type }) => { - const skip = page && rowsPerPage ? page * rowsPerPage : 0; - const sort = { [field]: order === "asc" ? 1 : -1 }; - const limitStage = rowsPerPage ? [{ $limit: rowsPerPage }] : []; - - if (typeof filter !== "undefined" && field === "name") { - matchStage.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }]; - } - - if (typeof filter !== "undefined" && field === "status") { - matchStage.status = filter === "true"; - } - - const pipeline = [{ $match: matchStage }, { $sort: sort }, { $skip: skip }, ...limitStage]; - - // Add checks - if (limit) { - const checksCollection = "checks"; - - pipeline.push({ - $lookup: { - from: checksCollection, - let: { monitorId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - }, - }, - { $sort: { createdAt: -1 } }, - { $limit: limit }, - ], - as: "checks", - }, - }); - } - - return pipeline; -}; - -const buildGetMonitorsByTeamIdPipeline = (req) => { - let { limit, type, page, rowsPerPage, filter, field, order } = req.query; - - limit = parseInt(limit); - page = parseInt(page); - rowsPerPage = parseInt(rowsPerPage); - if (field === undefined) { - field = "name"; - order = "asc"; - } - // Build the match stage - const matchStage = { teamId: new ObjectId(req.params.teamId) }; - if (type !== undefined) { - matchStage.type = Array.isArray(type) ? { $in: type } : type; - } - - const skip = page && rowsPerPage ? page * rowsPerPage : 0; - const sort = { [field]: order === "asc" ? 1 : -1 }; - return [ - { $match: matchStage }, - { - $facet: { - summary: [ - { - $group: { - _id: null, - totalMonitors: { $sum: 1 }, - upMonitors: { - $sum: { - $cond: [{ $eq: ["$status", true] }, 1, 0], - }, - }, - downMonitors: { - $sum: { - $cond: [{ $eq: ["$status", false] }, 1, 0], - }, - }, - pausedMonitors: { - $sum: { - $cond: [{ $eq: ["$isActive", false] }, 1, 0], - }, - }, - }, - }, - { - $project: { - _id: 0, - }, - }, - ], - monitors: [ - { $sort: sort }, - { - $project: { - _id: 1, - name: 1, - }, - }, - ], - filteredMonitors: [ - ...(filter !== undefined - ? [ - { - $match: { - $or: [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }], - }, - }, - ] - : []), - { $sort: sort }, - { $skip: skip }, - ...(rowsPerPage ? [{ $limit: rowsPerPage }] : []), - ...(limit - ? [ - { - $lookup: { - from: "checks", - let: { monitorId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - }, - }, - { $sort: { createdAt: -1 } }, - ...(limit ? [{ $limit: limit }] : []), - ], - as: "standardchecks", - }, - }, - ] - : []), - - { - $addFields: { - checks: { - $switch: { - branches: [ - { - case: { $in: ["$type", ["http", "ping", "docker", "port", "game"]] }, - then: "$standardchecks", - }, - ], - default: [], - }, - }, - }, - }, - { - $project: { - standardchecks: 0, - }, - }, - ], - }, - }, - { - $project: { - summary: { $arrayElemAt: ["$summary", 0] }, - filteredMonitors: 1, - monitors: 1, - }, - }, - ]; -}; - -export { - buildUptimeDetailsPipeline, - buildMonitorStatsPipeline, - buildGetMonitorsByTeamIdPipeline, - buildMonitorSummaryByTeamIdPipeline, - buildMonitorsByTeamIdPipeline, - buildMonitorsAndSummaryByTeamIdPipeline, - buildMonitorsWithChecksByTeamIdPipeline, - buildFilteredMonitorsByTeamIdPipeline, -}; - -export const getAggregateData = async (monitorId, dates) => { - const result = await CheckModel.aggregate([ - { - $match: { - "metadata.monitorId": new ObjectId(monitorId), - "metadata.type": "hardware", - createdAt: { $gte: dates.start, $lte: dates.end }, - }, - }, - { $sort: { createdAt: -1 } }, - { - $group: { - _id: null, - latestCheck: { $first: "$$ROOT" }, - totalChecks: { $sum: 1 }, - }, - }, - ]); - return result[0] || { totalChecks: 0, latestCheck: null }; -}; - -export const getUpChecks = async (monitorId, dates) => { - const count = await CheckModel.countDocuments({ - "metadata.monitorId": new ObjectId(monitorId), - "metadata.type": "hardware", - createdAt: { $gte: dates.start, $lte: dates.end }, - status: true, - }); - return { totalChecks: count }; -}; - -export const getHardwareStats = async (monitorId, dates, dateString) => { - return await CheckModel.aggregate([ - { - $match: { - "metadata.monitorId": new ObjectId(monitorId), - "metadata.type": "hardware", - createdAt: { $gte: dates.start, $lte: dates.end }, - }, - }, - { $sort: { createdAt: 1 } }, - { - $group: { - _id: { $dateToString: { format: dateString, date: "$createdAt" } }, - avgCpuUsage: { $avg: "$cpu.usage_percent" }, - avgMemoryUsage: { $avg: "$memory.usage_percent" }, - avgTemperatures: { $push: { $ifNull: ["$cpu.temperature", [0]] } }, - disks: { $push: "$disk" }, - net: { $push: "$net" }, - updatedAts: { $push: "$updatedAt" }, - sampleDoc: { $first: "$$ROOT" }, - }, - }, - { - $project: { - _id: 1, - avgCpuUsage: 1, - avgMemoryUsage: 1, - avgTemperature: { - $map: { - input: { $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$avgTemperatures", 0] }, [0]] } }] }, - as: "idx", - in: { $avg: { $map: { input: "$avgTemperatures", as: "t", in: { $arrayElemAt: ["$$t", "$$idx"] } } } }, - }, - }, - disks: { - $map: { - input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.disk", []] } }] }, - as: "dIdx", - in: { - name: { $concat: ["disk", { $toString: "$$dIdx" }] }, - readSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.read_speed_bytes", "$$dIdx"] } } } }, - writeSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.write_speed_bytes", "$$dIdx"] } } } }, - totalBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.total_bytes", "$$dIdx"] } } } }, - freeBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.free_bytes", "$$dIdx"] } } } }, - usagePercent: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.usage_percent", "$$dIdx"] } } } }, - }, - }, - }, - net: { - $map: { - input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.net", []] } }] }, - as: "nIdx", - in: { - name: { $arrayElemAt: ["$sampleDoc.net.name", "$$nIdx"] }, - bytesSentPerSecond: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaBytesRecv: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaPacketsSent: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaPacketsRecv: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaErrIn: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaErrOut: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaDropIn: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaDropOut: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - }, - }, - }, - }, - }, - { $sort: { _id: 1 } }, - ]); -}; diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 615754933..3816d8cc5 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -15,13 +15,11 @@ import type { } from "@/types/index.js"; import { CheckModel, type CheckDocument } from "@/db/models/index.js"; import mongoose from "mongoose"; -import { - getAggregateData as getHardwareAggregateData, - getHardwareStats, - getUpChecks as getHardwareUpChecks, -} from "@/db/modules/monitorModuleQueries.js"; export type LatestChecksMap = Record; +type DateRange = { start: Date; end: Date }; +type HardwareAggregateData = { latestCheck: CheckDocument | null; totalChecks: number }; +type HardwareUpChecks = { totalChecks: number }; class MongoChecksRepository implements IChecksRepository { private toEntity = (doc: CheckDocument): Check => { @@ -323,9 +321,9 @@ class MongoChecksRepository implements IChecksRepository { const monitorId = monitorObjectId.toHexString(); const dates = { start: startDate, end: endDate }; const [aggregateDataDoc, upChecksDoc, hardwareMetrics] = await Promise.all([ - getHardwareAggregateData(monitorId, dates), - getHardwareUpChecks(monitorId, dates), - getHardwareStats(monitorId, dates, dateString), + this.getHardwareAggregateData(monitorId, dates), + this.getHardwareUpChecks(monitorId, dates), + this.getHardwareStats(monitorId, dates, dateString), ]); const aggregateData = { @@ -387,9 +385,183 @@ class MongoChecksRepository implements IChecksRepository { }; deleteByMonitorId = async (monitorId: string): Promise => { - const result = await CheckModel.deleteMany({ "metadata.monitorId": monitorId }); + const result = await CheckModel.deleteMany({ "metadata.monitorId": new mongoose.Types.ObjectId(monitorId) }); return result.deletedCount; }; + + private getHardwareAggregateData = async (monitorId: string, dates: DateRange): Promise => { + const result = await CheckModel.aggregate([ + { + $match: { + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + "metadata.type": "hardware", + createdAt: { $gte: dates.start, $lte: dates.end }, + }, + }, + { $sort: { createdAt: -1 } }, + { + $group: { + _id: null, + latestCheck: { $first: "$$ROOT" }, + totalChecks: { $sum: 1 }, + }, + }, + ]); + return result[0] || { totalChecks: 0, latestCheck: null }; + }; + + private getHardwareUpChecks = async (monitorId: string, dates: DateRange): Promise => { + const count = await CheckModel.countDocuments({ + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + "metadata.type": "hardware", + createdAt: { $gte: dates.start, $lte: dates.end }, + status: true, + }); + return { totalChecks: count }; + }; + + private getHardwareStats = async (monitorId: string, dates: DateRange, dateString: string) => { + return await CheckModel.aggregate([ + { + $match: { + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + "metadata.type": "hardware", + createdAt: { $gte: dates.start, $lte: dates.end }, + }, + }, + { $sort: { createdAt: 1 } }, + { + $group: { + _id: { $dateToString: { format: dateString, date: "$createdAt" } }, + avgCpuUsage: { $avg: "$cpu.usage_percent" }, + avgMemoryUsage: { $avg: "$memory.usage_percent" }, + avgTemperatures: { $push: { $ifNull: ["$cpu.temperature", [0]] } }, + disks: { $push: "$disk" }, + net: { $push: "$net" }, + updatedAts: { $push: "$updatedAt" }, + sampleDoc: { $first: "$$ROOT" }, + }, + }, + { + $project: { + _id: 1, + avgCpuUsage: 1, + avgMemoryUsage: 1, + avgTemperature: { + $map: { + input: { $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$avgTemperatures", 0] }, [0]] } }] }, + as: "idx", + in: { $avg: { $map: { input: "$avgTemperatures", as: "t", in: { $arrayElemAt: ["$$t", "$$idx"] } } } }, + }, + }, + disks: { + $map: { + input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.disk", []] } }] }, + as: "dIdx", + in: { + name: { $concat: ["disk", { $toString: "$$dIdx" }] }, + readSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.read_speed_bytes", "$$dIdx"] } } } }, + writeSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.write_speed_bytes", "$$dIdx"] } } } }, + totalBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.total_bytes", "$$dIdx"] } } } }, + freeBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.free_bytes", "$$dIdx"] } } } }, + usagePercent: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.usage_percent", "$$dIdx"] } } } }, + }, + }, + }, + net: { + $map: { + input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.net", []] } }] }, + as: "nIdx", + in: { + name: { $arrayElemAt: ["$sampleDoc.net.name", "$$nIdx"] }, + bytesSentPerSecond: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaBytesRecv: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaPacketsSent: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaPacketsRecv: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaErrIn: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaErrOut: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaDropIn: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + deltaDropOut: { + $let: { + vars: { + tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] }, + l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] }, + }, + in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, + }, + }, + }, + }, + }, + }, + }, + { $sort: { _id: 1 } }, + ]); + }; } export default MongoChecksRepository; diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 06c57ddb9..29ebe37be 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -19,11 +19,12 @@ export interface IMonitorsRepository { create(monitor: Monitor, teamId: string, userId: string): Promise; createBulkMonitors(monitors: Monitor[]): Promise; // single fetch - findById(monitorId: string, teamId?: string): Promise; + findById(monitorId: string, teamId: string): Promise; // collection fetch findAll(): Promise; findByTeamId(teamId: string, config: TeamQueryConfig): Promise; + findByIds(monitorIds: string[]): Promise; // update updateById(monitorId: string, teamId: string, updates: Partial): Promise; diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 52f423154..878d6f7cf 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -21,19 +21,11 @@ class MongoMonitorsRepository implements IMonitorsRepository { return this.mapDocuments(inserted); }; - findById = async (monitorId: string, teamId?: string): Promise => { - const match: { _id: string; teamId?: string } = { _id: monitorId }; - if (teamId) { - match.teamId = teamId; - } + findById = async (monitorId: string, teamId: string): Promise => { + const match: { _id: string; teamId: string } = { _id: monitorId, teamId }; const monitor = await MonitorModel.findOne(match); if (!monitor) { - if (monitor === null || monitor === undefined) { - throw new AppError({ - message: `Monitor with ID ${monitorId} not found.`, - status: 404, - }); - } + throw new AppError({ message: `Monitor with ID ${monitorId} not found`, status: 404 }); } return this.toEntity(monitor); }; @@ -81,6 +73,12 @@ class MongoMonitorsRepository implements IMonitorsRepository { return this.mapDocuments(documents); }; + findByIds = async (monitorIds: string[]): Promise => { + const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); + const monitors = await MonitorModel.find({ _id: { $in: objectIds } }); + return this.mapDocuments(monitors); + }; + findMonitorCountByTeamIdAndType = async (teamId: string, config?: TeamQueryConfig): Promise => { const { type } = config ?? {}; diff --git a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts index 6beb23f0e..e1eb4715b 100644 --- a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts +++ b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts @@ -1,47 +1,60 @@ import { IStatusPagesRepository } from "@/repositories/index.js"; import { type StatusPageDocument, StatusPageModel } from "@/db/models/StatusPage.js"; -import type { StatusPage } from "@/types/statusPage.js"; +import type { StatusPage, StatusPageLogo } from "@/types/statusPage.js"; import mongoose from "mongoose"; class MongoStatusPagesRepository implements IStatusPagesRepository { - private toEntity(doc: StatusPageDocument): StatusPage { - const toStringId = (value: unknown): string => { - if (value instanceof mongoose.Types.ObjectId) { - return value.toString(); - } - return value?.toString() ?? ""; - }; + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; - const toDateString = (value: Date | string): string => { - return value instanceof Date ? value.toISOString() : value; - }; + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; - const mapIdArray = (values?: mongoose.Types.ObjectId[]): string[] => { - return values?.map((value) => toStringId(value)) ?? []; - }; + private mapIdArray = (values?: Array): string[] => { + return values?.map((value) => this.toStringId(value)) ?? []; + }; + private mapLogo = (logo?: StatusPageLogo | null): StatusPageLogo | undefined => { + if (!logo) { + return undefined; + } return { - id: toStringId(doc._id), - userId: toStringId(doc.userId), - teamId: toStringId(doc.teamId), + data: logo.data, + contentType: logo.contentType, + }; + }; + + private toEntity = (doc: StatusPageDocument): StatusPage => { + return { + id: this.toStringId(doc._id), + userId: this.toStringId(doc.userId), + teamId: this.toStringId(doc.teamId), type: doc.type, companyName: doc.companyName, url: doc.url, timezone: doc.timezone ?? undefined, color: doc.color, - monitors: mapIdArray(doc.monitors), - subMonitors: mapIdArray(doc.subMonitors), - originalMonitors: doc.originalMonitors?.map((value) => toStringId(value)), - logo: doc.logo ?? undefined, + monitors: this.mapIdArray(doc.monitors), + subMonitors: this.mapIdArray(doc.subMonitors), + originalMonitors: this.mapIdArray(doc.originalMonitors), + logo: this.mapLogo(doc.logo), isPublished: doc.isPublished, showCharts: doc.showCharts, showUptimePercentage: doc.showUptimePercentage, showAdminLoginLink: doc.showAdminLoginLink, customCSS: doc.customCSS, - createdAt: toDateString(doc.createdAt), - updatedAt: toDateString(doc.updatedAt), + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), }; - } + }; removeMonitorFromStatusPages = async (monitorId: string): Promise => { const res = await StatusPageModel.updateMany({ monitors: monitorId }, { $pull: { monitors: monitorId } }); diff --git a/server/src/service/business/checkService.js b/server/src/service/business/checkService.ts similarity index 61% rename from server/src/service/business/checkService.js rename to server/src/service/business/checkService.ts index 9beb43326..61e2fb6ad 100644 --- a/server/src/service/business/checkService.js +++ b/server/src/service/business/checkService.ts @@ -1,20 +1,41 @@ +import { IMonitorsRepository } from "@/repositories/index.js"; + const SERVICE_NAME = "checkService"; class CheckService { static SERVICE_NAME = SERVICE_NAME; - constructor({ db, settingsService, stringService, errorService }) { + private db: any; + private settingsService: any; + private stringService: any; + private errorService: any; + private monitorsRepository: IMonitorsRepository; + + constructor({ + db, + settingsService, + stringService, + errorService, + monitorsRepository, + }: { + db: any; + settingsService: any; + stringService: any; + errorService: any; + monitorsRepository: IMonitorsRepository; + }) { this.db = db; this.settingsService = settingsService; this.stringService = stringService; this.errorService = errorService; + this.monitorsRepository = monitorsRepository; } get serviceName() { return CheckService.SERVICE_NAME; } - getChecksByMonitor = async ({ monitorId, query, teamId }) => { + getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => { if (!monitorId) { throw this.errorService.createBadRequestError("No monitor ID in request"); } @@ -23,15 +44,8 @@ class CheckService { throw this.errorService.createBadRequestError("No team ID in request"); } - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - - if (!monitor) { - throw this.errorService.createNotFoundError("Monitor not found"); - } - - if (!monitor.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } + // For verificaiton, throws an error if monitor doesn't belong to team + await this.monitorsRepository.findById(monitorId, teamId); let { sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query; const result = await this.db.checkModule.getChecksByMonitor({ @@ -47,7 +61,7 @@ class CheckService { return result; }; - getChecksByTeam = async ({ teamId, query }) => { + getChecksByTeam = async ({ teamId, query }: { teamId: string; query: any }) => { let { sortOrder, dateRange, filter, ack, page, rowsPerPage } = query; if (!teamId) { @@ -66,7 +80,7 @@ class CheckService { return checkData; }; - getChecksSummaryByTeamId = async ({ teamId }) => { + getChecksSummaryByTeamId = async ({ teamId }: { teamId: string }) => { if (!teamId) { throw this.errorService.createBadRequestError("No team ID in request"); } @@ -75,7 +89,7 @@ class CheckService { return summary; }; - ackCheck = async ({ checkId, teamId, ack }) => { + ackCheck = async ({ checkId, teamId, ack }: { checkId: string; teamId: string; ack: any }) => { if (!checkId) { throw this.errorService.createBadRequestError("No check ID in request"); } @@ -88,27 +102,21 @@ class CheckService { return updatedCheck; }; - ackAllChecks = async ({ monitorId, path, teamId, ack }) => { + ackAllChecks = async ({ monitorId, path, teamId, ack }: { monitorId: string; path: string; teamId: string; ack: any }) => { if (path === "monitor") { if (!monitorId) { throw this.errorService.createBadRequestError("No monitor ID in request"); } - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - if (!monitor) { - throw this.errorService.createNotFoundError("Monitor not found"); - } - - if (!monitor.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } + // For verificaiton, throws an error if monitor doesn't belong to team + await this.monitorsRepository.findById(monitorId, teamId); } const updatedChecks = await this.db.checkModule.ackAllChecks(monitorId, teamId, ack, path); return updatedChecks; }; - deleteChecks = async ({ monitorId, teamId }) => { + deleteChecks = async ({ monitorId, teamId }: { monitorId: string; teamId: string }) => { if (!monitorId) { throw this.errorService.createBadRequestError("No monitor ID in request"); } @@ -117,20 +125,13 @@ class CheckService { throw this.errorService.createBadRequestError("No team ID in request"); } - const monitor = await this.db.monitorModule.getMonitorById(monitorId); - - if (!monitor) { - throw this.errorService.createNotFoundError("Monitor not found"); - } - - if (!monitor.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } + // For verificaiton, throws an error if monitor doesn't belong to team + await this.monitorsRepository.findById(monitorId, teamId); const deletedCount = await this.db.checkModule.deleteChecks(monitorId); return deletedCount; }; - deleteChecksByTeamId = async ({ teamId }) => { + deleteChecksByTeamId = async ({ teamId }: { teamId: string }) => { if (!teamId) { throw this.errorService.createBadRequestError("No team ID in request"); } @@ -139,7 +140,7 @@ class CheckService { return deletedCount; }; - updateChecksTTL = async ({ teamId, ttl }) => { + updateChecksTTL = async ({ teamId, ttl }: { teamId: string; ttl: string }) => { const SECONDS_PER_DAY = 86400; const newTTL = parseInt(ttl, 10) * SECONDS_PER_DAY; await this.db.checkModule.updateChecksTTL(teamId, newTTL); diff --git a/server/src/service/business/maintenanceWindowService.js b/server/src/service/business/maintenanceWindowService.ts similarity index 55% rename from server/src/service/business/maintenanceWindowService.js rename to server/src/service/business/maintenanceWindowService.ts index 9d52702e9..bb82821cf 100644 --- a/server/src/service/business/maintenanceWindowService.js +++ b/server/src/service/business/maintenanceWindowService.ts @@ -1,30 +1,50 @@ +import { IMonitorsRepository } from "@/repositories/index.js"; + const SERVICE_NAME = "maintenanceWindowService"; class MaintenanceWindowService { static SERVICE_NAME = SERVICE_NAME; + private db: any; + private settingsService: any; + private stringService: any; + private errorService: any; + private monitorsRepository: IMonitorsRepository; - constructor({ db, settingsService, stringService, errorService }) { + constructor({ + db, + settingsService, + stringService, + errorService, + monitorsRepository, + }: { + db: any; + settingsService: any; + stringService: any; + errorService: any; + monitorsRepository: IMonitorsRepository; + }) { this.db = db; this.settingsService = settingsService; this.stringService = stringService; this.errorService = errorService; + this.monitorsRepository = monitorsRepository; } get serviceName() { return MaintenanceWindowService.SERVICE_NAME; } - createMaintenanceWindow = async ({ teamId, body }) => { + createMaintenanceWindow = async ({ teamId, body }: { teamId: string; body: any }) => { const monitorIds = body.monitors; - const monitors = await this.db.monitorModule.getMonitorsByIds(monitorIds); + const monitors = await this.monitorsRepository.findByIds(monitorIds); - const unauthorizedMonitors = monitors.filter((monitor) => !monitor.teamId.equals(teamId)); + const unauthorizedMonitors = monitors.filter((monitor) => monitor.teamId !== teamId); if (unauthorizedMonitors.length > 0) { throw this.errorService.createAuthorizationError(); } - const dbTransactions = monitorIds.map((monitorId) => { + const dbTransactions = monitorIds.map((monitorId: string) => { return this.db.maintenanceWindowModule.createMaintenanceWindow({ teamId, monitorId, @@ -38,26 +58,26 @@ class MaintenanceWindowService { await Promise.all(dbTransactions); }; - getMaintenanceWindowById = async ({ id, teamId }) => { + getMaintenanceWindowById = async ({ id, teamId }: { id: string; teamId: string }) => { const maintenanceWindow = await this.db.maintenanceWindowModule.getMaintenanceWindowById({ id, teamId }); return maintenanceWindow; }; - getMaintenanceWindowsByTeamId = async ({ teamId, query }) => { + getMaintenanceWindowsByTeamId = async ({ teamId, query }: { teamId: string; query: any }) => { const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByTeamId(teamId, query); return maintenanceWindows; }; - getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }) => { + getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }: { monitorId: string; teamId: string }) => { const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({ monitorId, teamId }); return maintenanceWindows; }; - deleteMaintenanceWindow = async ({ id, teamId }) => { + deleteMaintenanceWindow = async ({ id, teamId }: { id: string; teamId: string }) => { await this.db.maintenanceWindowModule.deleteMaintenanceWindowById({ id, teamId }); }; - editMaintenanceWindow = async ({ id, teamId, body }) => { + editMaintenanceWindow = async ({ id, teamId, body }: { id: string; teamId: string; body: any }) => { const editedMaintenanceWindow = await this.db.maintenanceWindowModule.editMaintenanceWindowById({ id, body, teamId }); return editedMaintenanceWindow; }; diff --git a/server/src/service/business/userService.js b/server/src/service/business/userService.ts similarity index 79% rename from server/src/service/business/userService.js rename to server/src/service/business/userService.ts index 7d7f7b014..129ed1e59 100644 --- a/server/src/service/business/userService.js +++ b/server/src/service/business/userService.ts @@ -1,9 +1,44 @@ +import { IMonitorsRepository } from "@/repositories/index.js"; + const SERVICE_NAME = "userService"; class UserService { static SERVICE_NAME = SERVICE_NAME; - constructor({ crypto, db, emailService, settingsService, logger, stringService, jwt, errorService, jobQueue }) { + private db: any; + private emailService: any; + private settingsService: any; + private logger: any; + private stringService: any; + private jwt: any; + private errorService: any; + private jobQueue: any; + private crypto: any; + private monitorsRepository: IMonitorsRepository; + + constructor({ + crypto, + db, + emailService, + settingsService, + logger, + stringService, + jwt, + errorService, + jobQueue, + monitorsRepository, + }: { + crypto: any; + db: any; + emailService: any; + settingsService: any; + logger: any; + stringService: any; + jwt: any; + errorService: any; + jobQueue: any; + monitorsRepository: IMonitorsRepository; + }) { this.db = db; this.emailService = emailService; this.settingsService = settingsService; @@ -13,20 +48,21 @@ class UserService { this.errorService = errorService; this.jobQueue = jobQueue; this.crypto = crypto; + this.monitorsRepository = monitorsRepository; } get serviceName() { return UserService.SERVICE_NAME; } - issueToken = (payload, appSettings) => { + issueToken = (payload: any, appSettings: any) => { const tokenTTL = appSettings?.jwtTTL ?? "2h"; const tokenSecret = appSettings?.jwtSecret; const payloadData = payload; return this.jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); }; - registerUser = async (user, file) => { + registerUser = async (user: any, file: any) => { // Create a new user // If superAdmin exists, a token should be attached to all further register requests const superAdminExists = await this.db.userModule.checkSuperadmin(); @@ -61,7 +97,7 @@ class UserService { const html = await this.emailService.buildEmail("welcomeEmailTemplate", { name: newUser.firstName, }); - this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error) => { + this.emailService.sendEmail(newUser.email, "Welcome to Uptime Monitor", html).catch((error: any) => { this.logger.warn({ message: error.message, service: SERVICE_NAME, @@ -69,7 +105,7 @@ class UserService { stack: error.stack, }); }); - } catch (error) { + } catch (error: any) { this.logger.warn({ message: error.message, service: SERVICE_NAME, @@ -81,7 +117,7 @@ class UserService { return { user: newUser, token }; }; - loginUser = async (email, password) => { + loginUser = async (email: string, password: string) => { // Check if user exists const user = await this.db.userModule.getUserByEmail(email); // Compare password @@ -103,7 +139,7 @@ class UserService { return { user: userWithoutPassword, token }; }; - editUser = async (updates, file, currentUser) => { + editUser = async (updates: any, file: any, currentUser: any) => { // Change Password check if (updates?.password && updates?.newPassword) { // Get user's email @@ -131,7 +167,7 @@ class UserService { return superAdminExists; }; - requestRecovery = async (email) => { + requestRecovery = async (email: string) => { const user = await this.db.userModule.getUserByEmail(email); const recoveryToken = await this.db.recoveryModule.requestRecoveryToken(email); const name = user.firstName; @@ -147,18 +183,18 @@ class UserService { return msgId; }; - validateRecovery = async (recoveryToken) => { + validateRecovery = async (recoveryToken: string) => { await this.db.recoveryModule.validateRecoveryToken(recoveryToken); }; - resetPassword = async (password, recoveryToken) => { + resetPassword = async (password: string, recoveryToken: string) => { const user = await this.db.recoveryModule.resetPassword(password, recoveryToken); const appSettings = await this.settingsService.getSettings(); const token = this.issueToken(user._doc, appSettings); return { user, token }; }; - deleteUser = async (user) => { + deleteUser = async (user: any) => { const email = user?.email; if (!email) { throw this.errorService.createBadRequestError("No email in request"); @@ -181,15 +217,14 @@ class UserService { } // 1. Find all the monitors associated with the team ID if superadmin - const result = await this.db.monitorModule.getMonitorsByTeamId({ - teamId: teamId, - }); + const res = await this.monitorsRepository.findByTeamId(teamId, {}); if (roles.includes("superadmin")) { // 2. Remove all jobs, delete checks and alerts - result?.monitors.length > 0 && + res && + res?.length > 0 && (await Promise.all( - result.monitors.map(async (monitor) => { + res.map(async (monitor) => { await this.jobQueue.deleteJob(monitor); }) )); @@ -203,15 +238,15 @@ class UserService { return users; }; - getUserById = async (roles, userId) => { + getUserById = async (roles: any, userId: any) => { const user = await this.db.userModule.getUserById(roles, userId); return user; }; - editUserById = async (userId, user) => { + editUserById = async (userId: any, user: any) => { await this.db.userModule.editUserById(userId, user); }; - setPasswordByUserId = async (userId, password) => { + setPasswordByUserId = async (userId: any, password: string) => { const updatedUser = await this.db.userModule.updateUser({ userId: userId, user: { password: password }, file: null }); return updatedUser; }; diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index 0b2e9f44c..763b6d1fa 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -167,9 +167,8 @@ class StatusService { const check = this.buildCheck(networkResponse); await this.insertCheck(check); try { - const { monitorId, status, code } = networkResponse; - - const monitor = await this.monitorsRepository.findById(monitorId); + const { monitorId, teamId, status, code } = networkResponse; + const monitor = await this.monitorsRepository.findById(monitorId, teamId); // Update running stats this.updateRunningStats({ monitor, networkResponse }); From 8aac3086b8f8e1821867ef1e81135a42d5ca7d49 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 21:03:35 +0000 Subject: [PATCH 225/400] build error --- server/src/config/services.ts | 1 + server/test/monitorService.test.ts | 132 ++++++++++++++++------------- 2 files changed, 76 insertions(+), 57 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 8ebda0e16..daa576bf8 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -229,6 +229,7 @@ export const initializeServices = async ({ jwt, errorService, jobQueue: superSimpleQueue, + monitorsRepository, }); const checkService = new CheckService({ db, diff --git a/server/test/monitorService.test.ts b/server/test/monitorService.test.ts index 7a3bbe12f..02f2c456c 100644 --- a/server/test/monitorService.test.ts +++ b/server/test/monitorService.test.ts @@ -1,12 +1,19 @@ import { jest } from "@jest/globals"; import { MonitorService } from "../src/service/business/monitorService.ts"; -import type { IMonitorsRepository, IChecksRepository } from "../src/repositories/index.ts"; +import type { + IChecksRepository, + IMonitorStatsRepository, + IMonitorsRepository, + IStatusPagesRepository, +} from "../src/repositories/index.ts"; const createMonitorsRepositoryMock = () => ({ findMonitorCountByTeamIdAndType: jest.fn(), findByTeamId: jest.fn(), findById: jest.fn(), + findMonitorsSummaryByTeamId: jest.fn(), + findGroupsByTeamId: jest.fn(), create: jest.fn(), createBulkMonitors: jest.fn(), deleteByTeamId: jest.fn(), @@ -18,32 +25,34 @@ const createChecksRepositoryMock = () => findDateRangeChecksByMonitor: jest.fn(), }) as unknown as IChecksRepository; +const createMonitorStatsRepositoryMock = () => + ({ + findByMonitorId: jest.fn(), + deleteByMonitorId: jest.fn(), + }) as unknown as IMonitorStatsRepository; + +const createStatusPagesRepositoryMock = () => + ({ + removeMonitorFromStatusPages: jest.fn(), + }) as unknown as IStatusPagesRepository; + const createService = ({ monitorsRepository = createMonitorsRepositoryMock(), checksRepository = createChecksRepositoryMock(), - monitorStatsRepository = { findByMonitorId: jest.fn() }, - monitorModuleOverrides = {}, + monitorStatsRepository = createMonitorStatsRepositoryMock(), + statusPagesRepository = createStatusPagesRepositoryMock(), }: { monitorsRepository?: IMonitorsRepository; checksRepository?: IChecksRepository; - monitorStatsRepository?: { findByMonitorId: jest.Mock }; - monitorModuleOverrides?: Record; + monitorStatsRepository?: IMonitorStatsRepository; + statusPagesRepository?: IStatusPagesRepository; } = {}) => { - const monitorModule = { - getMonitorById: jest.fn().mockResolvedValue({ teamId: { equals: () => true } }), - getMonitorStatsById: jest.fn().mockResolvedValue({ latest: {} }), - getMonitorsByTeamId: jest.fn().mockResolvedValue([]), - getMonitorsAndSummaryByTeamId: jest.fn().mockResolvedValue({ monitors: [], summary: {} }), - ...monitorModuleOverrides, - }; - return new MonitorService({ db: { - monitorModule, - statusPageModule: { deleteStatusPagesByMonitorId: jest.fn() }, checkModule: { deleteChecks: jest.fn() }, + statusPageModule: { deleteStatusPagesByMonitorId: jest.fn() }, pageSpeedCheckModule: { deletePageSpeedChecksByMonitorId: jest.fn() }, - notificationsModule: { deleteNotificationsByMonitorId: jest.fn() }, + notificationModule: { deleteNotificationsByMonitorId: jest.fn() }, }, jobQueue: { addJob: jest.fn(), @@ -66,6 +75,7 @@ const createService = ({ monitorsRepository, checksRepository, monitorStatsRepository, + statusPagesRepository, }); }; @@ -79,17 +89,17 @@ describe("MonitorService", () => { { id: "m2", name: "Monitor 2", interval: 60000 }, ]); - const checksRepository = createChecksRepositoryMock(); - (checksRepository.findLatestChecksByMonitorIds as jest.Mock).mockResolvedValue({ - m1: [ + const checksRepository = createChecksRepositoryMock(); + (checksRepository.findLatestChecksByMonitorIds as jest.Mock).mockResolvedValue({ + m1: [ { responseTime: 10, status: true, message: "OK" }, { responseTime: 20, status: true, message: "OK" }, ], m2: [{ responseTime: 50, status: true, message: "OK" }], }); - const service = createService({ monitorsRepository, checksRepository }); - const result = await service.getMonitorsWithChecksByTeamId({ teamId: "team" }); + const service = createService({ monitorsRepository, checksRepository }); + const result = await service.getMonitorsWithChecksByTeamId({ teamId: "team" }); expect(result).toMatchObject({ count: 2 }); expect(result.monitors).toHaveLength(2); @@ -106,29 +116,41 @@ describe("MonitorService", () => { }); describe("getMonitorsByTeamId", () => { - it("returns monitors array from db module", async () => { - const monitorsPayload = [ + it("returns monitors array from repository", async () => { + const monitorsRepository = createMonitorsRepositoryMock(); + (monitorsRepository.findByTeamId as jest.Mock).mockResolvedValue([ { id: "m1", name: "Monitor 1" }, { id: "m2", name: "Monitor 2" }, - ]; - const monitorModuleOverrides = { - getMonitorsByTeamId: jest.fn().mockResolvedValue(monitorsPayload), - }; - const service = createService({ monitorModuleOverrides }); + ]); + const service = createService({ monitorsRepository }); const result = await service.getMonitorsByTeamId({ teamId: "team" } as any); expect(result).toHaveLength(2); expect(result[0]).toHaveProperty("id", "m1"); }); }); - describe("getMonitorsAndSummaryByTeamId", () => { - it("returns monitors with summary block", async () => { - const monitorModuleOverrides = { - getMonitorsAndSummaryByTeamId: jest.fn().mockResolvedValue({ monitors: [{ id: "m1" }], summary: { total: 1, uptime: 0.99 } }), - }; - const service = createService({ monitorModuleOverrides }); - const result = await service.getMonitorsAndSummaryByTeamId({ teamId: "team" }); - expect(result).toEqual({ monitors: [{ id: "m1" }], summary: { total: 1, uptime: 0.99 } }); + describe("getMonitorsWithChecksByTeamId summary", () => { + it("includes summary and monitors with checks", async () => { + const monitorsRepository = createMonitorsRepositoryMock(); + (monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(1); + (monitorsRepository.findByTeamId as jest.Mock).mockResolvedValue([{ id: "m1", type: "http" }]); + (monitorsRepository.findMonitorsSummaryByTeamId as jest.Mock).mockResolvedValue({ + totalMonitors: 1, + upMonitors: 1, + downMonitors: 0, + pausedMonitors: 0, + }); + + const checksRepository = createChecksRepositoryMock(); + (checksRepository.findLatestChecksByMonitorIds as jest.Mock).mockResolvedValue({ m1: [] }); + + const service = createService({ monitorsRepository, checksRepository }); + const result = await service.getMonitorsWithChecksByTeamId({ teamId: "team" }); + expect(result).toEqual({ + summary: { totalMonitors: 1, upMonitors: 1, downMonitors: 0, pausedMonitors: 0 }, + count: 1, + monitors: [{ id: "m1", type: "http", checks: [] }], + }); }); }); @@ -163,7 +185,7 @@ describe("MonitorService", () => { (monitorsRepository.findById as jest.Mock).mockResolvedValue(monitor); const checksRepository = createChecksRepositoryMock(); (checksRepository.findDateRangeChecksByMonitor as jest.Mock).mockResolvedValue({ - monitorType: "uptime", + monitorType: "http", groupedChecks: [{ _id: "2024-01-01", avgResponseTime: 100, totalChecks: 2 }], groupedUpChecks: [{ _id: "2024-01-01", totalChecks: 2, avgResponseTime: 90 }], groupedDownChecks: [{ _id: "2024-01-01", totalChecks: 0, avgResponseTime: 0 }], @@ -171,27 +193,23 @@ describe("MonitorService", () => { avgResponseTime: 95, }); - const monitorStatsRepository = { - findByMonitorId: jest.fn().mockResolvedValue({ - id: "stats-1", - monitorId: monitor.id, - avgResponseTime: 90, - totalChecks: 10, - totalUpChecks: 9, - totalDownChecks: 1, - uptimePercentage: 0.9, - lastCheckTimestamp: 123456789, - lastResponseTime: 80, - timeOfLastFailure: 123456700, - createdAt: new Date().toISOString(), - updatedAt: new Date().toISOString(), - }), - } as any; - const monitorModuleOverrides = { - getMonitorById: jest.fn().mockResolvedValue({ teamId: { equals: (value: string) => value === TEAM_ID } }), - }; + const monitorStatsRepository = createMonitorStatsRepositoryMock(); + (monitorStatsRepository.findByMonitorId as jest.Mock).mockResolvedValue({ + id: "stats-1", + monitorId: monitor.id, + avgResponseTime: 90, + totalChecks: 10, + totalUpChecks: 9, + totalDownChecks: 1, + uptimePercentage: 0.9, + lastCheckTimestamp: 123456789, + lastResponseTime: 80, + timeOfLastFailure: 123456700, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }); - const service = createService({ monitorsRepository, checksRepository, monitorModuleOverrides, monitorStatsRepository }); + const service = createService({ monitorsRepository, checksRepository, monitorStatsRepository }); const result = await service.getUptimeDetailsById({ teamId: TEAM_ID, monitorId: "monitor-1", dateRange: "recent" }); expect(result).toHaveProperty("monitorData"); From 56bfbe2c7708620d1144b21666569aae43ab1611 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 21:05:00 +0000 Subject: [PATCH 226/400] format --- server/test/monitorService.test.ts | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/server/test/monitorService.test.ts b/server/test/monitorService.test.ts index 02f2c456c..32708a42e 100644 --- a/server/test/monitorService.test.ts +++ b/server/test/monitorService.test.ts @@ -1,11 +1,6 @@ import { jest } from "@jest/globals"; import { MonitorService } from "../src/service/business/monitorService.ts"; -import type { - IChecksRepository, - IMonitorStatsRepository, - IMonitorsRepository, - IStatusPagesRepository, -} from "../src/repositories/index.ts"; +import type { IChecksRepository, IMonitorStatsRepository, IMonitorsRepository, IStatusPagesRepository } from "../src/repositories/index.ts"; const createMonitorsRepositoryMock = () => ({ @@ -89,17 +84,17 @@ describe("MonitorService", () => { { id: "m2", name: "Monitor 2", interval: 60000 }, ]); - const checksRepository = createChecksRepositoryMock(); - (checksRepository.findLatestChecksByMonitorIds as jest.Mock).mockResolvedValue({ - m1: [ + const checksRepository = createChecksRepositoryMock(); + (checksRepository.findLatestChecksByMonitorIds as jest.Mock).mockResolvedValue({ + m1: [ { responseTime: 10, status: true, message: "OK" }, { responseTime: 20, status: true, message: "OK" }, ], m2: [{ responseTime: 50, status: true, message: "OK" }], }); - const service = createService({ monitorsRepository, checksRepository }); - const result = await service.getMonitorsWithChecksByTeamId({ teamId: "team" }); + const service = createService({ monitorsRepository, checksRepository }); + const result = await service.getMonitorsWithChecksByTeamId({ teamId: "team" }); expect(result).toMatchObject({ count: 2 }); expect(result.monitors).toHaveLength(2); From 5a7b51bfec063f7c0ed94bcf71cffde4b53d5192 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 21:49:48 +0000 Subject: [PATCH 227/400] convert super simple queue to ts --- client/src/Pages/Incidents2/index.jsx | 4 +- server/src/config/services.ts | 2 - .../SuperSimpleQueue/SuperSimpleQueue.js | 177 ------------ .../SuperSimpleQueue/SuperSimpleQueue.ts | 254 ++++++++++++++++++ 4 files changed, 256 insertions(+), 181 deletions(-) delete mode 100644 server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js create mode 100644 server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts diff --git a/client/src/Pages/Incidents2/index.jsx b/client/src/Pages/Incidents2/index.jsx index 966388b51..2f31864bf 100644 --- a/client/src/Pages/Incidents2/index.jsx +++ b/client/src/Pages/Incidents2/index.jsx @@ -87,8 +87,8 @@ const Incidents2 = () => { useEffect(() => { const lookup = monitors?.reduce((acc, monitor) => { - acc[monitor._id] = { - _id: monitor._id, + acc[monitor.id] = { + _id: monitor.id, name: monitor.name, type: monitor.type, }; diff --git a/server/src/config/services.ts b/server/src/config/services.ts index daa576bf8..90d6bb518 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -211,8 +211,6 @@ export const initializeServices = async ({ }); const superSimpleQueue = await SuperSimpleQueue.create({ - envSettings, - db, logger, helper: superSimpleQueueHelper, monitorsRepository, diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js deleted file mode 100644 index a6637d4cc..000000000 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js +++ /dev/null @@ -1,177 +0,0 @@ -import Scheduler from "super-simple-scheduler"; -const SERVICE_NAME = "JobQueue"; - -class SuperSimpleQueue { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ envSettings, db, logger, helper, monitorsRepository }) { - this.envSettings = envSettings; - this.db = db; - this.logger = logger; - this.helper = helper; - this.monitorsRepository = monitorsRepository; - } - - get serviceName() { - return SuperSimpleQueue.SERVICE_NAME; - } - - static async create({ envSettings, db, logger, helper, monitorsRepository }) { - const instance = new SuperSimpleQueue({ envSettings, db, logger, helper, monitorsRepository }); - await instance.init(); - return instance; - } - - init = async () => { - try { - this.scheduler = new Scheduler({ - // storeType: "mongo", - // storeType: "redis", - logLevel: "debug", - debug: true, - // dbUri: this.envSettings.dbConnectionString, - }); - this.scheduler.start(); - - this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob()); - const monitors = await this.monitorsRepository.findAll(); - for (const monitor of monitors) { - const randomOffset = Math.floor(Math.random() * 100); - setTimeout(() => { - this.addJob(monitor.id, monitor); - }, randomOffset); - } - - return true; - } catch (error) { - this.logger.error({ - message: "Failed to initialize SuperSimpleQueue", - service: SERVICE_NAME, - method: "init", - details: error, - }); - return false; - } - }; - - addJob = async (monitorId, monitor) => { - this.scheduler.addJob({ - id: monitorId, - template: "monitor-job", - repeat: monitor.interval, - active: monitor.isActive, - data: monitor, - }); - }; - - deleteJob = async (monitor) => { - this.scheduler.removeJob(monitor.id); - }; - - pauseJob = async (monitor) => { - const result = this.scheduler.pauseJob(monitor.id); - if (result === false) { - throw new Error("Failed to resume monitor"); - } - this.logger.debug({ - message: `Paused monitor ${monitor.id}`, - service: SERVICE_NAME, - method: "pauseJob", - }); - }; - - resumeJob = async (monitor) => { - const result = this.scheduler.resumeJob(monitor.id); - if (result === false) { - throw new Error("Failed to resume monitor"); - } - this.logger.debug({ - message: `Resumed monitor ${monitor.id}`, - service: SERVICE_NAME, - method: "resumeJob", - }); - }; - - updateJob = async (monitor) => { - this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor }); - }; - - shutdown = async () => { - this.scheduler.stop(); - }; - - getMetrics = async () => { - const jobs = await this.scheduler.getJobs(); - const metrics = jobs.reduce( - (acc, job) => { - acc.totalRuns += job.runCount || 0; - acc.totalFailures += job.failCount || 0; - acc.jobs++; - if (job.failCount > 0 && job.lastFailedAt >= job.lsatRunAt) { - acc.failingJobs++; - } - - if (job.lockedAt) { - acc.activeJobs++; - } - - if (job.failCount > 0) { - acc.jobsWithFailures.push({ - monitorId: job.id, - monitorUrl: job?.data?.url || null, - monitorType: job?.data?.type || null, - failedAt: job.lastFailedAt, - failCount: job.failCount, - failReason: job.lastFailReason, - }); - } - return acc; - }, - { - jobs: 0, - activeJobs: 0, - failingJobs: 0, - jobsWithFailures: [], - totalRuns: 0, - totalFailures: 0, - } - ); - return metrics; - }; - - getJobs = async () => { - const jobs = await this.scheduler.getJobs(); - return jobs.map((job) => { - return { - monitorId: job.id, - monitorUrl: job?.data?.url || null, - monitorType: job?.data?.type || null, - monitorInterval: job?.data?.interval || null, - active: job.active, - lockedAt: job.lockedAt, - runCount: job.runCount || 0, - failCount: job.failCount || 0, - failReason: job.lastFailReason, - lastRunAt: job.lastRunAt, - lastFinishedAt: job.lastFinishedAt, - lastRunTook: job.lockedAt ? null : job.lastFinishedAt - job.lastRunAt, - lastFailedAt: job.lastFailedAt, - }; - }); - }; - - flushQueues = async () => { - const stopRes = this.scheduler.stop(); - const flushRes = this.scheduler.flushJobs(); - const initRes = await this.init(); - return { - success: stopRes && flushRes && initRes, - }; - }; - - obliterate = async () => { - console.log("obliterate not implemented"); - }; -} - -export default SuperSimpleQueue; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts new file mode 100644 index 000000000..daf7e130f --- /dev/null +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts @@ -0,0 +1,254 @@ +import { IMonitorsRepository } from "@/repositories/index.js"; +import Scheduler from "super-simple-scheduler"; +const SERVICE_NAME = "JobQueue"; + +type QueueJobFailure = { + monitorId: string | number; + monitorUrl: string | null; + monitorType: string | null; + failedAt: number | null; + failCount: number; + failReason: string | null; +}; + +type QueueMetrics = { + jobs: number; + activeJobs: number; + failingJobs: number; + jobsWithFailures: QueueJobFailure[]; + totalRuns: number; + totalFailures: number; +}; + +type QueueJobSummary = { + monitorId: string | number; + monitorUrl: string | null; + monitorType: string | null; + monitorInterval: number | null; + active: boolean; + lockedAt: number | null; + runCount: number; + failCount: number; + failReason: string | null; + lastRunAt: number | null; + lastFinishedAt: number | null; + lastRunTook: number | null; + lastFailedAt: number | null; +}; + +export interface ISuperSimpleQueue { + readonly serviceName: string; + init(): Promise; + addJob(monitorId: string, monitor: any): Promise; + deleteJob(monitor: any): Promise; + pauseJob(monitor: any): Promise; + resumeJob(monitor: any): Promise; + updateJob(monitor: any): Promise; + shutdown(): Promise; + getMetrics(): Promise; + getJobs(): Promise; + flushQueues(): Promise<{ success: boolean }>; + obliterate(): Promise; +} + +class SuperSimpleQueue implements ISuperSimpleQueue { + static SERVICE_NAME = SERVICE_NAME; + + private logger: any; + private helper: any; + private monitorsRepository: IMonitorsRepository; + private readonly scheduler: Scheduler; + + constructor({ + logger, + helper, + monitorsRepository, + scheduler, + }: { + logger: any; + helper: any; + monitorsRepository: IMonitorsRepository; + scheduler: Scheduler; + }) { + this.logger = logger; + this.helper = helper; + this.monitorsRepository = monitorsRepository; + this.scheduler = scheduler; + } + + get serviceName() { + return SuperSimpleQueue.SERVICE_NAME; + } + + static async create({ logger, helper, monitorsRepository }: { logger: any; helper: any; monitorsRepository: IMonitorsRepository }) { + const scheduler = new Scheduler({ + // storeType: "mongo", + // storeType: "redis", + logLevel: "debug", + // dbUri: envSettings.dbConnectionString, + }); + const instance = new SuperSimpleQueue({ logger, helper, monitorsRepository, scheduler }); + await instance.init(); + return instance; + } + + init = async () => { + try { + this.scheduler.start(); + + this.scheduler.addTemplate("monitor-job", this.helper.getMonitorJob()); + const monitors = await this.monitorsRepository.findAll(); + if (!monitors) { + return true; + } + for (const monitor of monitors) { + const randomOffset = Math.floor(Math.random() * 100); + setTimeout(() => { + this.addJob(monitor.id, monitor); + }, randomOffset); + } + + return true; + } catch (error) { + this.logger.error({ + message: "Failed to initialize SuperSimpleQueue", + service: SERVICE_NAME, + method: "init", + details: error, + }); + return false; + } + }; + + addJob = async (monitorId: string, monitor: any) => { + this.scheduler.addJob({ + id: monitorId, + template: "monitor-job", + repeat: monitor.interval, + active: monitor.isActive, + data: monitor, + }); + }; + + deleteJob = async (monitor: any) => { + this.scheduler.removeJob(monitor.id); + }; + + pauseJob = async (monitor: any) => { + const result = await this.scheduler.pauseJob(monitor.id); + if (result === false) { + throw new Error("Failed to resume monitor"); + } + this.logger.debug({ + message: `Paused monitor ${monitor.id}`, + service: SERVICE_NAME, + method: "pauseJob", + }); + }; + + resumeJob = async (monitor: any) => { + const result = await this.scheduler.resumeJob(monitor.id); + if (result === false) { + throw new Error("Failed to resume monitor"); + } + this.logger.debug({ + message: `Resumed monitor ${monitor.id}`, + service: SERVICE_NAME, + method: "resumeJob", + }); + }; + + updateJob = async (monitor: any) => { + this.scheduler.updateJob(monitor.id, { repeat: monitor.interval, data: monitor }); + }; + + shutdown = async () => { + this.scheduler.stop(); + }; + + getMetrics = async () => { + const jobs = await this.scheduler.getJobs(); + const metrics = jobs.reduce( + (acc, job) => { + const runCount = job.runCount ?? 0; + const failCount = job.failCount ?? 0; + const lastFailedAt = job.lastFailedAt ?? 0; + const lastRunAt = job.lastRunAt ?? 0; + acc.totalRuns += runCount; + acc.totalFailures += failCount; + acc.jobs++; + if (failCount > 0 && lastFailedAt >= lastRunAt) { + acc.failingJobs++; + } + + if (job.lockedAt) { + acc.activeJobs++; + } + + if (failCount > 0) { + acc.jobsWithFailures.push({ + monitorId: job.id, + monitorUrl: job?.data?.url || null, + monitorType: job?.data?.type || null, + failedAt: job.lastFailedAt ?? null, + failCount, + failReason: job.lastFailReason ?? null, + }); + } + return acc; + }, + { + jobs: 0, + activeJobs: 0, + failingJobs: 0, + jobsWithFailures: [] as Array<{ + monitorId: string | number; + monitorUrl: any; + monitorType: any; + failedAt: number | null; + failCount: number; + failReason: string | null; + }>, + totalRuns: 0, + totalFailures: 0, + } + ); + return metrics; + }; + + getJobs = async () => { + const jobs = await this.scheduler.getJobs(); + return jobs.map((job) => { + return { + monitorId: job.id, + monitorUrl: job?.data?.url || null, + monitorType: job?.data?.type || null, + monitorInterval: job?.data?.interval || null, + active: job.active, + lockedAt: job.lockedAt ?? null, + runCount: job.runCount ?? 0, + failCount: job.failCount ?? 0, + failReason: job.lastFailReason ?? null, + lastRunAt: job.lastRunAt ?? null, + lastFinishedAt: job.lastFinishedAt ?? null, + lastRunTook: job.lockedAt ? null : (job.lastFinishedAt ?? 0) - (job.lastRunAt ?? 0), + lastFailedAt: job.lastFailedAt ?? null, + }; + }); + }; + + flushQueues = async () => { + const stopRes = await this.scheduler.stop(); + const flushRes = await this.scheduler.flushJobs(); + const initRes = await this.init(); + return { + success: Boolean(stopRes && flushRes && initRes), + }; + }; + + obliterate = async () => { + console.log("obliterate not implemented"); + }; +} + +export default SuperSimpleQueue; From cf67c82f5ecf1714a33eb1390c19dbdc452dba7b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 22:06:46 +0000 Subject: [PATCH 228/400] convert job queue and job queue helper --- server/jest.config.ts | 11 ++- ...eueHelper.js => SuperSimpleQueueHelper.ts} | 60 ++++++----- server/test/superSimpleQueueHelper.test.ts | 99 +++++++++++++++++++ server/tsconfig.jest.json | 3 +- server/tsconfig.json | 2 +- 5 files changed, 149 insertions(+), 26 deletions(-) rename server/src/service/infrastructure/SuperSimpleQueue/{SuperSimpleQueueHelper.js => SuperSimpleQueueHelper.ts} (69%) create mode 100644 server/test/superSimpleQueueHelper.test.ts diff --git a/server/jest.config.ts b/server/jest.config.ts index 2bcfcbf21..4fb2c9831 100644 --- a/server/jest.config.ts +++ b/server/jest.config.ts @@ -1,5 +1,14 @@ +import fs from "fs"; +import path from "path"; +import { fileURLToPath } from "url"; +import { pathsToModuleNameMapper } from "ts-jest"; import type { Config } from "jest"; +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const tsconfigPath = path.resolve(__dirname, "./tsconfig.json"); +const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, "utf-8")); + const config: Config = { rootDir: ".", testEnvironment: "node", @@ -8,7 +17,7 @@ const config: Config = { "^.+\\.(t|j)sx?$": ["ts-jest", { useESM: true, tsconfig: "./tsconfig.jest.json" }], }, moduleNameMapper: { - "^@/(.*)$": "/src/$1", + ...pathsToModuleNameMapper(tsconfig.compilerOptions.paths || {}, { prefix: "/" }), }, testMatch: ["/test/**/*.test.ts"], setupFilesAfterEnv: [], diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts similarity index 69% rename from server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js rename to server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 3cd6b37a8..678986c62 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -1,18 +1,30 @@ const SERVICE_NAME = "JobQueueHelper"; +import type { Monitor } from "@/types/monitor.js"; +import { AppError } from "@/utils/AppError.js"; +import { error } from "winston"; class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; - /** - * @param {{ - * db: import("../database.js").Database, - * logger: import("../logger.js").Logger, - * networkService: import("../networkService.js").NetworkService, - * statusService: import("../statusService.js").StatusService, - * notificationService: import("../notificationService.js").NotificationService - * }} - */ - constructor({ db, logger, networkService, statusService, notificationService }) { + private db: any; + private logger: any; + private networkService: any; + private statusService: any; + private notificationService: any; + + constructor({ + db, + logger, + networkService, + statusService, + notificationService, + }: { + db: any; + logger: any; + networkService: any; + statusService: any; + notificationService: any; + }) { this.db = db; this.logger = logger; this.networkService = networkService; @@ -25,39 +37,41 @@ class SuperSimpleQueueHelper { } getMonitorJob = () => { - return async (monitor) => { + return async (monitor: Monitor) => { try { const monitorId = monitor.id; const teamId = monitor.teamId; if (!monitorId) { - throw new Error("No monitor id"); + throw new AppError({ message: "No monitor id", service: SERVICE_NAME, method: "getMonitorJob" }); } + // Step 1. Check for maintenacne window, if found, skip the check const maintenanceWindowActive = await this.isInMaintenanceWindow(monitorId, teamId); if (maintenanceWindowActive) { - this.logger.info({ + this.logger.debug({ message: `Monitor ${monitorId} is in maintenance window`, service: SERVICE_NAME, method: "getMonitorJob", }); return; } - const networkResponse = await this.networkService.requestStatus(monitor); - if (!networkResponse) { + // Step 2. Request monitor status + const status = await this.networkService.requestStatus(monitor); + if (!status) { throw new Error("No network response"); } - const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(networkResponse); + const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(status); this.notificationService .handleNotifications({ - ...networkResponse, + ...status, monitor: updatedMonitor, prevStatus, statusChanged, }) - .catch((error) => { + .catch((error: any) => { this.logger.error({ message: error.message, service: SERVICE_NAME, @@ -66,7 +80,7 @@ class SuperSimpleQueueHelper { stack: error.stack, }); }); - } catch (error) { + } catch (error: any) { this.logger.warn({ message: error.message, service: error.service || SERVICE_NAME, @@ -78,13 +92,13 @@ class SuperSimpleQueueHelper { }; }; - async isInMaintenanceWindow(monitorId, teamId) { + async isInMaintenanceWindow(monitorId: string, teamId: string) { const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({ - monitorId: monitorId.toString(), - teamId: teamId.toString(), + monitorId: monitorId, + teamId: teamId, }); // Check for active maintenance window: - const maintenanceWindowIsActive = maintenanceWindows.reduce((acc, window) => { + const maintenanceWindowIsActive = maintenanceWindows.reduce((acc: any, window: any) => { if (window.active) { const start = new Date(window.start); const end = new Date(window.end); diff --git a/server/test/superSimpleQueueHelper.test.ts b/server/test/superSimpleQueueHelper.test.ts new file mode 100644 index 000000000..056173c82 --- /dev/null +++ b/server/test/superSimpleQueueHelper.test.ts @@ -0,0 +1,99 @@ +import { describe, expect, it, jest } from "@jest/globals"; +import SuperSimpleQueueHelper from "../src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts"; +import type { Monitor } from "../src/types/monitor.ts"; + +const createLogger = () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn() }); + +const createHelper = (overrides?: Partial[0]>) => { + const maintenanceWindowModule = { + getMaintenanceWindowsByMonitorId: jest.fn().mockResolvedValue([]), + }; + const helper = new SuperSimpleQueueHelper({ + db: { maintenanceWindowModule }, + logger: createLogger(), + networkService: { requestStatus: jest.fn() }, + statusService: { updateStatus: jest.fn() }, + notificationService: { handleNotifications: jest.fn().mockResolvedValue(undefined) }, + ...overrides, + }); + return { helper, maintenanceWindowModule }; +}; + +describe("SuperSimpleQueueHelper", () => { + describe("getMonitorJob", () => { + it("skips execution when monitor is in maintenance window", async () => { + const { helper } = createHelper(); + const spy = jest.spyOn(helper, "isInMaintenanceWindow").mockResolvedValue(true); + const job = helper.getMonitorJob(); + await job({ id: "m1", teamId: "team", interval: 60000 } as Monitor); + expect(helper["networkService"].requestStatus).not.toHaveBeenCalled(); + expect(helper["logger"].info).toHaveBeenCalledWith( + expect.objectContaining({ message: expect.stringContaining("Monitor m1 is in maintenance window") }) + ); + spy.mockRestore(); + }); + + it("processes monitor status and notifications when active", async () => { + const networkResponse = { monitor: { id: "m1" }, status: true }; + const updatedMonitor = { id: "m1", status: true }; + const { helper } = createHelper({ + networkService: { requestStatus: jest.fn().mockResolvedValue(networkResponse) }, + statusService: { + updateStatus: jest.fn().mockResolvedValue({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }), + }, + notificationService: { handleNotifications: jest.fn().mockResolvedValue(undefined) }, + }); + jest.spyOn(helper, "isInMaintenanceWindow").mockResolvedValue(false); + const job = helper.getMonitorJob(); + const monitor = { id: "m1", teamId: "team" } as Monitor; + await job(monitor); + expect(helper["networkService"].requestStatus).toHaveBeenCalledWith(monitor); + expect(helper["statusService"].updateStatus).toHaveBeenCalledWith(networkResponse); + expect(helper["notificationService"].handleNotifications).toHaveBeenCalledWith( + expect.objectContaining({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }) + ); + }); + + it("throws when monitor id is missing", async () => { + const { helper } = createHelper(); + const job = helper.getMonitorJob(); + await expect(job({} as Monitor)).rejects.toThrow("No monitor id"); + expect(helper["logger"].warn).toHaveBeenCalled(); + }); + }); + + describe("isInMaintenanceWindow", () => { + it("returns true when an active window spans now", async () => { + const now = new Date(); + const { helper, maintenanceWindowModule } = createHelper(); + maintenanceWindowModule.getMaintenanceWindowsByMonitorId.mockResolvedValue([ + { + active: true, + start: new Date(now.getTime() - 1000).toISOString(), + end: new Date(now.getTime() + 1000).toISOString(), + repeat: 0, + }, + ]); + await expect(helper.isInMaintenanceWindow("m1", "team")).resolves.toBe(true); + }); + + it("returns true when repeat interval advances window into current time", async () => { + const now = Date.now(); + const { helper, maintenanceWindowModule } = createHelper(); + maintenanceWindowModule.getMaintenanceWindowsByMonitorId.mockResolvedValue([ + { + active: true, + start: new Date(now - 7200000).toISOString(), + end: new Date(now - 6600000).toISOString(), + repeat: 3600000, + }, + ]); + await expect(helper.isInMaintenanceWindow("m1", "team")).resolves.toBe(true); + }); + + it("returns false when no active windows exist", async () => { + const { helper } = createHelper(); + await expect(helper.isInMaintenanceWindow("m1", "team")).resolves.toBe(false); + }); + }); +}); diff --git a/server/tsconfig.jest.json b/server/tsconfig.jest.json index 928b0977c..80c321cf6 100644 --- a/server/tsconfig.jest.json +++ b/server/tsconfig.jest.json @@ -3,7 +3,8 @@ "compilerOptions": { "rootDir": ".", "types": ["jest"], - "noEmit": true + "noEmit": true, + "resolveJsonModule": true }, "include": ["src", "test"] } diff --git a/server/tsconfig.json b/server/tsconfig.json index cbb874656..bdb3a7b0e 100644 --- a/server/tsconfig.json +++ b/server/tsconfig.json @@ -4,7 +4,7 @@ "allowJs": true, "checkJs": false, "paths": { - "@/*": ["./src/*"] // allows "@/db" -> "./src/db" + "@/*": ["./src/*"] }, "outDir": "./dist", "strict": true, From ed8083781061773239d894686c2cb193b01d2efb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 22:33:25 +0000 Subject: [PATCH 229/400] related serverices to TS --- server/jest.config.ts | 16 +- server/src/service/business/checkService.ts | 3 + .../SuperSimpleQueueHelper.ts | 1 - .../{networkService.js => networkService.ts} | 141 +++++++++++++----- .../{statusService.js => statusService.ts} | 111 ++++++-------- server/test/superSimpleQueueHelper.test.ts | 4 +- 6 files changed, 162 insertions(+), 114 deletions(-) rename server/src/service/infrastructure/{networkService.js => networkService.ts} (84%) rename server/src/service/infrastructure/{statusService.js => statusService.ts} (78%) diff --git a/server/jest.config.ts b/server/jest.config.ts index 4fb2c9831..fd0c87ad7 100644 --- a/server/jest.config.ts +++ b/server/jest.config.ts @@ -1,14 +1,5 @@ -import fs from "fs"; -import path from "path"; -import { fileURLToPath } from "url"; -import { pathsToModuleNameMapper } from "ts-jest"; import type { Config } from "jest"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); -const tsconfigPath = path.resolve(__dirname, "./tsconfig.json"); -const tsconfig = JSON.parse(fs.readFileSync(tsconfigPath, "utf-8")); - const config: Config = { rootDir: ".", testEnvironment: "node", @@ -17,7 +8,12 @@ const config: Config = { "^.+\\.(t|j)sx?$": ["ts-jest", { useESM: true, tsconfig: "./tsconfig.jest.json" }], }, moduleNameMapper: { - ...pathsToModuleNameMapper(tsconfig.compilerOptions.paths || {}, { prefix: "/" }), + "^@/validation/(.*)\\.js$": "/src/validation/$1.js", + "^@/utils/(AppError)\\.js$": "/src/utils/$1.ts", + "^@/utils/(.*)\\.js$": "/src/utils/$1.js", + "^@/(.*)\\.ts$": "/src/$1.ts", + "^@/(.*)\\.js$": "/src/$1.ts", + "^@/(.*)$": "/src/$1", }, testMatch: ["/test/**/*.test.ts"], setupFilesAfterEnv: [], diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index 61e2fb6ad..8aea221f6 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -1,4 +1,5 @@ import { IMonitorsRepository } from "@/repositories/index.js"; +import type { MonitorType } from "@/types/index.js"; const SERVICE_NAME = "checkService"; @@ -35,6 +36,8 @@ class CheckService { return CheckService.SERVICE_NAME; } + buildCheck = async (statusResponse: any, type: MonitorType) => {}; + getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => { if (!monitorId) { throw this.errorService.createBadRequestError("No monitor ID in request"); diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 678986c62..b025a65c6 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -1,7 +1,6 @@ const SERVICE_NAME = "JobQueueHelper"; import type { Monitor } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; -import { error } from "winston"; class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; diff --git a/server/src/service/infrastructure/networkService.js b/server/src/service/infrastructure/networkService.ts similarity index 84% rename from server/src/service/infrastructure/networkService.js rename to server/src/service/infrastructure/networkService.ts index 2375e7233..b9f02e7c9 100644 --- a/server/src/service/infrastructure/networkService.js +++ b/server/src/service/infrastructure/networkService.ts @@ -1,10 +1,63 @@ +import { Got } from "got"; +import type { Monitor } from "@/types/index.js"; + import CacheableLookup from "cacheable-lookup"; const SERVICE_NAME = "NetworkService"; class NetworkService { static SERVICE_NAME = SERVICE_NAME; - constructor({ axios, got, https, jmespath, GameDig, ping, logger, http, Docker, net, stringService, settingsService }) { + private TYPE_PING: string; + private TYPE_HTTP: string; + private TYPE_PAGESPEED: string; + private TYPE_HARDWARE: string; + private TYPE_DOCKER: string; + private TYPE_PORT: string; + private TYPE_GAME: string; + private SERVICE_NAME: string; + private NETWORK_ERROR: number; + private PING_ERROR: number; + + private axios: any; + private got: Got; + private https: any; + private jmespath: any; + private GameDig: any; + private ping: any; + private logger: any; + private http: any; + private Docker: any; + private net: any; + private stringService: any; + private settingsService: any; + + constructor({ + axios, + got, + https, + jmespath, + GameDig, + ping, + logger, + http, + Docker, + net, + stringService, + settingsService, + }: { + axios: any; + got: Got; + https: any; + jmespath: any; + GameDig: any; + ping: any; + logger: any; + http: any; + Docker: any; + net: any; + stringService: any; + settingsService: any; + }) { this.TYPE_PING = "ping"; this.TYPE_HTTP = "http"; this.TYPE_PAGESPEED = "pagespeed"; @@ -17,11 +70,11 @@ class NetworkService { this.PING_ERROR = 5001; this.axios = axios; this.https = https; + this.http = http; this.jmespath = jmespath; this.GameDig = GameDig; this.ping = ping; this.logger = logger; - this.http = http; this.Docker = Docker; this.net = net; this.stringService = stringService; @@ -39,7 +92,7 @@ class NetworkService { } // Helper functions - async timeRequest(operation) { + async timeRequest(operation: any) { const start = process.hrtime.bigint(); try { const response = await operation(); @@ -52,7 +105,7 @@ class NetworkService { } // Main entry point - async requestStatus(monitor) { + async requestStatus(monitor: Monitor) { const type = monitor?.type || "unknown"; switch (type) { case this.TYPE_PING: @@ -74,7 +127,7 @@ class NetworkService { } } - async requestPing(monitor) { + async requestPing(monitor: Monitor) { try { if (!monitor?.url) { throw new Error("Monitor URL is required"); @@ -112,16 +165,16 @@ class NetworkService { } return pingResponse; - } catch (err) { + } catch (err: any) { err.service = this.SERVICE_NAME; err.method = "requestPing"; throw err; } } - async requestHttp(monitor) { + async requestHttp(monitor: Monitor) { const { url, secret, id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor; - const httpResponse = { + const httpResponse: Record = { monitorId: id, teamId: teamId, type, @@ -131,7 +184,7 @@ class NetworkService { if (!url) { throw new Error("Monitor URL is required"); } - const config = { + const config: Record = { headers: secret ? { Authorization: `Bearer ${secret}` } : undefined, }; @@ -145,7 +198,7 @@ class NetworkService { const response = await this.got(url, config); - let payload; + let payload: any; const contentType = response.headers["content-type"]; if (contentType && contentType.includes("application/json")) { @@ -231,7 +284,7 @@ class NetworkService { } } return httpResponse; - } catch (err) { + } catch (err: any) { if (err.name === "HTTPError" || err.name === "RequestError") { httpResponse.code = err?.response?.statusCode || this.NETWORK_ERROR; httpResponse.status = false; @@ -247,7 +300,7 @@ class NetworkService { } } - async requestPageSpeed(monitor) { + async requestPageSpeed(monitor: Monitor) { try { const url = monitor.url; if (!url) { @@ -269,24 +322,24 @@ class NetworkService { ...monitor, url: pageSpeedUrl, }); - } catch (err) { + } catch (err: any) { err.service = this.SERVICE_NAME; err.method = "requestPageSpeed"; throw err; } } - async requestHardware(monitor) { + async requestHardware(monitor: Monitor) { try { return await this.requestHttp(monitor); - } catch (err) { + } catch (err: any) { err.service = this.SERVICE_NAME; err.method = "requestHardware"; throw err; } } - async requestDocker(monitor) { + async requestDocker(monitor: Monitor) { try { if (!monitor.url) { throw new Error("Monitor URL is required"); @@ -297,7 +350,7 @@ class NetworkService { handleError: true, // Enable error handling }); - const dockerResponse = { + const dockerResponse: Record = { monitorId: monitor.id, type: monitor.type, }; @@ -309,18 +362,18 @@ class NetworkService { // Priority-based matching to avoid ambiguity: // 1. Exact full ID match (64-char) - let exactIdMatch = containers.find((c) => c.Id.toLowerCase() === normalizedInput); + let exactIdMatch = containers.find((c: any) => c.Id.toLowerCase() === normalizedInput); // 2. Exact container name match (case-insensitive) - let exactNameMatch = containers.find((c) => - c.Names.some((name) => { + let exactNameMatch = containers.find((c: any) => + c.Names.some((name: string) => { const cleanName = name.replace(/^\/+/, "").toLowerCase(); return cleanName === normalizedInput; }) ); // 3. Partial ID match (fallback for backwards compatibility) - let partialIdMatch = containers.find((c) => c.Id.toLowerCase().startsWith(normalizedInput)); + let partialIdMatch = containers.find((c: any) => c.Id.toLowerCase().startsWith(normalizedInput)); // Select container based on priority let targetContainer = exactIdMatch || exactNameMatch || partialIdMatch; @@ -360,7 +413,9 @@ class NetworkService { } const container = docker.getContainer(targetContainer.Id); - const { response, responseTime, error } = await this.timeRequest(() => container.inspect()); + const { response, responseTime, error }: { response?: any; responseTime?: number; error?: any } = await this.timeRequest(() => + container.inspect() + ); dockerResponse.responseTime = responseTime; dockerResponse.status = response?.State?.Status === "running" ? true : false; @@ -375,14 +430,14 @@ class NetworkService { } return dockerResponse; - } catch (err) { + } catch (err: any) { err.service = this.SERVICE_NAME; err.method = "requestDocker"; throw err; } } - async requestPort(monitor) { + async requestPort(monitor: Monitor) { try { const { url, port } = monitor; const { response, responseTime, error } = await this.timeRequest(async () => { @@ -405,7 +460,7 @@ class NetworkService { reject(new Error("Connection timeout")); }); - socket.on("error", (err) => { + socket.on("error", (err: any) => { socket.destroy(); reject(err); }); @@ -429,18 +484,18 @@ class NetworkService { } return portResponse; - } catch (error) { + } catch (error: any) { error.service = this.SERVICE_NAME; error.method = "requestTCP"; throw error; } } - async requestGame(monitor) { + async requestGame(monitor: Monitor) { try { const { url, port, gameId } = monitor; - const gameResponse = { + const gameResponse: Record = { code: 200, status: true, message: "Success", @@ -452,7 +507,7 @@ class NetworkService { type: gameId, host: url, port: port, - }).catch((error) => { + }).catch((error: any) => { this.logger.warn({ message: error.message, service: this.SERVICE_NAME, @@ -471,21 +526,21 @@ class NetworkService { gameResponse.responseTime = state.ping; gameResponse.payload = state; return gameResponse; - } catch (error) { + } catch (error: any) { error.service = this.SERVICE_NAME; error.method = "requestPing"; throw error; } } - async handleUnsupportedType(type) { - const err = new Error(`Unsupported type: ${type}`); + async handleUnsupportedType(type: string) { + const err: any = new Error(`Unsupported type: ${type}`); err.service = this.SERVICE_NAME; err.method = "getStatus"; throw err; } // Other network requests unrelated to monitoring: - async requestWebhook(type, url, body) { + async requestWebhook(type: string, url: string, body: any) { try { const response = await this.axios.post(url, body, { headers: { @@ -500,7 +555,7 @@ class NetworkService { message: `Successfully sent ${type} notification`, payload: response.data, }; - } catch (error) { + } catch (error: any) { this.logger.warn({ message: error.message, service: this.SERVICE_NAME, @@ -517,7 +572,7 @@ class NetworkService { } } - async requestPagerDuty({ message, routingKey, monitorUrl }) { + async requestPagerDuty({ message, routingKey, monitorUrl }: { message: string; routingKey: string; monitorUrl: string }) { try { const response = await this.axios.post(`https://events.pagerduty.com/v2/enqueue`, { routing_key: routingKey, @@ -532,7 +587,7 @@ class NetworkService { if (response?.data?.status !== "success") return false; return true; - } catch (error) { + } catch (error: any) { error.details = error.response?.data; error.service = this.SERVICE_NAME; error.method = "requestPagerDuty"; @@ -540,7 +595,17 @@ class NetworkService { } } - async requestMatrix({ homeserverUrl, accessToken, roomId, message }) { + async requestMatrix({ + homeserverUrl, + accessToken, + roomId, + message, + }: { + homeserverUrl: string; + accessToken: string; + roomId: string; + message: string; + }) { try { const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`; const body = { @@ -560,7 +625,7 @@ class NetworkService { code: response.status, message: "Successfully sent Matrix notification", }; - } catch (error) { + } catch (error: any) { this.logger.warn({ message: error.message, service: this.SERVICE_NAME, diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.ts similarity index 78% rename from server/src/service/infrastructure/statusService.js rename to server/src/service/infrastructure/statusService.ts index 763b6d1fa..d68ec345a 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.ts @@ -1,11 +1,30 @@ +import { IMonitorsRepository } from "@/repositories/index.js"; import MonitorStats from "../../db/models/MonitorStats.js"; import { CheckModel } from "@/db/models/index.js"; +import type { Monitor } from "@/types/index.js"; const SERVICE_NAME = "StatusService"; class StatusService { static SERVICE_NAME = SERVICE_NAME; + private db: any; + private logger: any; + private buffer: any; + private incidentService: any; + private monitorsRepository: IMonitorsRepository; - constructor({ db, logger, buffer, incidentService, monitorsRepository }) { + constructor({ + db, + logger, + buffer, + incidentService, + monitorsRepository, + }: { + db: any; + logger: any; + buffer: any; + incidentService: any; + monitorsRepository: IMonitorsRepository; + }) { this.db = db; this.logger = logger; this.buffer = buffer; @@ -17,7 +36,7 @@ class StatusService { return StatusService.SERVICE_NAME; } - async updateRunningStats({ monitor, networkResponse }) { + async updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }) { try { const monitorId = monitor.id; const { responseTime, status } = networkResponse; @@ -78,9 +97,9 @@ class StatusService { await stats.save(); return true; - } catch (error) { + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, message: error.message, method: "updateRunningStats", stack: error.stack, @@ -89,23 +108,13 @@ class StatusService { } } - getStatusString = (status) => { + getStatusString = (status: boolean | undefined) => { if (status === true) return "up"; if (status === false) return "down"; return "unknown"; }; - /** - * Saves check if needed and adds to incident buffer - * Removes check from checks buffer if it was saved immediately - * - * @param {Object} check - The check object - * @param {Object} monitor - The monitor object - * @param {string} action - The incident action ("create" or "resolve") - * @param {string} errorContext - Context for error messages - * @returns {Promise} - */ - handleIncidentForCheck = async (check, monitor, action, errorContext = "incident handling") => { + handleIncidentForCheck = async (check: any, monitor: Monitor, action: any, errorContext = "incident handling") => { try { let savedCheck = check; @@ -115,9 +124,9 @@ class StatusService { savedCheck = await checkModel.save(); this.buffer.removeCheckFromBuffer(check); - } catch (checkError) { + } catch (checkError: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "handleIncidentForCheck", message: `Failed to save check immediately for ${errorContext}: ${checkError.message}`, monitorId: monitor.id, @@ -130,9 +139,9 @@ class StatusService { if (savedCheck && savedCheck._id) { try { this.buffer.addIncidentToBuffer({ monitor, check: savedCheck, action }); - } catch (incidentError) { + } catch (incidentError: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "handleIncidentForCheck", message: `Failed to add incident to buffer for ${errorContext}: ${incidentError.message}`, monitorId: monitor.id, @@ -141,9 +150,9 @@ class StatusService { }); } } - } catch (error) { + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "handleIncidentForCheck", message: `Error in ${errorContext}: ${error.message}`, monitorId: monitor?.id, @@ -151,19 +160,8 @@ class StatusService { }); } }; - /** - * Updates the status of a monitor based on the network response. - * - * @param {Object} networkResponse - The network response containing monitorId and status. - * @param {string} networkResponse.monitorId - The ID of the monitor. - * @param {string} networkResponse.status - The new status of the monitor. - * @returns {Promise} - A promise that resolves to an object containinfg the monitor, statusChanged flag, and previous status if the status changed, or false if an error occurred. - * @returns {Promise} returnObject - The object returned by the function. - * @returns {Object} returnObject.monitor - The monitor object. - * @returns {boolean} returnObject.statusChanged - Flag indicating if the status has changed. - * @returns {boolean} returnObject.prevStatus - The previous status of the monitor - */ - updateStatus = async (networkResponse) => { + + updateStatus = async (networkResponse: any) => { const check = this.buildCheck(networkResponse); await this.insertCheck(check); try { @@ -221,7 +219,7 @@ class StatusService { if (statusChanged) { this.logger.info({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, message: `${monitor.name} went from ${this.getStatusString(prevStatus)} to ${this.getStatusString(newStatus)}`, prevStatus, newStatus, @@ -242,7 +240,7 @@ class StatusService { if (lastManuallyResolvedIncident && lastManuallyResolvedIncident.endTime) { try { - const checksAfterResolution = await Check.find({ + const checksAfterResolution = await CheckModel.find({ monitorId: monitor.id, createdAt: { $gt: lastManuallyResolvedIncident.endTime }, }) @@ -258,9 +256,9 @@ class StatusService { } else { calculatedFailureRate = 0; } - } catch (checkQueryError) { + } catch (checkQueryError: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "updateStatus", message: `Failed to query checks after manual resolution: ${checkQueryError.message}`, monitorId: monitor.id, @@ -272,9 +270,9 @@ class StatusService { if (calculatedFailureRate >= monitor.statusWindowThreshold) { await this.handleIncidentForCheck(check, monitor, "create", "threshold check without status change"); } - } catch (error) { + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "updateStatus", message: `Error handling threshold check without status change: ${error.message}`, monitorId: monitor.id, @@ -293,27 +291,14 @@ class StatusService { code, timestamp: new Date().getTime(), }; - } catch (error) { - error.service = this.SERVICE_NAME; + } catch (error: any) { + error.service = SERVICE_NAME; error.method = "updateStatus"; throw error; } }; - /** - * Builds a check object from the network response. - * - * @param {Object} networkResponse - The network response object. - * @param {string} networkResponse.monitorId - The monitor ID. - * @param {string} networkResponse.type - The type of the response. - * @param {string} networkResponse.status - The status of the response. - * @param {number} networkResponse.responseTime - The response time. - * @param {number} networkResponse.code - The status code. - * @param {string} networkResponse.message - The message. - * @param {Object} networkResponse.payload - The payload of the response. - * @returns {Object} The check object. - */ - buildCheck = (networkResponse) => { + buildCheck = (networkResponse: any) => { const { monitorId, teamId, @@ -355,7 +340,7 @@ class StatusService { if (typeof payload === "undefined") { this.logger.warn({ message: "Failed to build check", - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "buildCheck", details: "empty payload", }); @@ -363,7 +348,7 @@ class StatusService { } const categories = payload?.lighthouseResult?.categories ?? {}; const audits = payload?.lighthouseResult?.audits ?? {}; - const mapAudit = (audit) => { + const mapAudit = (audit: any) => { if (!audit || typeof audit !== "object") { return undefined; } @@ -416,22 +401,22 @@ class StatusService { * @param {Object} networkResponse.payload - The payload of the response. * @returns {Promise} A promise that resolves when the check is inserted. */ - insertCheck = async (check) => { + insertCheck = async (check: any) => { try { if (typeof check === "undefined") { this.logger.warn({ message: "Failed to build check", - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "insertCheck", }); return false; } this.buffer.addToBuffer({ check }); return true; - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, - service: error.service || this.SERVICE_NAME, + service: error.service || SERVICE_NAME, method: error.method || "insertCheck", details: error.details || `Error inserting check for monitor: ${check?.monitorId}`, stack: error.stack, diff --git a/server/test/superSimpleQueueHelper.test.ts b/server/test/superSimpleQueueHelper.test.ts index 056173c82..997eb9e83 100644 --- a/server/test/superSimpleQueueHelper.test.ts +++ b/server/test/superSimpleQueueHelper.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, jest } from "@jest/globals"; import SuperSimpleQueueHelper from "../src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts"; import type { Monitor } from "../src/types/monitor.ts"; -const createLogger = () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn() }); +const createLogger = () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn() }); const createHelper = (overrides?: Partial[0]>) => { const maintenanceWindowModule = { @@ -27,7 +27,7 @@ describe("SuperSimpleQueueHelper", () => { const job = helper.getMonitorJob(); await job({ id: "m1", teamId: "team", interval: 60000 } as Monitor); expect(helper["networkService"].requestStatus).not.toHaveBeenCalled(); - expect(helper["logger"].info).toHaveBeenCalledWith( + expect(helper["logger"].debug).toHaveBeenCalledWith( expect.objectContaining({ message: expect.stringContaining("Monitor m1 is in maintenance window") }) ); spy.mockRestore(); From 8b2ee8d937094dd284a044ae567d789e716abb73 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Thu, 15 Jan 2026 17:30:15 -0500 Subject: [PATCH 230/400] Simplify theme and fix Redux state bugs ## Changes - Remove unused semantic color groups (successSecondary, map, chatbot) - Remove unused properties from primary and tertiary groups - Inline accent.darker usage in globalTheme.js - Fix greeting component crash with persisted Redux state - Fix MonitorGrid key prop warning ## Details Theme simplification: - Reduced constants.js from 355 to 291 lines - Removed 3 unused semantic color groups - Removed 6 unused properties (contrastBorder, contrastBorderDisabled, etc.) - Inlined darken() call for accent hover states Bug fixes: - Added null-safe selectors in greeting.jsx for state.ui.greeting - Added initialization check in setGreeting reducer for older persisted state - Fixed key prop in MonitorGrid to handle both _id and id - Removed unused console.log and Grid2 import --- client/src/Components/LanguageSelector.jsx | 2 +- client/src/Components/v1/I18nLoader/index.jsx | 2 +- client/src/Components/v1/Sidebar/index.jsx | 2 +- client/src/Features/UI/uiSlice.js | 3 + .../Monitors/Components/MonitorGrid/index.jsx | 9 +-- client/src/Pages/Settings/index.jsx | 2 +- client/src/Utils/Theme/constants.js | 66 +------------------ client/src/Utils/Theme/globalTheme.js | 4 +- client/src/Utils/greeting.jsx | 4 +- 9 files changed, 17 insertions(+), 77 deletions(-) diff --git a/client/src/Components/LanguageSelector.jsx b/client/src/Components/LanguageSelector.jsx index 9b46204e3..fdda73f4a 100644 --- a/client/src/Components/LanguageSelector.jsx +++ b/client/src/Components/LanguageSelector.jsx @@ -16,7 +16,7 @@ const langMap = { const LanguageSelector = () => { const { i18n } = useTranslation(); const theme = useTheme(); - const { language } = useSelector((state) => state.ui); + const { language = "en" } = useSelector((state) => state.ui); const dispatch = useDispatch(); const handleChange = (event) => { const newLang = event.target.value; diff --git a/client/src/Components/v1/I18nLoader/index.jsx b/client/src/Components/v1/I18nLoader/index.jsx index 2a6a4966d..676e63d7b 100644 --- a/client/src/Components/v1/I18nLoader/index.jsx +++ b/client/src/Components/v1/I18nLoader/index.jsx @@ -2,7 +2,7 @@ import i18n from "../../../Utils/i18n.js"; import { useSelector } from "react-redux"; import { useEffect } from "react"; const I18nLoader = () => { - const language = useSelector((state) => state.ui.language); + const language = useSelector((state) => state.ui.language ?? "en"); useEffect(() => { if (language && i18n.language !== language) { diff --git a/client/src/Components/v1/Sidebar/index.jsx b/client/src/Components/v1/Sidebar/index.jsx index eb38b9129..80022c7e3 100644 --- a/client/src/Components/v1/Sidebar/index.jsx +++ b/client/src/Components/v1/Sidebar/index.jsx @@ -84,7 +84,7 @@ const Sidebar = () => { const { t } = useTranslation(); const navigate = useNavigate(); // Redux state - const collapsed = useSelector((state) => state.ui.sidebar.collapsed); + const collapsed = useSelector((state) => state.ui.sidebar?.collapsed ?? false); const menu = getMenu(t); const otherMenuItems = getOtherMenuItems(t); diff --git a/client/src/Features/UI/uiSlice.js b/client/src/Features/UI/uiSlice.js index 63f7ae90e..af9731e9b 100644 --- a/client/src/Features/UI/uiSlice.js +++ b/client/src/Features/UI/uiSlice.js @@ -58,6 +58,9 @@ const uiSlice = createSlice({ state.showURL = action.payload; }, setGreeting(state, action) { + if (!state.greeting) { + state.greeting = { index: 0, lastUpdate: null }; + } state.greeting.index = action.payload.index; state.greeting.lastUpdate = action.payload.lastUpdate; }, diff --git a/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx b/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx index 8708502cd..9df5f7577 100644 --- a/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx +++ b/client/src/Pages/PageSpeed/Monitors/Components/MonitorGrid/index.jsx @@ -1,8 +1,9 @@ -import { Grid, Grid2 } from "@mui/material"; +import { Grid } from "@mui/material"; import Card from "../Card/index.jsx"; -const MonitorGrid = ({ shouldRender, monitors }) => { - console.log(monitors); +const MonitorGrid = ({ monitors, shouldRender = true }) => { + if (!shouldRender) return null; + return ( { {monitors?.map((monitor) => ( ))} diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx index 27cc6bb21..3e753ab60 100644 --- a/client/src/Pages/Settings/index.jsx +++ b/client/src/Pages/Settings/index.jsx @@ -33,7 +33,7 @@ const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }]; const Settings = () => { // Redux state - const { mode, language, timezone, showURL } = useSelector((state) => state.ui); + const { mode, language = "en", timezone, showURL } = useSelector((state) => state.ui); // Local state const [settingsData, setSettingsData] = useState({}); diff --git a/client/src/Utils/Theme/constants.js b/client/src/Utils/Theme/constants.js index 45217fdeb..2664c6126 100644 --- a/client/src/Utils/Theme/constants.js +++ b/client/src/Utils/Theme/constants.js @@ -1,4 +1,4 @@ -import { lighten, darken } from "@mui/material/styles"; // CAIO_REVIEW +import { lighten } from "@mui/material/styles"; // CAIO_REVIEW const typographyBase = 13; @@ -133,12 +133,6 @@ const newSemanticColors = { light: lighten(newColors.blueBlueWave, 0.2), //CAIO_REVIEW dark: lighten(newColors.blueBlueWave, 0.2), //CAIO_REVIEW }, - darker: { - // CAIO_REVIEW - light: darken(newColors.blueBlueWave, 0.2), // CAIO_REVIEW - dark: darken(newColors.blueBlueWave, 0.2), // CAIO_REVIEW - }, - contrastText: { light: newColors.offWhite, dark: newColors.offWhite, @@ -165,14 +159,6 @@ const newSemanticColors = { light: newColors.blueGray500, dark: newColors.gray500, }, - contrastBorder: { - light: newColors.gray500, - dark: newColors.blueGray600, - }, - contrastBorderDisabled: { - light: newColors.gray100, - dark: newColors.blueGray800, - }, lowContrast: { light: newColors.gray250, dark: newColors.blueGray600, @@ -207,18 +193,6 @@ const newSemanticColors = { light: newColors.blueGray800, dark: newColors.gray100, }, - background: { - light: newColors.gray50, - dark: newColors.offBlack, - }, - border: { - light: newColors.gray500, - dark: newColors.blueGray600, - }, - cardShadow: { - light: "0 0 0 1px rgba(0, 0, 0, 0.04), 0 12px 24px rgba(0, 0, 0, 0.08)", - dark: "0 2px 10px rgba(0, 0, 0, 0.2), 0 0 0 1px rgba(255, 255, 255, 0.04)", - }, }, success: { main: { @@ -234,20 +208,6 @@ const newSemanticColors = { dark: newColors.green200, }, }, - successSecondary: { - main: { - light: undefined, - dark: "#073C2D", - }, - contrastText: { - light: undefined, - dark: "#34D399", - }, - lowContrast: { - light: undefined, - dark: "#094434", - }, - }, warning: { main: { light: newColors.orange700, @@ -316,30 +276,6 @@ const newSemanticColors = { }, }, - map: { - main: { - light: newColors.offWhite, - dark: undefined, - }, - lowContrast: { - light: newColors.gray200, - dark: undefined, - }, - highContrast: { - light: newColors.gray500, - dark: undefined, - }, - }, - chatbot: { - background: { - light: newColors.gray200, - dark: "#112B2B", - }, - textAccent: { - light: "#C084FC", - dark: "#C084FC", - }, - }, alert: { main: { light: newColors.gray200, diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js index e65baf4c0..58e1a9b63 100644 --- a/client/src/Utils/Theme/globalTheme.js +++ b/client/src/Utils/Theme/globalTheme.js @@ -64,7 +64,7 @@ const baseTheme = (palette) => ({ letterSpacing: "0.5px", textShadow: "0 0 1px rgba(0, 0, 0, 0.15)", "&:hover": { - backgroundColor: darken(theme.palette.accent.darker, 0.05), + backgroundColor: darken(theme.palette.accent.main, 0.25), boxShadow: `0 2px 6px rgba(0, 0, 0, 0.1)`, transition: "all 0.2s ease-in-out", }, @@ -74,7 +74,7 @@ const baseTheme = (palette) => ({ props: (props) => props.color === "accent", style: { "&:hover": { - backgroundColor: theme.palette.accent.darker, + backgroundColor: darken(theme.palette.accent.main, 0.2), }, }, }, diff --git a/client/src/Utils/greeting.jsx b/client/src/Utils/greeting.jsx index d2b7fa033..c4afbf88c 100644 --- a/client/src/Utils/greeting.jsx +++ b/client/src/Utils/greeting.jsx @@ -136,8 +136,8 @@ const Greeting = ({ type = "" }) => { const dispatch = useDispatch(); const { t } = useTranslation(); const { firstName } = useSelector((state) => state.auth.user); - const index = useSelector((state) => state.ui.greeting.index); - const lastUpdate = useSelector((state) => state.ui.greeting.lastUpdate); + const index = useSelector((state) => state.ui.greeting?.index ?? 0); + const lastUpdate = useSelector((state) => state.ui.greeting?.lastUpdate ?? null); const now = new Date(); const hour = now.getHours(); From 0f764ca0841ab2910851d31549b49a30d17d0b62 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 15 Jan 2026 23:45:40 +0000 Subject: [PATCH 231/400] typing --- server/src/config/services.ts | 2 +- server/src/db/models/Check.ts | 7 +- .../repositories/checks/IChecksRepository.ts | 3 +- .../checks/MongoChecksRepistory.ts | 111 ++++++++- server/src/service/index.ts | 1 + .../SuperSimpleQueueHelper.ts | 15 +- .../service/infrastructure/networkService.ts | 220 +++++++++++++----- .../service/infrastructure/statusService.ts | 37 +-- server/src/types/check.ts | 27 +-- server/src/types/index.ts | 1 + server/src/types/network.ts | 112 +++++++++ server/test/superSimpleQueueHelper.test.ts | 11 +- 12 files changed, 431 insertions(+), 116 deletions(-) create mode 100644 server/src/types/network.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 90d6bb518..57a223ebf 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -155,7 +155,7 @@ export const initializeServices = async ({ // Repositories const monitorsRepository = new MongoMonitorsRepository(); - const checksRepository = new MongoChecksRepository(); + const checksRepository = new MongoChecksRepository(logger); const monitorStatsRepository = new MongoMonitorStatsRepository(); const statusPagesRepository = new MongoStatusPagesRepository(); diff --git a/server/src/db/models/Check.ts b/server/src/db/models/Check.ts index c176aa3bb..94dfa873c 100644 --- a/server/src/db/models/Check.ts +++ b/server/src/db/models/Check.ts @@ -11,8 +11,7 @@ import type { CheckMemoryInfo, CheckMetadata, CheckNetworkInterfaceInfo, - CheckTimings, - CheckTimingPhases, + GotTimings, ILighthouseAudit, } from "@/types/check.js"; @@ -35,7 +34,7 @@ interface CheckDocument extends CheckDocumentBase { _id: Types.ObjectId; } -const timingPhasesSchema = new Schema( +const timingPhasesSchema = new Schema( { wait: { type: Number, default: 0 }, dns: { type: Number, default: 0 }, @@ -49,7 +48,7 @@ const timingPhasesSchema = new Schema( { _id: false } ); -const timingsSchema = new Schema( +const timingsSchema = new Schema( { start: { type: Number, default: 0 }, socket: { type: Number, default: 0 }, diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index 19faa0159..5737daff6 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -1,4 +1,4 @@ -import type { Check, CheckAudits, MonitorType } from "@/types/index.js"; +import type { Check, CheckAudits, MonitorStatusResponse, MonitorType } from "@/types/index.js"; import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js"; export interface PageSpeedChecksResult { @@ -55,6 +55,7 @@ export interface UptimeChecksResult { export interface IChecksRepository { // create + buildCheck(status: MonitorStatusResponse, type: MonitorType): Check; // single fetch // collection fetch findLatestChecksByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise; diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 3816d8cc5..225aec193 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -10,18 +10,29 @@ import type { CheckMemoryInfo, CheckMetadata, CheckNetworkInterfaceInfo, - CheckTimings, + GotTimings, + MonitorStatusResponse, MonitorType, } from "@/types/index.js"; +import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; import { CheckModel, type CheckDocument } from "@/db/models/index.js"; import mongoose from "mongoose"; +const SERVICE_NAME = "StatusService"; + export type LatestChecksMap = Record; type DateRange = { start: Date; end: Date }; type HardwareAggregateData = { latestCheck: CheckDocument | null; totalChecks: number }; type HardwareUpChecks = { totalChecks: number }; class MongoChecksRepository implements IChecksRepository { + static SERVICE_NAME = SERVICE_NAME; + + private logger: any; + constructor(logger: any) { + this.logger = logger; + } + private toEntity = (doc: CheckDocument): Check => { const toStringId = (value: mongoose.Types.ObjectId | string | undefined | null): string => { if (!value) { @@ -44,7 +55,7 @@ class MongoChecksRepository implements IChecksRepository { return toDateString(value); }; - const mapTimings = (timings?: CheckTimings): CheckTimings => { + const mapTimings = (timings?: GotTimings): GotTimings => { const phases = timings?.phases ?? { wait: 0, dns: 0, @@ -176,6 +187,102 @@ class MongoChecksRepository implements IChecksRepository { }; }; + buildCheck = ( + statusResponse: MonitorStatusResponse + ) => { + const { + monitorId, + teamId, + type, + status, + responseTime, + code, + message, + payload, + first_byte_took, + body_read_took, + dns_took, + conn_took, + connect_took, + tls_took, + timings, + } = statusResponse; + + const check: any = { + metadata: { + monitorId, + teamId, + type, + }, + status, + statusCode: code, + responseTime, + timings: timings || {}, + message, + first_byte_took, + body_read_took, + dns_took, + conn_took, + connect_took, + tls_took, + }; + + if (type === "pagespeed") { + const pageSpeedPayload = payload as PageSpeedStatusPayload | undefined; + if (!pageSpeedPayload) { + this.logger.warn({ + message: "Failed to build check", + service: SERVICE_NAME, + method: "buildCheck", + details: "empty payload", + }); + return undefined; + } + const categories = pageSpeedPayload.lighthouseResult?.categories ?? {}; + const audits = pageSpeedPayload.lighthouseResult?.audits ?? {}; + const mapAudit = (audit: any) => { + if (!audit || typeof audit !== "object") { + return undefined; + } + return { + id: audit.id, + title: audit.title, + score: typeof audit.score === "number" ? audit.score : (audit.score ?? null), + displayValue: audit.displayValue, + numericValue: typeof audit.numericValue === "number" ? audit.numericValue : undefined, + numericUnit: audit.numericUnit, + }; + }; + check.accessibility = (categories?.accessibility?.score || 0) * 100; + check.bestPractices = (categories?.["best-practices"]?.score || 0) * 100; + check.seo = (categories?.seo?.score || 0) * 100; + check.performance = (categories?.performance?.score || 0) * 100; + check.audits = { + cls: mapAudit(audits?.["cumulative-layout-shift"]), + si: mapAudit(audits?.["speed-index"]), + fcp: mapAudit(audits?.["first-contentful-paint"]), + lcp: mapAudit(audits?.["largest-contentful-paint"]), + tbt: mapAudit(audits?.["total-blocking-time"]), + }; + } + + if (type === "hardware") { + const hardwarePayload = payload as HardwareStatusPayload | undefined; + const { cpu, memory, disk, host, net } = hardwarePayload?.data ?? {}; + const errorsSource = Array.isArray(hardwarePayload?.errors) + ? hardwarePayload?.errors + : (hardwarePayload?.errors as { errors?: CheckErrorInfo[] } | undefined)?.errors; + check.cpu = cpu ?? {}; + check.memory = memory ?? {}; + check.disk = disk ?? {}; + check.host = host ?? {}; + check.errors = errorsSource ?? []; + check.capture = hardwarePayload?.capture ?? {}; + check.net = net ?? {}; + } + return check; + }; + findLatestChecksByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise => { if (monitorIds.length === 0) { return {}; diff --git a/server/src/service/index.ts b/server/src/service/index.ts index 4793f055b..a094f8b01 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -1 +1,2 @@ export * from "@/service/business/monitorService.js"; +export * from "@/service/infrastructure/networkService.js"; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index b025a65c6..aa4c4ab10 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -1,13 +1,13 @@ const SERVICE_NAME = "JobQueueHelper"; import type { Monitor } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; - +import { INetworkService } from "@/service/index.js"; class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; private db: any; private logger: any; - private networkService: any; + private networkService: INetworkService; private statusService: any; private notificationService: any; @@ -20,7 +20,7 @@ class SuperSimpleQueueHelper { }: { db: any; logger: any; - networkService: any; + networkService: INetworkService; statusService: any; notificationService: any; }) { @@ -61,14 +61,13 @@ class SuperSimpleQueueHelper { throw new Error("No network response"); } - const { monitor: updatedMonitor, statusChanged, prevStatus } = await this.statusService.updateStatus(status); - + const statusChangeResult = await this.statusService.updateMonitorStatus(status); this.notificationService .handleNotifications({ ...status, - monitor: updatedMonitor, - prevStatus, - statusChanged, + monitor: statusChangeResult.monitor, + prevStatus: statusChangeResult.prevStatus, + statusChanged: statusChangeResult.statusChanged, }) .catch((error: any) => { this.logger.error({ diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index b9f02e7c9..e4e5972af 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -1,10 +1,38 @@ -import { Got } from "got"; -import type { Monitor } from "@/types/index.js"; +import { HTTPError, RequestError } from "got"; +import type { Got, Response } from "got"; +import type { Monitor, MonitorStatusResponse } from "@/types/index.js"; import CacheableLookup from "cacheable-lookup"; const SERVICE_NAME = "NetworkService"; -class NetworkService { +type MonitorStatusResponseOverrides = Partial, "monitorId" | "teamId" | "type">>; + +interface BuildStatusResponseArgs { + monitor: Monitor; + response?: Response | null; + error?: Error | RequestError | HTTPError | null; + payload?: T | null; + jsonPath?: string; + matchMethod?: MonitorStatusResponse["matchMethod"]; + expectedValue?: string; + extracted?: unknown; + overrides?: MonitorStatusResponseOverrides; +} + +export interface INetworkService { + readonly serviceName: string; + requestStatus(monitor: Monitor): Promise; + requestWebhook(type: string, url: string, body: any): Promise<{ type: string; status: boolean; code: number; message: string; payload?: unknown }>; + requestPagerDuty(args: { message: string; routingKey: string; monitorUrl: string }): Promise; + requestMatrix(args: { homeserverUrl: string; accessToken: string; roomId: string; message: string }): Promise<{ + status: boolean; + code: number; + message: string; + payload?: unknown; + }>; +} + +class NetworkService implements INetworkService { static SERVICE_NAME = SERVICE_NAME; private TYPE_PING: string; @@ -31,6 +59,60 @@ class NetworkService { private stringService: any; private settingsService: any; + private buildStatusResponse = ({ + monitor, + response, + error, + payload, + jsonPath, + matchMethod, + expectedValue, + extracted, + overrides, + }: BuildStatusResponseArgs): MonitorStatusResponse => { + if (error) { + const statusResponse: MonitorStatusResponse = { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: false, + code: this.NETWORK_ERROR, + message: error.message ?? "Network error", + responseTime: 0, + timings: undefined, + jsonPath, + matchMethod, + expectedValue, + extracted, + payload, + }; + if (error instanceof HTTPError || error instanceof RequestError) { + statusResponse.code = error?.response?.statusCode ?? this.NETWORK_ERROR; + statusResponse.message = error.message; + statusResponse.responseTime = error.timings?.phases?.total ?? 0; + statusResponse.timings = error.timings; + } + return { ...statusResponse, ...(overrides ?? {}) }; + } + + return { + monitorId: monitor.id, + teamId: monitor.teamId, + type: monitor.type, + status: response?.ok ?? false, + code: response?.statusCode ?? this.NETWORK_ERROR, + message: response?.statusMessage ?? "", + responseTime: response?.timings?.phases?.total ?? 0, + timings: response?.timings, + payload: payload ?? response?.body, + jsonPath, + matchMethod, + expectedValue, + extracted, + ...(overrides ?? {}), + }; + }; + constructor({ axios, got, @@ -91,13 +173,17 @@ class NetworkService { }); } + get serviceName(): string { + return NetworkService.SERVICE_NAME; + } + // Helper functions - async timeRequest(operation: any) { + private async timeRequest(operation: () => Promise): Promise<{ response: T | null; responseTime: number; error: unknown }> { const start = process.hrtime.bigint(); try { const response = await operation(); const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000); - return { response, responseTime: elapsedMs }; + return { response, responseTime: elapsedMs, error: null }; } catch (error) { const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000); return { response: null, responseTime: elapsedMs, error }; @@ -105,7 +191,7 @@ class NetworkService { } // Main entry point - async requestStatus(monitor: Monitor) { + async requestStatus(monitor: Monitor): Promise { const type = monitor?.type || "unknown"; switch (type) { case this.TYPE_PING: @@ -123,11 +209,11 @@ class NetworkService { case this.TYPE_GAME: return await this.requestGame(monitor); default: - return await this.handleUnsupportedType(type); + return this.handleUnsupportedType(type); } } - async requestPing(monitor: Monitor) { + private async requestPing(monitor: Monitor): Promise { try { if (!monitor?.url) { throw new Error("Monitor URL is required"); @@ -147,19 +233,21 @@ class NetworkService { throw new Error("Ping failed - no result returned"); } - const pingResponse = { - monitorId: monitor.id, - type: "ping", - status: response.alive, - code: 200, - responseTime: response.time, - message: "Success", + const pingResponse = this.buildStatusResponse({ + monitor, payload: response, - }; + overrides: { + status: (response as { alive?: boolean })?.alive ?? false, + code: 200, + message: "Success", + responseTime: (response as { time?: number })?.time ?? 0, + payload: response, + }, + }); if (error) { pingResponse.status = false; - pingResponse.code = 200; + pingResponse.code = this.PING_ERROR; pingResponse.message = "Ping failed"; return pingResponse; } @@ -172,13 +260,16 @@ class NetworkService { } } - async requestHttp(monitor: Monitor) { + private async requestHttp(monitor: Monitor): Promise { const { url, secret, id, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor; - const httpResponse: Record = { - monitorId: id, - teamId: teamId, - type, - }; + const httpResponse = this.buildStatusResponse({ + monitor, + overrides: { + status: false, + code: this.NETWORK_ERROR, + message: "Request not executed", + }, + }); try { if (!url) { @@ -211,12 +302,14 @@ class NetworkService { payload = response.body; } - httpResponse.code = response.statusCode; - httpResponse.status = response.ok; - httpResponse.message = response.statusMessage; - httpResponse.responseTime = response.timings.phases.total || 0; - httpResponse.payload = payload; - httpResponse.timings = response.timings || {}; + Object.assign(httpResponse, { + code: response.statusCode, + status: response.ok, + message: response.statusMessage ?? "", + responseTime: response.timings.phases.total || 0, + payload, + timings: response.timings, + }); if (!expectedValue && !jsonPath) { return httpResponse; @@ -300,7 +393,7 @@ class NetworkService { } } - async requestPageSpeed(monitor: Monitor) { + private async requestPageSpeed(monitor: Monitor): Promise { try { const url = monitor.url; if (!url) { @@ -329,7 +422,7 @@ class NetworkService { } } - async requestHardware(monitor: Monitor) { + private async requestHardware(monitor: Monitor): Promise { try { return await this.requestHttp(monitor); } catch (err: any) { @@ -339,7 +432,7 @@ class NetworkService { } } - async requestDocker(monitor: Monitor) { + private async requestDocker(monitor: Monitor): Promise { try { if (!monitor.url) { throw new Error("Monitor URL is required"); @@ -350,10 +443,14 @@ class NetworkService { handleError: true, // Enable error handling }); - const dockerResponse: Record = { - monitorId: monitor.id, - type: monitor.type, - }; + const dockerResponse = this.buildStatusResponse({ + monitor, + overrides: { + status: false, + code: this.NETWORK_ERROR, + message: "No response", + }, + }); const containers = await docker.listContainers({ all: true }); @@ -406,7 +503,6 @@ class NetworkService { method: "requestDocker", details: { url: monitor.url }, }); - dockerResponse.status = 404; dockerResponse.status = false; dockerResponse.message = `Ambiguous container match for "${monitor.url}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`; return dockerResponse; @@ -437,7 +533,7 @@ class NetworkService { } } - async requestPort(monitor: Monitor) { + private async requestPort(monitor: Monitor): Promise { try { const { url, port } = monitor; const { response, responseTime, error } = await this.timeRequest(async () => { @@ -467,14 +563,15 @@ class NetworkService { }); }); - const portResponse = { - code: 200, - status: response.success, - message: this.stringService.portSuccess, - monitorId: monitor.id, - type: monitor.type, - responseTime: responseTime, - }; + const portResponse = this.buildStatusResponse({ + monitor, + overrides: { + code: 200, + status: (response as { success?: boolean })?.success ?? false, + message: this.stringService.portSuccess, + responseTime, + }, + }); if (error) { portResponse.code = this.NETWORK_ERROR; @@ -491,17 +588,18 @@ class NetworkService { } } - async requestGame(monitor: Monitor) { + private async requestGame(monitor: Monitor): Promise { try { const { url, port, gameId } = monitor; - const gameResponse: Record = { - code: 200, - status: true, - message: "Success", - monitorId: monitor.id, - type: "game", - }; + const gameResponse = this.buildStatusResponse({ + monitor, + overrides: { + code: 200, + status: true, + message: "Success", + }, + }); const state = await this.GameDig.query({ type: gameId, @@ -532,11 +630,15 @@ class NetworkService { throw error; } } - async handleUnsupportedType(type: string) { - const err: any = new Error(`Unsupported type: ${type}`); - err.service = this.SERVICE_NAME; - err.method = "getStatus"; - throw err; + private async handleUnsupportedType(type: string): Promise { + return { + monitorId: "unknown", + teamId: "unknown", + type: "unknown", + status: false, + code: this.NETWORK_ERROR, + message: `Unsupported type: ${type}`, + }; } // Other network requests unrelated to monitoring: diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index d68ec345a..23b691e0e 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -1,7 +1,8 @@ import { IMonitorsRepository } from "@/repositories/index.js"; import MonitorStats from "../../db/models/MonitorStats.js"; import { CheckModel } from "@/db/models/index.js"; -import type { Monitor } from "@/types/index.js"; +import type { CheckErrorInfo, Monitor, MonitorStatusResponse, StatusChangeResult } from "@/types/index.js"; +import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; const SERVICE_NAME = "StatusService"; class StatusService { @@ -161,15 +162,17 @@ class StatusService { } }; - updateStatus = async (networkResponse: any) => { - const check = this.buildCheck(networkResponse); + updateMonitorStatus = async ( + statusResponse: MonitorStatusResponse + ): Promise => { + const check = this.buildCheck(statusResponse); await this.insertCheck(check); try { - const { monitorId, teamId, status, code } = networkResponse; + const { monitorId, teamId, status, code } = statusResponse; const monitor = await this.monitorsRepository.findById(monitorId, teamId); // Update running stats - this.updateRunningStats({ monitor, networkResponse }); + this.updateRunningStats({ monitor, networkResponse: statusResponse }); // If the status window size has changed, empty while (monitor.statusWindow.length > monitor.statusWindowSize) { @@ -298,7 +301,9 @@ class StatusService { } }; - buildCheck = (networkResponse: any) => { + buildCheck = ( + networkResponse: MonitorStatusResponse + ) => { const { monitorId, teamId, @@ -317,7 +322,7 @@ class StatusService { timings, } = networkResponse; - const check = { + const check: any = { metadata: { monitorId, teamId, @@ -337,7 +342,8 @@ class StatusService { }; if (type === "pagespeed") { - if (typeof payload === "undefined") { + const pageSpeedPayload = payload as PageSpeedStatusPayload | undefined; + if (!pageSpeedPayload) { this.logger.warn({ message: "Failed to build check", service: SERVICE_NAME, @@ -346,8 +352,8 @@ class StatusService { }); return undefined; } - const categories = payload?.lighthouseResult?.categories ?? {}; - const audits = payload?.lighthouseResult?.audits ?? {}; + const categories = pageSpeedPayload.lighthouseResult?.categories ?? {}; + const audits = pageSpeedPayload.lighthouseResult?.audits ?? {}; const mapAudit = (audit: any) => { if (!audit || typeof audit !== "object") { return undefined; @@ -375,14 +381,17 @@ class StatusService { } if (type === "hardware") { - const { cpu, memory, disk, host, net } = payload?.data ?? {}; - const { errors } = payload?.errors ?? []; + const hardwarePayload = payload as HardwareStatusPayload | undefined; + const { cpu, memory, disk, host, net } = hardwarePayload?.data ?? {}; + const errorsSource = Array.isArray(hardwarePayload?.errors) + ? hardwarePayload?.errors + : (hardwarePayload?.errors as { errors?: CheckErrorInfo[] } | undefined)?.errors; check.cpu = cpu ?? {}; check.memory = memory ?? {}; check.disk = disk ?? {}; check.host = host ?? {}; - check.errors = errors ?? []; - check.capture = payload?.capture ?? {}; + check.errors = errorsSource ?? []; + check.capture = hardwarePayload?.capture ?? {}; check.net = net ?? {}; } return check; diff --git a/server/src/types/check.ts b/server/src/types/check.ts index 28353497e..e3f32280f 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -1,4 +1,6 @@ import type { MonitorType } from "@/types/index.js"; +import type { Response } from "got"; +export type GotTimings = Response["timings"]; export interface CheckMetadata { monitorId: string; @@ -6,29 +8,6 @@ export interface CheckMetadata { type: MonitorType; } -export interface CheckTimingPhases { - wait: number; - dns: number; - tcp: number; - tls: number; - request: number; - firstByte: number; - download: number; - total: number; -} - -export interface CheckTimings { - start: number; - socket: number; - lookup: number; - connect: number; - secureConnect: number; - upload: number; - response: number; - end: number; - phases: CheckTimingPhases; -} - export interface CheckCpuInfo { physical_core: number; logical_core: number; @@ -107,7 +86,7 @@ export interface Check { metadata: CheckMetadata; status: boolean; responseTime: number; - timings: CheckTimings; + timings: GotTimings; statusCode: number; message: string; ack: boolean; diff --git a/server/src/types/index.ts b/server/src/types/index.ts index 81620da03..4f71788f2 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -2,3 +2,4 @@ export * from "@/types/check.js"; export * from "@/types/monitor.js"; export * from "@/types/monitorStats.js"; export * from "@/types/statusPage.js"; +export * from "@/types/network.js"; diff --git a/server/src/types/network.ts b/server/src/types/network.ts new file mode 100644 index 000000000..097cb928d --- /dev/null +++ b/server/src/types/network.ts @@ -0,0 +1,112 @@ +import type { + CheckCaptureInfo, + CheckCpuInfo, + CheckDiskInfo, + CheckErrorInfo, + CheckHostInfo, + CheckMemoryInfo, + CheckNetworkInterfaceInfo, + GotTimings, + ILighthouseAudit, + Monitor, + MonitorMatchMethod, + MonitorType, +} from "@/types/index.js"; + +export interface MonitorStatusResponse { + monitorId: string; + teamId: string; + type: MonitorType | "unknown"; + status: boolean; + code: number; + message: string; + responseTime?: number; + payload?: T | null; + timings?: GotTimings; + first_byte_took?: number; + body_read_took?: number; + dns_took?: number; + conn_took?: number; + connect_took?: number; + tls_took?: number; + jsonPath?: string; + matchMethod?: MonitorMatchMethod; + expectedValue?: string; + extracted?: unknown; +} + +export interface PingStatusPayload { + host: string; + numeric_host?: string; + alive: boolean; + time: number; + times?: number[]; + output?: string; + min?: string; + max?: string; + avg?: string; + stddev?: string; + packetLoss?: string; +} + +export type HttpStatusPayload = unknown; + +export interface PageSpeedCategoryScore { + score?: number | null; +} + +export interface PageSpeedStatusPayload { + lighthouseResult?: { + categories?: { + accessibility?: PageSpeedCategoryScore; + "best-practices"?: PageSpeedCategoryScore; + performance?: PageSpeedCategoryScore; + seo?: PageSpeedCategoryScore; + [key: string]: PageSpeedCategoryScore | undefined; + }; + audits?: Record; + }; + [key: string]: unknown; +} + +export interface HardwareStatusMetrics { + cpu?: CheckCpuInfo; + memory?: CheckMemoryInfo; + disk?: CheckDiskInfo[]; + host?: CheckHostInfo; + net?: CheckNetworkInterfaceInfo[]; +} + +export interface HardwareStatusPayload { + data?: HardwareStatusMetrics; + errors?: CheckErrorInfo[] | { errors?: CheckErrorInfo[] }; + capture?: CheckCaptureInfo; + [key: string]: unknown; +} + +export type DockerStatusPayload = Record; + +export interface PortStatusPayload { + success: boolean; +} + +export type GameStatusPayload = Record; + +export interface MonitorPayloadMap { + ping: PingStatusPayload; + http: HttpStatusPayload; + pagespeed: PageSpeedStatusPayload; + hardware: HardwareStatusPayload; + docker: DockerStatusPayload; + port: PortStatusPayload; + game: GameStatusPayload; + default: unknown; +} + +export type StatusChangeResult = { + monitor: Monitor; + statusChanged: boolean; + prevStatus: boolean | undefined; + code: number; + timestamp: number; +}; diff --git a/server/test/superSimpleQueueHelper.test.ts b/server/test/superSimpleQueueHelper.test.ts index 997eb9e83..6fba58eda 100644 --- a/server/test/superSimpleQueueHelper.test.ts +++ b/server/test/superSimpleQueueHelper.test.ts @@ -8,11 +8,14 @@ const createHelper = (overrides?: Partial { const { helper } = createHelper({ networkService: { requestStatus: jest.fn().mockResolvedValue(networkResponse) }, statusService: { - updateStatus: jest.fn().mockResolvedValue({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }), + updateMonitorStatus: jest + .fn() + .mockResolvedValue({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }), }, notificationService: { handleNotifications: jest.fn().mockResolvedValue(undefined) }, }); @@ -48,7 +53,7 @@ describe("SuperSimpleQueueHelper", () => { const monitor = { id: "m1", teamId: "team" } as Monitor; await job(monitor); expect(helper["networkService"].requestStatus).toHaveBeenCalledWith(monitor); - expect(helper["statusService"].updateStatus).toHaveBeenCalledWith(networkResponse); + expect(helper["statusService"].updateMonitorStatus).toHaveBeenCalledWith(networkResponse); expect(helper["notificationService"].handleNotifications).toHaveBeenCalledWith( expect.objectContaining({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }) ); From 1ab31d19f3f483bbfc245dd148735f244d15abd6 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Thu, 15 Jan 2026 20:44:51 -0500 Subject: [PATCH 232/400] Improve UI consistency for Logs and Incidents pages ## Changes ### Logs page - Convert server logs from monospace text to DataTable with pagination - Add table columns: Timestamp, Level, Service, Method, Message - Add colored level badges (info=green, warn=yellow, error=red, debug=blue) - Add pagination with configurable rows per page - Align diagnostics gauge cards with 16px gap and responsive widths ### Incidents page - Redesign summary cards with consistent styling - Remove shadows and use 1px border matching other sections - Reduce icon sizes to 18-24px (sidebar-style) - Reduce font sizes (13px titles, 18-32px values) - Add titles to all three summary cards - Fix "Most Affected Monitor" showing "Unknown Monitor" when no incidents - Show "N/A" for avg resolution time when no incidents - Remove cluttered "Resolutions" progress bar section - Remove horizontal dividers between statistics rows - Standardize card padding and gap to 16px ### Uptime page - Fix oversized monitor names in table (removed h6 variant) --- client/src/Components/v1/Host/index.jsx | 10 +- .../Components/ActiveIncidentsPanel/index.jsx | 46 ++--- .../IncidentsSummaryPanel/index.jsx | 2 +- .../Components/LatestIncidentsPanel/index.jsx | 40 +++- .../Components/StatisticsPanel/index.jsx | 103 ++--------- .../Components/SummaryCard/index.jsx | 24 +-- .../Diagnostics/components/gauges/index.jsx | 15 +- client/src/Pages/Logs/Logs/index.jsx | 172 +++++++++++++----- client/src/locales/en.json | 14 +- 9 files changed, 223 insertions(+), 203 deletions(-) diff --git a/client/src/Components/v1/Host/index.jsx b/client/src/Components/v1/Host/index.jsx index 935224994..6670722c1 100644 --- a/client/src/Components/v1/Host/index.jsx +++ b/client/src/Components/v1/Host/index.jsx @@ -23,12 +23,12 @@ const Host = ({ url, title, percentageColor, percentage, showURL, status }) => { direction="row" position="relative" alignItems="center" - gap={theme.spacing(5)} + gap={theme.spacing(4)} > {title} @@ -38,9 +38,9 @@ const Host = ({ url, title, percentageColor, percentage, showURL, status }) => { <> diff --git a/client/src/Pages/Incidents2/Components/ActiveIncidentsPanel/index.jsx b/client/src/Pages/Incidents2/Components/ActiveIncidentsPanel/index.jsx index b6024ce53..8ed80d037 100644 --- a/client/src/Pages/Incidents2/Components/ActiveIncidentsPanel/index.jsx +++ b/client/src/Pages/Incidents2/Components/ActiveIncidentsPanel/index.jsx @@ -43,39 +43,36 @@ const ActiveIncidentsPanel = ({ totalCount = 0, isLoading = false, error = null if (!totalCount || totalCount === 0) { return ( - + {t("incidentsPage.allSystemsAreOperational")} @@ -86,21 +83,20 @@ const ActiveIncidentsPanel = ({ totalCount = 0, isLoading = false, error = null } return ( - + {totalCount} - - {t("incidentsPage.incidentsActivePanelTitle")} - ); diff --git a/client/src/Pages/Incidents2/Components/IncidentsSummaryPanel/index.jsx b/client/src/Pages/Incidents2/Components/IncidentsSummaryPanel/index.jsx index 43071fa03..33ba21cd1 100644 --- a/client/src/Pages/Incidents2/Components/IncidentsSummaryPanel/index.jsx +++ b/client/src/Pages/Incidents2/Components/IncidentsSummaryPanel/index.jsx @@ -53,7 +53,7 @@ const IncidentsSummaryPanel = ({ updateTrigger }) => { <> {!incidents || incidents.length === 0 ? ( - + + + + {t("incidentsPage.incidentsLatestPanelEmpty")} - + ) : ( {incidents.map((incident, index) => ( diff --git a/client/src/Pages/Incidents2/Components/StatisticsPanel/index.jsx b/client/src/Pages/Incidents2/Components/StatisticsPanel/index.jsx index ede9c619a..c8559d7a2 100644 --- a/client/src/Pages/Incidents2/Components/StatisticsPanel/index.jsx +++ b/client/src/Pages/Incidents2/Components/StatisticsPanel/index.jsx @@ -3,10 +3,8 @@ import { Box, Stack, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; import PanelSkeleton from "../IncidentsSummaryPanel/skeleton.jsx"; import { useTranslation } from "react-i18next"; -import { Divider } from "@mui/material"; import Clock from "@/assets/icons/maintenance.svg?react"; import Incidents from "@/assets/icons/incidents.svg?react"; -import ResolutionItem from "@/assets/icons/interval-check.svg?react"; import NotificationIcon from "@/assets/icons/notifications.svg?react"; import SummaryCard from "../SummaryCard/index.jsx"; @@ -43,10 +41,15 @@ const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => { const iconWrapperStyle = { display: "flex", justifyContent: "center", - mx: theme.spacing(3), + mx: theme.spacing(2), color: theme.palette.primary.contrastTextTertiary, + "& svg": { + width: 18, + height: 18, + }, "& svg path": { stroke: "currentColor", + strokeWidth: 1.5, }, }; if (isLoading) { @@ -76,6 +79,13 @@ const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => { ); } + const getMostAffectedMonitor = () => { + if (!summary.total || summary.total === 0) { + return t("incidentsPage.none"); + } + return summary.topMonitor?.monitorName || t("incidentsPage.none"); + }; + return ( @@ -99,11 +109,10 @@ const StatisticsPanel = ({ isLoading = false, error = null, summary = {} }) => { lineHeight: 1.2, }} > - {t("incidentsPage.totalIncidents")} : {summary.total || 0} + {t("incidentsPage.totalIncidents")}: {summary.total || 0} - { lineHeight: 1.2, }} > - {t("incidentsPage.mostAffectedMonitor")} :{" "} - {summary.topMonitor?.monitorName || t("incidentsPage.unknownMonitor")} + {t("incidentsPage.mostAffectedMonitor")}: {getMostAffectedMonitor()} - { - {t("incidentsPage.avgResolutionTime")} :{" "} - {summary.avgResolutionTimeHours || 0} {t("incidentsPage.hours")} + {t("incidentsPage.avgResolutionTime")}:{" "} + {summary.total > 0 ? `${summary.avgResolutionTimeHours || 0} ${t("incidentsPage.hours")}` : "N/A"} - - - - - - - - - - - {t("incidentsPage.resolutions")}: {totalResolutions} - - - - - - - - - {summary.totalAutomaticResolutions > 0 && ( - - {t("incidentsPage.automatic")} ({summary.totalAutomaticResolutions}) - - )} - {summary.totalManualResolutions > 0 && ( - - {t("incidentsPage.manual")} ({summary.totalManualResolutions}) - - )} - - ); diff --git a/client/src/Pages/Incidents2/Components/SummaryCard/index.jsx b/client/src/Pages/Incidents2/Components/SummaryCard/index.jsx index cb895dc50..3a8589dc1 100644 --- a/client/src/Pages/Incidents2/Components/SummaryCard/index.jsx +++ b/client/src/Pages/Incidents2/Components/SummaryCard/index.jsx @@ -8,17 +8,14 @@ const SummaryCard = ({ children, isHighPriority = false, sx = {}, title = null } {title} @@ -49,9 +45,9 @@ const SummaryCard = ({ children, isHighPriority = false, sx = {}, title = null } )} {children} diff --git a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx index b38837cf2..be6dc99aa 100644 --- a/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx +++ b/client/src/Pages/Logs/Diagnostics/components/gauges/index.jsx @@ -12,13 +12,16 @@ import { Box } from "@mui/material"; const BaseContainer = ({children}) => { const theme = useTheme() return( - {children} @@ -99,7 +102,7 @@ const Gauges = ({ diagnostics, isLoading }) => { return ( { +const LevelBadge = ({ level }) => { + const theme = useTheme(); + const levelColors = { info: theme.palette.success.main, warn: theme.palette.warning.main, @@ -17,31 +20,38 @@ const formatLog = (theme, log, idx) => { debug: theme.palette.accent.main, }; - const color = levelColors[log.level] || theme.palette.primary.contrastText; + const color = levelColors[level] || theme.palette.primary.contrastText; return ( - - [{log.timestamp}]{" "} - {log.level.toUpperCase()} - {": "} - {`(${log.service})`} - {`(${log.method})`} - {": "} - {log.message} -
-
+ + {level} + ); }; -const Logs = () => { - // Local state - const [logLevel, setLogLevel] = useState("all"); +const formatTimestamp = (timestamp) => { + if (!timestamp) return "-"; + const date = new Date(timestamp); + return date.toLocaleString(); +}; + +const Logs = () => { + const [logLevel, setLogLevel] = useState("all"); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(15); - // Hooks const theme = useTheme(); const { t } = useTranslation(); const [logs, isLoading, error] = useFetchLogs(); - // Setup + const LOG_LEVELS = [ { _id: "all", name: t("logsPage.logLevelSelect.values.all") }, { _id: "info", name: t("logsPage.logLevelSelect.values.info") }, @@ -49,52 +59,116 @@ const Logs = () => { { _id: "error", name: t("logsPage.logLevelSelect.values.error") }, { _id: "debug", name: t("logsPage.logLevelSelect.values.debug") }, ]; + + const headers = [ + { + id: "timestamp", + content: t("logsPage.table.timestamp"), + render: (row) => ( + + {formatTimestamp(row.timestamp)} + + ), + }, + { + id: "level", + content: t("logsPage.table.level"), + render: (row) => , + }, + { + id: "service", + content: t("logsPage.table.service"), + render: (row) => ( + {row.service || "-"} + ), + }, + { + id: "method", + content: t("logsPage.table.method"), + render: (row) => ( + {row.method || "-"} + ), + }, + { + id: "message", + content: t("logsPage.table.message"), + render: (row) => ( + + {row.message || "-"} + + ), + }, + ]; + + const filteredLogs = logs + ?.filter((log) => { + if (logLevel === "all") return true; + return log.level === logLevel; + }) + .reverse() + .map((log, idx) => ({ ...log, id: idx })); + + const paginatedLogs = filteredLogs?.slice( + page * rowsPerPage, + page * rowsPerPage + rowsPerPage + ); + + const handleChangePage = (event, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + + const handleLogLevelChange = (e) => { + setLogLevel(e.target.value); + setPage(0); + }; + return ( {t("logsPage.logLevelSelect.title")} ( - )} displayEmpty diff --git a/client/src/Components/v1/Icon/iconMap.js b/client/src/Components/v1/Icon/iconMap.js new file mode 100644 index 000000000..58da7304b --- /dev/null +++ b/client/src/Components/v1/Icon/iconMap.js @@ -0,0 +1,120 @@ +/** + * Maps legacy icon names to Lucide icon names + * Use: import { iconMap } from './iconMap'; LucideIcons[iconMap['monitors']] + */ +export const iconMap = { + // Navigation/Sidebar icons + monitors: "Globe", + incidents: "AlertTriangle", + integrations: "Link", + "page-speed": "Gauge", + settings: "Settings", + notifications: "Bell", + maintenance: "Wrench", + "status-pages": "Wifi", + docs: "FileText", + discussions: "MessageCircle", + support: "HelpCircle", + changeLog: "Code", + logs: "Database", + + // User icons + user: "User", + "user-two": "Users", + "user-edit": "UserPen", + groups: "Users", + lock: "Lock", + key: "Key", + logout: "LogOut", + + // Action icons + edit: "Pencil", + "trash-bin": "Trash2", + search: "Search", + "close-icon": "X", + check: "Check", + "check-icon": "CheckCircle", + "check-outlined": "CheckCircle", + + // Arrow icons + "left-arrow": "ChevronLeft", + "right-arrow": "ChevronRight", + "up-arrow": "ChevronUp", + "down-arrow": "ChevronDown", + "left-arrow-double": "ChevronsLeft", + "right-arrow-double": "ChevronsRight", + "left-arrow-long": "ArrowLeft", + "top-right-arrow": "ArrowUpRight", + "open-in-new-page": "ExternalLink", + "selector-vertical": "ChevronsUpDown", + + // Status/Alert icons + "alert-icon": "Bell", + "warning-icon": "AlertCircle", + "history-icon": "History", + + // Data/Chart icons + dashboard: "LayoutGrid", + "monitor-graph-line": "TrendingUp", + "response-time-icon": "TrendingUp", + "uptime-icon": "Activity", + "speedometer-icon": "Gauge", + "spedometer-icon": "Gauge", + "ruler-icon": "Ruler", + "performance-report": "Layers", + "average-response-icon": "BarChart3", + "distributed-uptime": "Network", + + // Misc icons + calendar: "Calendar", + "calendar-check": "CalendarCheck", + "clock-snooze": "Clock", + mail: "Mail", + email: "Mail", + folder: "Folder", + "dots-vertical": "MoreVertical", + "pause-icon": "PauseCircle", + "resume-icon": "PlayCircle", + "checkbox-filled": "CheckSquare", + "checkbox-outline": "Square", + "radio-checked": "Circle", + "cpu-chip": "Cpu", + certificate: "Award", + "interval-check": "RefreshCw", +}; + +/** + * MUI icon to Lucide mapping + */ +export const muiToLucideMap = { + AddCircleOutline: "PlusCircle", + ArrowDownwardRounded: "ArrowDown", + ArrowUpwardRounded: "ArrowUp", + ArrowDropDown: "ChevronDown", + ArrowOutward: "ExternalLink", + CheckCircle: "CheckCircle2", + Clear: "X", + Close: "X", + CloseRounded: "X", + CloudUpload: "Upload", + Delete: "Trash2", + DeleteOutlineRounded: "Trash2", + Email: "Mail", + ErrorOutline: "AlertTriangle", + ErrorOutlineOutlined: "AlertTriangle", + Image: "ImageIcon", + InfoOutlined: "Info", + KeyboardArrowDown: "ChevronDown", + Menu: "Menu", + PauseCircleOutline: "PauseCircle", + PauseOutlined: "Pause", + PlayArrowOutlined: "Play", + PlayCircleOutlineRounded: "PlayCircle", + RadioButtonChecked: "Circle", + ReorderRounded: "GripVertical", + SettingsOutlined: "Settings", + Share: "Share2", + Visibility: "Eye", + VisibilityOff: "EyeOff", + WarningAmberOutlined: "AlertTriangle", +}; diff --git a/client/src/Components/v1/Icon/index.jsx b/client/src/Components/v1/Icon/index.jsx new file mode 100644 index 000000000..4fa82a686 --- /dev/null +++ b/client/src/Components/v1/Icon/index.jsx @@ -0,0 +1,63 @@ +import { useTheme } from "@mui/material"; +import * as LucideIcons from "lucide-react"; +import PropTypes from "prop-types"; + +/** + * Theme-aware icon component wrapping Lucide icons + * + * @param {string} name - Lucide icon name (e.g., "Bell", "Check", "ArrowLeft") + * @param {number} size - Icon size in pixels (default: 20) + * @param {string} color - Direct color value OR theme path like "primary.contrastText" + * @param {number} strokeWidth - Stroke width (default: 1.5) + * @param {string} fill - Fill color (default: "none") + */ +const Icon = ({ name, size = 20, color, strokeWidth = 1.5, fill = "none", ...props }) => { + const theme = useTheme(); + + // Resolve color from theme path or use direct value + const resolveColor = (colorValue) => { + if (!colorValue) { + return theme.palette.primary.contrastTextTertiary; // Default icon color + } + + // If it's a theme path like "primary.contrastText" + if (typeof colorValue === "string" && colorValue.includes(".")) { + const parts = colorValue.split("."); + let resolved = theme.palette; + for (const part of parts) { + resolved = resolved?.[part]; + } + return resolved || colorValue; + } + + // Direct color value + return colorValue; + }; + + const LucideIcon = LucideIcons[name]; + + if (!LucideIcon) { + console.warn(`Icon "${name}" not found in Lucide icons`); + return null; + } + + return ( + + ); +}; + +Icon.propTypes = { + name: PropTypes.string.isRequired, + size: PropTypes.number, + color: PropTypes.string, + strokeWidth: PropTypes.number, + fill: PropTypes.string, +}; + +export default Icon; diff --git a/client/src/Components/v1/Inputs/Checkbox/index.jsx b/client/src/Components/v1/Inputs/Checkbox/index.jsx index 64f3cc3db..caa6830e7 100644 --- a/client/src/Components/v1/Inputs/Checkbox/index.jsx +++ b/client/src/Components/v1/Inputs/Checkbox/index.jsx @@ -1,8 +1,7 @@ import PropTypes from "prop-types"; import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material"; import { useTheme } from "@emotion/react"; -import CheckboxOutline from "../../../../assets/icons/checkbox-outline.svg?react"; -import CheckboxFilled from "../../../../assets/icons/checkbox-filled.svg?react"; +import Icon from "../../Icon"; /** * Checkbox Component @@ -65,8 +64,8 @@ const Checkbox = ({ name={name} value={value} onChange={onChange} - icon={} - checkedIcon={} + icon={} + checkedIcon={} inputProps={{ "aria-label": "controlled checkbox", id: id, diff --git a/client/src/Components/v1/Inputs/ImageUpload/index.jsx b/client/src/Components/v1/Inputs/ImageUpload/index.jsx index e7415bf8f..5b2be991a 100644 --- a/client/src/Components/v1/Inputs/ImageUpload/index.jsx +++ b/client/src/Components/v1/Inputs/ImageUpload/index.jsx @@ -1,11 +1,10 @@ // Components import { Box, Stack, Typography } from "@mui/material"; -import CloudUploadIcon from "@mui/icons-material/CloudUpload"; +import Icon from "../../Icon"; import Image from "../../Image/index.jsx"; import TextField from "@mui/material/TextField"; import IconButton from "@mui/material/IconButton"; import ProgressUpload from "../../ProgressBars/index.jsx"; -import ImageIcon from "@mui/icons-material/Image"; // Utils import PropTypes from "prop-types"; @@ -176,7 +175,7 @@ const ImageUpload = ({ boxShadow: theme.shape.boxShadow, }} > - + {(localError || progress.isLoading || progress.value !== 0) && ( } + icon={} label={file?.name || "Upload failed"} size={file?.size} progress={progress.value} diff --git a/client/src/Components/v1/Inputs/Radio/index.jsx b/client/src/Components/v1/Inputs/Radio/index.jsx index 575c1af74..79a9e64f3 100644 --- a/client/src/Components/v1/Inputs/Radio/index.jsx +++ b/client/src/Components/v1/Inputs/Radio/index.jsx @@ -1,7 +1,7 @@ import PropTypes from "prop-types"; import { FormControlLabel, Radio as MUIRadio, Typography } from "@mui/material"; import { useTheme } from "@emotion/react"; -import RadioChecked from "../../../../assets/icons/radio-checked.svg?react"; +import Icon from "../../Icon"; import "./index.css"; /** @@ -47,7 +47,7 @@ const Radio = ({ } + checkedIcon={} sx={{ color: "transparent", width: 16, diff --git a/client/src/Components/v1/Inputs/Search/index.jsx b/client/src/Components/v1/Inputs/Search/index.jsx index 8138ee6f4..4371ef971 100644 --- a/client/src/Components/v1/Inputs/Search/index.jsx +++ b/client/src/Components/v1/Inputs/Search/index.jsx @@ -8,7 +8,7 @@ import { Checkbox, } from "@mui/material"; import { useTheme } from "@emotion/react"; -import SearchIcon from "../../../../assets/icons/search.svg?react"; +import Icon from "../../Icon"; import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; import FieldWrapper from "../FieldWrapper/index.jsx"; @@ -31,20 +31,13 @@ import FieldWrapper from "../FieldWrapper/index.jsx"; const SearchAdornment = () => { const theme = useTheme(); return ( - - + + ); }; diff --git a/client/src/Components/v1/Inputs/Select/index.jsx b/client/src/Components/v1/Inputs/Select/index.jsx index f952b6818..b292d6896 100644 --- a/client/src/Components/v1/Inputs/Select/index.jsx +++ b/client/src/Components/v1/Inputs/Select/index.jsx @@ -1,7 +1,7 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { MenuItem, Select as MuiSelect, Stack, Typography } from "@mui/material"; -import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown"; +import Icon from "../../Icon"; import FieldWrapper from "../FieldWrapper/index.jsx"; import "./index.css"; @@ -98,7 +98,7 @@ const Select = ({ error={error} name={name} inputProps={{ id: id }} - IconComponent={KeyboardArrowDownIcon} + IconComponent={(props) => } MenuProps={{ disableScrollLock: true }} sx={{ fontSize: 13, diff --git a/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx b/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx index 4e4bbd2af..8df4ee3b1 100644 --- a/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx +++ b/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx @@ -1,10 +1,7 @@ import { Stack, Typography, InputAdornment, IconButton } from "@mui/material"; import { useTheme } from "@mui/material/styles"; import PropTypes from "prop-types"; -import VisibilityOff from "@mui/icons-material/VisibilityOff"; -import Visibility from "@mui/icons-material/Visibility"; -import ReorderRoundedIcon from "@mui/icons-material/ReorderRounded"; -import DeleteIcon from "../../../../../assets/icons/trash-bin.svg?react"; +import Icon from "../../../Icon"; export const HttpAdornment = ({ https }) => { const theme = useTheme(); @@ -56,7 +53,11 @@ export const PasswordEndAdornment = ({ fieldType, setFieldType }) => { }, }} > - {fieldType === "password" ? : } + {fieldType === "password" ? ( + + ) : ( + + )} ); diff --git a/client/src/Components/v1/MonitorActions/index.jsx b/client/src/Components/v1/MonitorActions/index.jsx index 624d34b9a..d5bab53ba 100644 --- a/client/src/Components/v1/MonitorActions/index.jsx +++ b/client/src/Components/v1/MonitorActions/index.jsx @@ -1,7 +1,7 @@ import * as React from "react"; import Button from "@mui/material/Button"; import ButtonGroup from "@mui/material/ButtonGroup"; -import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown"; +import Icon from "../Icon"; import ClickAwayListener from "@mui/material/ClickAwayListener"; import Grow from "@mui/material/Grow"; import Paper from "@mui/material/Paper"; @@ -70,7 +70,7 @@ const MonitorActions = ({ isLoading }) => { aria-haspopup="menu" onClick={handleToggle} > - + } + startIcon={} disabled={isTestNotificationsDisabled} onClick={() => { testAllNotifications({ monitorId: monitor?.id }); @@ -103,7 +100,7 @@ const MonitorDetailsControlHeader = ({ color="secondary" loading={isPausing} startIcon={ - monitor?.isActive ? : + monitor?.isActive ? : } onClick={() => { pauseMonitor({ @@ -119,7 +116,7 @@ const MonitorDetailsControlHeader = ({ @@ -111,7 +110,7 @@ const ControlsHeader = ({ statusPage, isPublic, url, type = "uptime" }) => { }} > {t("publicLink")} - + )}
diff --git a/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx b/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx index 1898ff3d7..44cfbaca3 100644 --- a/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx +++ b/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx @@ -1,7 +1,6 @@ // Components import { Stack, Typography } from "@mui/material"; -import CheckCircleIcon from "@mui/icons-material/CheckCircle"; -import ErrorOutlineIcon from "@mui/icons-material/ErrorOutline"; +import Icon from "@/Components/v1/Icon"; // Utils import { useTheme } from "@mui/material/styles"; @@ -10,18 +9,14 @@ import PropTypes from "prop-types"; const getMonitorStatus = (monitors, theme) => { const monitorsStatus = { icon: ( - + ), }; if (monitors.every((monitor) => monitor.status === true)) { monitorsStatus.msg = "All systems operational"; monitorsStatus.color = theme.palette.success.lowContrast; monitorsStatus.icon = ( - + ); } diff --git a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx index 1afe25409..57f7235e6 100644 --- a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx +++ b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx @@ -2,7 +2,7 @@ import DataTable from "@/Components/v1/Table/index.jsx"; import { useTheme } from "@emotion/react"; import { useNavigate } from "react-router-dom"; import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import ArrowOutwardIcon from "@mui/icons-material/ArrowOutward"; +import Icon from "@/Components/v1/Icon"; import { Stack, Typography } from "@mui/material"; import { useTranslation } from "react-i18next"; const StatusPagesTable = ({ data }) => { @@ -48,7 +48,7 @@ const StatusPagesTable = ({ data }) => { }} > {content} - {row.isPublished && } + {row.isPublished && }
); }, diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index 2c7821571..70be3bc00 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -28,10 +28,7 @@ import { useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { monitorValidation } from "../../../Validation/validation.js"; import { createToast } from "../../../Utils/toastUtils.jsx"; -import { - PauseOutlined as PauseOutlinedIcon, - PlayArrowOutlined as PlayArrowOutlinedIcon, -} from "@mui/icons-material"; +import Icon from "@/Components/v1/Icon"; import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js"; import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications.js"; import { useParams } from "react-router-dom"; @@ -429,7 +426,7 @@ const UptimeCreate = ({ isClone = false }) => { color="secondary" loading={isBusy} startIcon={ - monitor?.isActive ? : + monitor?.isActive ? : } onClick={handlePause} > diff --git a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx b/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx index 582c7891f..6449946f3 100644 --- a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx +++ b/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx @@ -1,9 +1,7 @@ // Components import { Stack, Typography, Box } from "@mui/material"; import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; -import UptimeIcon from "@/assets/icons/uptime-icon.svg?react"; -import IncidentsIcon from "@/assets/icons/incidents.svg?react"; -import AverageResponseIcon from "@/assets/icons/average-response-icon.svg?react"; +import Icon from "@/Components/v1/Icon"; import UpBarChart from "../Charts/UpBarChart.jsx"; import DownBarChart from "../Charts/DownBarChart.jsx"; import ResponseGaugeChart from "../Charts/ResponseGaugeChart.jsx"; @@ -38,7 +36,7 @@ const ChartBoxes = ({ gap={theme.spacing(8)} > } + icon={} header="Uptime" isEmpty={monitorData?.groupedUpChecks?.length === 0} > @@ -89,7 +87,7 @@ const ChartBoxes = ({ /> } + icon={} header="Incidents" noDataMessage={noIncidentsMessage} isEmpty={monitorData?.groupedDownChecks?.length === 0} @@ -123,7 +121,7 @@ const ChartBoxes = ({ /> } + icon={} header="Average Response Time" > diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx index 861b600d4..365324b4d 100644 --- a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx +++ b/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx @@ -1,6 +1,6 @@ import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; import MonitorDetailsAreaChart from "@/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx"; -import ResponseTimeIcon from "@/assets/icons/response-time-icon.svg?react"; +import Icon from "@/Components/v1/Icon"; import SkeletonLayout from "./ResponseTimeChartSkeleton.jsx"; import PropTypes from "prop-types"; @@ -11,7 +11,7 @@ const ResponseTImeChart = ({ isLoading = false, groupedChecks = [], dateRange }) return ( } + icon={} header="Response Times" > } + icon={} header="Response Times" height="100%" > diff --git a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx b/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx index b9a6824c1..003617046 100644 --- a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx @@ -3,7 +3,7 @@ import PropTypes from "prop-types"; import FilterHeader from "@/Components/v1/FilterHeader/index.jsx"; import { useMemo } from "react"; import { Box, Button } from "@mui/material"; -import ClearIcon from "@mui/icons-material/Clear"; +import Icon from "@/Components/v1/Icon"; import { useTranslation } from "react-i18next"; /** @@ -131,7 +131,7 @@ const Filter = ({ color={theme.palette.primary.contrastText} onClick={handleReset} variant="contained" - endIcon={} + endIcon={} sx={{ visibility: isFilterActive ? "visible" : "hidden", }} diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx index c9015816a..ee9a5d037 100644 --- a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx @@ -1,9 +1,8 @@ import PropTypes from "prop-types"; import { useTheme } from "@emotion/react"; import { Box, Stack, Typography } from "@mui/material"; -import Arrow from "@/assets/icons/top-right-arrow.svg?react"; +import Icon from "@/Components/v1/Icon"; import Background from "@/assets/Images/background-grid.svg?react"; -import ClockSnooze from "@/assets/icons/clock-snooze.svg?react"; const StatusBox = ({ title, value, status }) => { const theme = useTheme(); @@ -20,21 +19,21 @@ const StatusBox = ({ title, value, status }) => { color = theme.palette.success.lowContrast; icon = ( - + ); } else if (status === "down") { color = theme.palette.error.lowContrast; icon = ( - + ); } else if (status === "paused") { color = theme.palette.warning.lowContrast; icon = ( - + ); } diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx b/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx index fe220251d..43a467405 100644 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx @@ -1,8 +1,7 @@ // Components import { Box, Stack } from "@mui/material"; import DataTable from "@/Components/v1/Table/index.jsx"; -import ArrowDownwardRoundedIcon from "@mui/icons-material/ArrowDownwardRounded"; -import ArrowUpwardRoundedIcon from "@mui/icons-material/ArrowUpwardRounded"; +import Icon from "@/Components/v1/Icon"; import Host from "@/Components/v1/Host/index.jsx"; import { StatusLabel } from "@/Components/v1/Label/index.jsx"; import BarChart from "@/Components/v1/Charts/BarChart/index.jsx"; @@ -92,9 +91,9 @@ const UptimeDataTable = ({ }} > {sort?.order === "asc" ? ( - + ) : ( - + )} @@ -128,9 +127,9 @@ const UptimeDataTable = ({ }} > {sort?.order === "asc" ? ( - + ) : ( - + )} diff --git a/client/src/assets/icons/alert-icon.svg b/client/src/assets/icons/alert-icon.svg deleted file mode 100644 index 1dc7fc2b7..000000000 --- a/client/src/assets/icons/alert-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/average-response-icon.svg b/client/src/assets/icons/average-response-icon.svg deleted file mode 100644 index 572e2002f..000000000 --- a/client/src/assets/icons/average-response-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/bwu-icon.svg b/client/src/assets/icons/bwu-icon.svg deleted file mode 100644 index 80ab50ad9..000000000 --- a/client/src/assets/icons/bwu-icon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/calendar-check.svg b/client/src/assets/icons/calendar-check.svg deleted file mode 100644 index 07d96dad9..000000000 --- a/client/src/assets/icons/calendar-check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/calendar.svg b/client/src/assets/icons/calendar.svg deleted file mode 100644 index be023a4de..000000000 --- a/client/src/assets/icons/calendar.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/certificate.svg b/client/src/assets/icons/certificate.svg deleted file mode 100644 index 2bc6c7e74..000000000 --- a/client/src/assets/icons/certificate.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/changeLog.svg b/client/src/assets/icons/changeLog.svg deleted file mode 100644 index d12f6c10e..000000000 --- a/client/src/assets/icons/changeLog.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/check-icon.svg b/client/src/assets/icons/check-icon.svg deleted file mode 100644 index 9a2cfac5a..000000000 --- a/client/src/assets/icons/check-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/check-outlined.svg b/client/src/assets/icons/check-outlined.svg deleted file mode 100644 index 1e779d547..000000000 --- a/client/src/assets/icons/check-outlined.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/check.svg b/client/src/assets/icons/check.svg deleted file mode 100644 index 357378f40..000000000 --- a/client/src/assets/icons/check.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/checkbox-filled.svg b/client/src/assets/icons/checkbox-filled.svg deleted file mode 100644 index 8a5be81f0..000000000 --- a/client/src/assets/icons/checkbox-filled.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/checkbox-green.svg b/client/src/assets/icons/checkbox-green.svg deleted file mode 100644 index dd5676298..000000000 --- a/client/src/assets/icons/checkbox-green.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/client/src/assets/icons/checkbox-outline.svg b/client/src/assets/icons/checkbox-outline.svg deleted file mode 100644 index bf5a7218c..000000000 --- a/client/src/assets/icons/checkbox-outline.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/checkbox-red.svg b/client/src/assets/icons/checkbox-red.svg deleted file mode 100644 index 3c43a2042..000000000 --- a/client/src/assets/icons/checkbox-red.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/client/src/assets/icons/clock-snooze.svg b/client/src/assets/icons/clock-snooze.svg deleted file mode 100644 index 708e7ba91..000000000 --- a/client/src/assets/icons/clock-snooze.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/close-icon.svg b/client/src/assets/icons/close-icon.svg deleted file mode 100644 index e1b6b690a..000000000 --- a/client/src/assets/icons/close-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/cpu-chip.svg b/client/src/assets/icons/cpu-chip.svg deleted file mode 100644 index 5645840b1..000000000 --- a/client/src/assets/icons/cpu-chip.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/dashboard.svg b/client/src/assets/icons/dashboard.svg deleted file mode 100644 index 8958e90ed..000000000 --- a/client/src/assets/icons/dashboard.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/discussions.svg b/client/src/assets/icons/discussions.svg deleted file mode 100644 index 293cd51f2..000000000 --- a/client/src/assets/icons/discussions.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/client/src/assets/icons/distributed-uptime.svg b/client/src/assets/icons/distributed-uptime.svg deleted file mode 100644 index e612081e4..000000000 --- a/client/src/assets/icons/distributed-uptime.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/docs.svg b/client/src/assets/icons/docs.svg deleted file mode 100644 index d36458de2..000000000 --- a/client/src/assets/icons/docs.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/dots-vertical.svg b/client/src/assets/icons/dots-vertical.svg deleted file mode 100644 index e720066c4..000000000 --- a/client/src/assets/icons/dots-vertical.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/client/src/assets/icons/down-arrow.svg b/client/src/assets/icons/down-arrow.svg deleted file mode 100644 index 59b4b6bc5..000000000 --- a/client/src/assets/icons/down-arrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/edit.svg b/client/src/assets/icons/edit.svg deleted file mode 100644 index be2c78408..000000000 --- a/client/src/assets/icons/edit.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/email.svg b/client/src/assets/icons/email.svg deleted file mode 100644 index 01f55e961..000000000 --- a/client/src/assets/icons/email.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/folder.svg b/client/src/assets/icons/folder.svg deleted file mode 100644 index 15941e5b5..000000000 --- a/client/src/assets/icons/folder.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/client/src/assets/icons/groups.svg b/client/src/assets/icons/groups.svg deleted file mode 100644 index fa2cae03c..000000000 --- a/client/src/assets/icons/groups.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/client/src/assets/icons/history-icon.svg b/client/src/assets/icons/history-icon.svg deleted file mode 100644 index 7343455ec..000000000 --- a/client/src/assets/icons/history-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/incidents.svg b/client/src/assets/icons/incidents.svg deleted file mode 100644 index ffe04d09f..000000000 --- a/client/src/assets/icons/incidents.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/integrations.svg b/client/src/assets/icons/integrations.svg deleted file mode 100644 index 50e817f00..000000000 --- a/client/src/assets/icons/integrations.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/interval-check.svg b/client/src/assets/icons/interval-check.svg deleted file mode 100644 index 923475c8b..000000000 --- a/client/src/assets/icons/interval-check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/jupiter_ag_logo.svg b/client/src/assets/icons/jupiter_ag_logo.svg deleted file mode 100644 index b344961c8..000000000 --- a/client/src/assets/icons/jupiter_ag_logo.svg +++ /dev/null @@ -1,52 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/icons/jupiter_logo_banner_dark.svg b/client/src/assets/icons/jupiter_logo_banner_dark.svg deleted file mode 100644 index 3d3e23b14..000000000 --- a/client/src/assets/icons/jupiter_logo_banner_dark.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/icons/jupiter_logo_banner_light.svg b/client/src/assets/icons/jupiter_logo_banner_light.svg deleted file mode 100644 index 4744e0bcb..000000000 --- a/client/src/assets/icons/jupiter_logo_banner_light.svg +++ /dev/null @@ -1,35 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/icons/key.svg b/client/src/assets/icons/key.svg deleted file mode 100644 index 5db8d5f16..000000000 --- a/client/src/assets/icons/key.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/left-arrow-double.svg b/client/src/assets/icons/left-arrow-double.svg deleted file mode 100644 index 4cb2a5186..000000000 --- a/client/src/assets/icons/left-arrow-double.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/left-arrow-long.svg b/client/src/assets/icons/left-arrow-long.svg deleted file mode 100644 index 56a774730..000000000 --- a/client/src/assets/icons/left-arrow-long.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/left-arrow.svg b/client/src/assets/icons/left-arrow.svg deleted file mode 100644 index 649d1681c..000000000 --- a/client/src/assets/icons/left-arrow.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/lock.svg b/client/src/assets/icons/lock.svg deleted file mode 100644 index a87535d54..000000000 --- a/client/src/assets/icons/lock.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/logout.svg b/client/src/assets/icons/logout.svg deleted file mode 100644 index 3f3b69c49..000000000 --- a/client/src/assets/icons/logout.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/logs.svg b/client/src/assets/icons/logs.svg deleted file mode 100644 index 556c3ba33..000000000 --- a/client/src/assets/icons/logs.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/mail.svg b/client/src/assets/icons/mail.svg deleted file mode 100644 index 75c22b111..000000000 --- a/client/src/assets/icons/mail.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/maintenance.svg b/client/src/assets/icons/maintenance.svg deleted file mode 100644 index 9486f6086..000000000 --- a/client/src/assets/icons/maintenance.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/monitor-graph-line.svg b/client/src/assets/icons/monitor-graph-line.svg deleted file mode 100644 index 86049eb9e..000000000 --- a/client/src/assets/icons/monitor-graph-line.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/monitors.svg b/client/src/assets/icons/monitors.svg deleted file mode 100644 index f40f36433..000000000 --- a/client/src/assets/icons/monitors.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/notifications.svg b/client/src/assets/icons/notifications.svg deleted file mode 100644 index c09ac5746..000000000 --- a/client/src/assets/icons/notifications.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/open-in-new-page.svg b/client/src/assets/icons/open-in-new-page.svg deleted file mode 100644 index 70599f390..000000000 --- a/client/src/assets/icons/open-in-new-page.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/page-speed.svg b/client/src/assets/icons/page-speed.svg deleted file mode 100644 index 30a1c8a53..000000000 --- a/client/src/assets/icons/page-speed.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/pause-icon.svg b/client/src/assets/icons/pause-icon.svg deleted file mode 100644 index c73abfc1d..000000000 --- a/client/src/assets/icons/pause-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/performance-report.svg b/client/src/assets/icons/performance-report.svg deleted file mode 100644 index 8b7073ac3..000000000 --- a/client/src/assets/icons/performance-report.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/radio-checked.svg b/client/src/assets/icons/radio-checked.svg deleted file mode 100644 index 1c584b79a..000000000 --- a/client/src/assets/icons/radio-checked.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/response-time-icon.svg b/client/src/assets/icons/response-time-icon.svg deleted file mode 100644 index db5b2af38..000000000 --- a/client/src/assets/icons/response-time-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/resume-icon.svg b/client/src/assets/icons/resume-icon.svg deleted file mode 100644 index 1da787081..000000000 --- a/client/src/assets/icons/resume-icon.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/right-arrow-double.svg b/client/src/assets/icons/right-arrow-double.svg deleted file mode 100644 index 6ad4882ad..000000000 --- a/client/src/assets/icons/right-arrow-double.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/right-arrow.svg b/client/src/assets/icons/right-arrow.svg deleted file mode 100644 index 77d022798..000000000 --- a/client/src/assets/icons/right-arrow.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - \ No newline at end of file diff --git a/client/src/assets/icons/ruler-icon.svg b/client/src/assets/icons/ruler-icon.svg deleted file mode 100644 index 7266d2876..000000000 --- a/client/src/assets/icons/ruler-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/search.svg b/client/src/assets/icons/search.svg deleted file mode 100644 index a1bc74224..000000000 --- a/client/src/assets/icons/search.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/selector-vertical.svg b/client/src/assets/icons/selector-vertical.svg deleted file mode 100644 index 29d9a4998..000000000 --- a/client/src/assets/icons/selector-vertical.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/settings-bold.svg b/client/src/assets/icons/settings-bold.svg deleted file mode 100644 index d8057286c..000000000 --- a/client/src/assets/icons/settings-bold.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/settings.svg b/client/src/assets/icons/settings.svg deleted file mode 100644 index eb86f1d19..000000000 --- a/client/src/assets/icons/settings.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/client/src/assets/icons/solana_logo.svg b/client/src/assets/icons/solana_logo.svg deleted file mode 100644 index bd349af26..000000000 --- a/client/src/assets/icons/solana_logo.svg +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/icons/solana_logo_banner.svg b/client/src/assets/icons/solana_logo_banner.svg deleted file mode 100644 index 08ec87cb1..000000000 --- a/client/src/assets/icons/solana_logo_banner.svg +++ /dev/null @@ -1,24 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/client/src/assets/icons/spedometer-icon.svg b/client/src/assets/icons/spedometer-icon.svg deleted file mode 100644 index 19b13f44e..000000000 --- a/client/src/assets/icons/spedometer-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/speedometer-icon.svg b/client/src/assets/icons/speedometer-icon.svg deleted file mode 100644 index 19b13f44e..000000000 --- a/client/src/assets/icons/speedometer-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/status-pages.svg b/client/src/assets/icons/status-pages.svg deleted file mode 100644 index 97cea88fb..000000000 --- a/client/src/assets/icons/status-pages.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/support.svg b/client/src/assets/icons/support.svg deleted file mode 100644 index 7de16379a..000000000 --- a/client/src/assets/icons/support.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/top-right-arrow.svg b/client/src/assets/icons/top-right-arrow.svg deleted file mode 100644 index d569cdd7e..000000000 --- a/client/src/assets/icons/top-right-arrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/trash-bin.svg b/client/src/assets/icons/trash-bin.svg deleted file mode 100644 index 83bca0c6f..000000000 --- a/client/src/assets/icons/trash-bin.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/up-arrow.svg b/client/src/assets/icons/up-arrow.svg deleted file mode 100644 index 0f8f3bc34..000000000 --- a/client/src/assets/icons/up-arrow.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/uptime-icon.svg b/client/src/assets/icons/uptime-icon.svg deleted file mode 100644 index 55706ae71..000000000 --- a/client/src/assets/icons/uptime-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/user-edit.svg b/client/src/assets/icons/user-edit.svg deleted file mode 100644 index cbd1a252f..000000000 --- a/client/src/assets/icons/user-edit.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/user-two.svg b/client/src/assets/icons/user-two.svg deleted file mode 100644 index 031b22787..000000000 --- a/client/src/assets/icons/user-two.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/user.svg b/client/src/assets/icons/user.svg deleted file mode 100644 index 231c89d40..000000000 --- a/client/src/assets/icons/user.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/client/src/assets/icons/warning-icon.svg b/client/src/assets/icons/warning-icon.svg deleted file mode 100644 index 468611758..000000000 --- a/client/src/assets/icons/warning-icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - From 7140d436a54c1bfbda12477d9a6ea0ab91c393d5 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Thu, 15 Jan 2026 23:39:26 -0500 Subject: [PATCH 239/400] style: apply Prettier formatting --- .../src/Components/v1/ActionsMenu/index.jsx | 5 ++- client/src/Components/v1/Alert/index.jsx | 26 +++++++++++++--- client/src/Components/v1/Check/Check.jsx | 10 ++++-- client/src/Components/v1/Common/AppBar.jsx | 12 +++++-- .../v1/Inputs/ImageUpload/index.jsx | 12 +++++-- .../src/Components/v1/Inputs/Radio/index.jsx | 7 ++++- .../src/Components/v1/Inputs/Select/index.jsx | 8 ++++- .../v1/Inputs/TextInput/Adornments/index.jsx | 10 ++++-- .../Components/v1/MonitorActions/index.jsx | 5 ++- .../v1/MonitorDetailsControlHeader/index.jsx | 26 ++++++++++++++-- .../src/Components/v1/ProgressBars/index.jsx | 10 ++++-- .../Components/v1/ProgressStepper/index.jsx | 12 +++++-- .../Components/v1/ShareComponent/index.jsx | 8 ++++- client/src/Components/v1/StarPrompt/index.jsx | 5 ++- .../v1/Table/TablePagination/index.jsx | 7 ++++- client/src/Components/v1/Toast/index.jsx | 31 ++++++++++++++++--- .../components/AddMemberMenu/index.jsx | 8 ++++- .../Components/StatusBoxes/StatusBox.jsx | 20 +++++++++--- .../Components/ActiveIncidentsPanel/index.jsx | 10 ++++-- .../LatestIncidentsPanel/IncidentItem.jsx | 10 ++++-- .../Components/StatisticsPanel/index.jsx | 20 +++++++++--- .../Components/MonitorActionButtons.jsx | 10 ++++-- .../Monitors/Components/Filters/index.jsx | 7 ++++- .../Components/MonitorsTable/index.jsx | 5 ++- .../Components/MonitorsTableMenu/index.jsx | 5 ++- .../Components/MonitorList/index.jsx | 4 ++- .../Maintenance/CreateMaintenance/index.jsx | 9 +++++- .../MaintenanceTable/ActionsMenu/index.jsx | 5 ++- .../Maintenance/MaintenanceTable/index.jsx | 20 +++++++++--- .../Notifications/components/ActionMenu.jsx | 5 ++- client/src/Pages/PageSpeed/Create/index.jsx | 10 ++++-- .../Components/Charts/AreaChartLegend.jsx | 7 ++++- .../Components/Charts/PieChartLegend.jsx | 7 ++++- .../Components/PageSpeedAreaChart/index.jsx | 7 ++++- .../Components/PerformanceReport/index.jsx | 7 ++++- .../Monitors/Components/Card/index.jsx | 5 ++- .../Create/Components/MonitorList/index.jsx | 9 ++++-- .../Create/Components/Progress/index.jsx | 7 ++++- .../Components/ControlsHeader/index.jsx | 12 +++++-- .../Status/Components/StatusBar/index.jsx | 12 +++++-- .../Components/StatusPagesTable/index.jsx | 7 ++++- client/src/Pages/Uptime/Create/index.jsx | 12 ++++++- .../Details/Components/ChartBoxes/index.jsx | 21 +++++++++++-- .../Components/Charts/ResponseTimeChart.jsx | 7 ++++- .../Components/ResponseTable/index.jsx | 7 ++++- .../Monitors/Components/Filter/index.jsx | 7 ++++- .../Components/StatusBoxes/statusBox.jsx | 15 +++++++-- .../Components/UptimeDataTable/index.jsx | 20 +++++++++--- 48 files changed, 425 insertions(+), 86 deletions(-) diff --git a/client/src/Components/v1/ActionsMenu/index.jsx b/client/src/Components/v1/ActionsMenu/index.jsx index 8fcc77f5d..b469e6764 100644 --- a/client/src/Components/v1/ActionsMenu/index.jsx +++ b/client/src/Components/v1/ActionsMenu/index.jsx @@ -84,7 +84,10 @@ const ActionsMenu = ({ }, }} > - + , - error: , - warning: , + info: ( + + ), + error: ( + + ), + warning: ( + + ), }; /** @@ -94,7 +109,10 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => { }, }} > - + )} diff --git a/client/src/Components/v1/Check/Check.jsx b/client/src/Components/v1/Check/Check.jsx index 5e25569be..01a41879e 100644 --- a/client/src/Components/v1/Check/Check.jsx +++ b/client/src/Components/v1/Check/Check.jsx @@ -34,7 +34,10 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) => alignItems="center" > {outlined ? ( - + ) : ( "& svg > path": { fill: colors[variant] }, }} > - + )} { aria-label="Menu button" onClick={toggleDrawer(true)} > - + { }} > - + diff --git a/client/src/Components/v1/Inputs/ImageUpload/index.jsx b/client/src/Components/v1/Inputs/ImageUpload/index.jsx index 5b2be991a..d224418e0 100644 --- a/client/src/Components/v1/Inputs/ImageUpload/index.jsx +++ b/client/src/Components/v1/Inputs/ImageUpload/index.jsx @@ -175,7 +175,10 @@ const ImageUpload = ({ boxShadow: theme.shape.boxShadow, }} > - + {(localError || progress.isLoading || progress.value !== 0) && ( } + icon={ + + } label={file?.name || "Upload failed"} size={file?.size} progress={progress.value} diff --git a/client/src/Components/v1/Inputs/Radio/index.jsx b/client/src/Components/v1/Inputs/Radio/index.jsx index 79a9e64f3..b6bd4e69c 100644 --- a/client/src/Components/v1/Inputs/Radio/index.jsx +++ b/client/src/Components/v1/Inputs/Radio/index.jsx @@ -47,7 +47,12 @@ const Radio = ({ } + checkedIcon={ + + } sx={{ color: "transparent", width: 16, diff --git a/client/src/Components/v1/Inputs/Select/index.jsx b/client/src/Components/v1/Inputs/Select/index.jsx index b292d6896..0f96a2ff6 100644 --- a/client/src/Components/v1/Inputs/Select/index.jsx +++ b/client/src/Components/v1/Inputs/Select/index.jsx @@ -98,7 +98,13 @@ const Select = ({ error={error} name={name} inputProps={{ id: id }} - IconComponent={(props) => } + IconComponent={(props) => ( + + )} MenuProps={{ disableScrollLock: true }} sx={{ fontSize: 13, diff --git a/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx b/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx index 8df4ee3b1..078684405 100644 --- a/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx +++ b/client/src/Components/v1/Inputs/TextInput/Adornments/index.jsx @@ -54,9 +54,15 @@ export const PasswordEndAdornment = ({ fieldType, setFieldType }) => { }} > {fieldType === "password" ? ( - + ) : ( - + )} diff --git a/client/src/Components/v1/MonitorActions/index.jsx b/client/src/Components/v1/MonitorActions/index.jsx index d5bab53ba..f63fc23fe 100644 --- a/client/src/Components/v1/MonitorActions/index.jsx +++ b/client/src/Components/v1/MonitorActions/index.jsx @@ -70,7 +70,10 @@ const MonitorActions = ({ isLoading }) => { aria-haspopup="menu" onClick={handleToggle} > - + } + startIcon={ + + } disabled={isTestNotificationsDisabled} onClick={() => { testAllNotifications({ monitorId: monitor?.id }); @@ -100,7 +105,17 @@ const MonitorDetailsControlHeader = ({ color="secondary" loading={isPausing} startIcon={ - monitor?.isActive ? : + monitor?.isActive ? ( + + ) : ( + + ) } onClick={() => { pauseMonitor({ @@ -116,7 +131,12 @@ const MonitorDetailsControlHeader = ({ @@ -110,7 +115,10 @@ const ControlsHeader = ({ statusPage, isPublic, url, type = "uptime" }) => { }} > {t("publicLink")} - + )} diff --git a/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx b/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx index 44cfbaca3..7b9460068 100644 --- a/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx +++ b/client/src/Pages/StatusPage/Status/Components/StatusBar/index.jsx @@ -9,14 +9,22 @@ import PropTypes from "prop-types"; const getMonitorStatus = (monitors, theme) => { const monitorsStatus = { icon: ( - + ), }; if (monitors.every((monitor) => monitor.status === true)) { monitorsStatus.msg = "All systems operational"; monitorsStatus.color = theme.palette.success.lowContrast; monitorsStatus.icon = ( - + ); } diff --git a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx index 57f7235e6..9882239bd 100644 --- a/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx +++ b/client/src/Pages/StatusPage/StatusPages/Components/StatusPagesTable/index.jsx @@ -48,7 +48,12 @@ const StatusPagesTable = ({ data }) => { }} > {content} - {row.isPublished && } + {row.isPublished && ( + + )} ); }, diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index 70be3bc00..b1460dba6 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -426,7 +426,17 @@ const UptimeCreate = ({ isClone = false }) => { color="secondary" loading={isBusy} startIcon={ - monitor?.isActive ? : + monitor?.isActive ? ( + + ) : ( + + ) } onClick={handlePause} > diff --git a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx b/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx index 6449946f3..b8496f5ea 100644 --- a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx +++ b/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx @@ -36,7 +36,12 @@ const ChartBoxes = ({ gap={theme.spacing(8)} > } + icon={ + + } header="Uptime" isEmpty={monitorData?.groupedUpChecks?.length === 0} > @@ -87,7 +92,12 @@ const ChartBoxes = ({ /> } + icon={ + + } header="Incidents" noDataMessage={noIncidentsMessage} isEmpty={monitorData?.groupedDownChecks?.length === 0} @@ -121,7 +131,12 @@ const ChartBoxes = ({ /> } + icon={ + + } header="Average Response Time" > diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx index 365324b4d..7e7a940e0 100644 --- a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx +++ b/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx @@ -11,7 +11,12 @@ const ResponseTImeChart = ({ isLoading = false, groupedChecks = [], dateRange }) return ( } + icon={ + + } header="Response Times" > } + icon={ + + } header="Response Times" height="100%" > diff --git a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx b/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx index 003617046..62ba0be13 100644 --- a/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/Filter/index.jsx @@ -131,7 +131,12 @@ const Filter = ({ color={theme.palette.primary.contrastText} onClick={handleReset} variant="contained" - endIcon={} + endIcon={ + + } sx={{ visibility: isFilterActive ? "visible" : "hidden", }} diff --git a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx index ee9a5d037..136c3575d 100644 --- a/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/StatusBoxes/statusBox.jsx @@ -19,21 +19,30 @@ const StatusBox = ({ title, value, status }) => { color = theme.palette.success.lowContrast; icon = ( - + ); } else if (status === "down") { color = theme.palette.error.lowContrast; icon = ( - + ); } else if (status === "paused") { color = theme.palette.warning.lowContrast; icon = ( - + ); } diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx b/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx index 43a467405..8c3239dd3 100644 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx +++ b/client/src/Pages/Uptime/Monitors/Components/UptimeDataTable/index.jsx @@ -91,9 +91,15 @@ const UptimeDataTable = ({ }} > {sort?.order === "asc" ? ( - + ) : ( - + )} @@ -127,9 +133,15 @@ const UptimeDataTable = ({ }} > {sort?.order === "asc" ? ( - + ) : ( - + )} From 7874439e4c87a41db14f103e2a14ccd6cd72709e Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 16 Jan 2026 07:31:23 -0500 Subject: [PATCH 240/400] fix: address PR review feedback for icon migration - Use AlertCircle for error and AlertTriangle for warning icons - Use Icon color prop instead of CSS fill for Lucide stroke-based icons - Replace wildcard import with explicit named imports for tree-shaking - Replace sx prop with style prop on Icon components - Add Trash2 to Icon component imports --- client/src/Components/v1/Alert/index.jsx | 2 +- client/src/Components/v1/Check/Check.jsx | 16 +- client/src/Components/v1/Icon/index.jsx | 138 +++++++++++++++++- client/src/Components/v1/Toast/index.jsx | 2 +- .../Components/MonitorList/index.jsx | 2 +- .../Create/Components/MonitorList/index.jsx | 2 +- 6 files changed, 144 insertions(+), 18 deletions(-) diff --git a/client/src/Components/v1/Alert/index.jsx b/client/src/Components/v1/Alert/index.jsx index 89416f5b1..dd3b2f9f2 100644 --- a/client/src/Components/v1/Alert/index.jsx +++ b/client/src/Components/v1/Alert/index.jsx @@ -18,7 +18,7 @@ const icons = { ), error: ( ), diff --git a/client/src/Components/v1/Check/Check.jsx b/client/src/Components/v1/Check/Check.jsx index 01a41879e..cc3749fda 100644 --- a/client/src/Components/v1/Check/Check.jsx +++ b/client/src/Components/v1/Check/Check.jsx @@ -39,17 +39,11 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) => size={20} /> ) : ( - path": { fill: colors[variant] }, - }} - > - - + )} ), diff --git a/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx b/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx index 3f606e4a2..2bf7a991f 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx +++ b/client/src/Pages/Maintenance/CreateMaintenance/Components/MonitorList/index.jsx @@ -17,7 +17,7 @@ const MonitorListItem = ({ monitor, onDelete }) => { onDelete(monitor)} /> diff --git a/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx b/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx index 81e6df6c5..349571fe8 100644 --- a/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx +++ b/client/src/Pages/StatusPage/Create/Components/MonitorList/index.jsx @@ -35,7 +35,7 @@ const MonitorListItem = ({ { onDelete(monitor); }} From f5aa24d77e6864ba67a8f18149fc4b3e287ed931 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 16 Jan 2026 07:34:03 -0500 Subject: [PATCH 241/400] fix: rename Image import to ImageIcon to avoid conflicts --- client/src/Components/v1/Icon/index.jsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/client/src/Components/v1/Icon/index.jsx b/client/src/Components/v1/Icon/index.jsx index 2f4ff968d..5828601fd 100644 --- a/client/src/Components/v1/Icon/index.jsx +++ b/client/src/Components/v1/Icon/index.jsx @@ -34,7 +34,7 @@ import { GripVertical, HelpCircle, History, - Image, + Image as ImageIcon, Info, Key, Layers, @@ -67,7 +67,8 @@ import { /** * Map of icon names to Lucide icon components. - * Only includes icons that are actually used in the application. + * Only icons explicitly imported here will be included in the bundle (tree-shaking). + * To add a new icon: import it above and add it to this object. */ const iconComponents = { Activity, @@ -103,7 +104,7 @@ const iconComponents = { GripVertical, HelpCircle, History, - Image, + Image: ImageIcon, Info, Key, Layers, From 95cd33ac065d53940873e6cfe1e796b2e60994ce Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 18:11:04 +0000 Subject: [PATCH 242/400] queue and buffer refactor --- server/src/config/services.ts | 22 +-- .../repositories/checks/IChecksRepository.ts | 5 +- .../checks/MongoChecksRepistory.ts | 108 +-------------- server/src/service/business/checkService.ts | 90 +++++++++++- .../SuperSimpleQueueHelper.ts | 18 ++- .../{bufferService.js => bufferService.ts} | 59 +++++--- .../service/infrastructure/statusService.ts | 129 ++---------------- server/src/types/check.ts | 70 +++++----- server/src/types/monitor.ts | 2 +- server/src/types/network.ts | 2 +- 10 files changed, 218 insertions(+), 287 deletions(-) rename server/src/service/infrastructure/{bufferService.js => bufferService.ts} (72%) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 57a223ebf..02a3d2345 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -183,7 +183,17 @@ export const initializeServices = async ({ stringService, }); - const bufferService = new BufferService({ db, logger, envSettings, incidentService }); + const checkService = new CheckService({ + db, + settingsService, + stringService, + errorService, + monitorsRepository, + logger, + checksRepository, + }); + + const bufferService = new BufferService({ db, logger, envSettings, incidentService, checkService }); const statusService = new StatusService({ db, logger, buffer: bufferService, incidentService, monitorsRepository }); @@ -208,6 +218,8 @@ export const initializeServices = async ({ networkService, statusService, notificationService, + checkService, + buffer: bufferService, }); const superSimpleQueue = await SuperSimpleQueue.create({ @@ -229,13 +241,7 @@ export const initializeServices = async ({ jobQueue: superSimpleQueue, monitorsRepository, }); - const checkService = new CheckService({ - db, - settingsService, - stringService, - errorService, - monitorsRepository, - }); + const diagnosticService = new DiagnosticService(); const inviteService = new InviteService({ db, diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index 5737daff6..7b98ae7b6 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -1,4 +1,4 @@ -import type { Check, CheckAudits, MonitorStatusResponse, MonitorType } from "@/types/index.js"; +import type { Check, MonitorType } from "@/types/index.js"; import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js"; export interface PageSpeedChecksResult { @@ -55,7 +55,8 @@ export interface UptimeChecksResult { export interface IChecksRepository { // create - buildCheck(status: MonitorStatusResponse, type: MonitorType): Check; + createChecks(checks: Check[]): Promise; + // single fetch // collection fetch findLatestChecksByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise; diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 225aec193..7707a7b17 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -11,10 +11,8 @@ import type { CheckMetadata, CheckNetworkInterfaceInfo, GotTimings, - MonitorStatusResponse, MonitorType, } from "@/types/index.js"; -import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; import { CheckModel, type CheckDocument } from "@/db/models/index.js"; import mongoose from "mongoose"; @@ -144,11 +142,11 @@ class MongoChecksRepository implements IChecksRepository { return undefined; } return { - cls: audits.cls ?? 0, - si: audits.si ?? 0, - fcp: audits.fcp ?? 0, - lcp: audits.lcp ?? 0, - tbt: audits.tbt ?? 0, + cls: audits.cls, + si: audits.si, + fcp: audits.fcp, + lcp: audits.lcp, + tbt: audits.tbt, }; }; @@ -187,100 +185,8 @@ class MongoChecksRepository implements IChecksRepository { }; }; - buildCheck = ( - statusResponse: MonitorStatusResponse - ) => { - const { - monitorId, - teamId, - type, - status, - responseTime, - code, - message, - payload, - first_byte_took, - body_read_took, - dns_took, - conn_took, - connect_took, - tls_took, - timings, - } = statusResponse; - - const check: any = { - metadata: { - monitorId, - teamId, - type, - }, - status, - statusCode: code, - responseTime, - timings: timings || {}, - message, - first_byte_took, - body_read_took, - dns_took, - conn_took, - connect_took, - tls_took, - }; - - if (type === "pagespeed") { - const pageSpeedPayload = payload as PageSpeedStatusPayload | undefined; - if (!pageSpeedPayload) { - this.logger.warn({ - message: "Failed to build check", - service: SERVICE_NAME, - method: "buildCheck", - details: "empty payload", - }); - return undefined; - } - const categories = pageSpeedPayload.lighthouseResult?.categories ?? {}; - const audits = pageSpeedPayload.lighthouseResult?.audits ?? {}; - const mapAudit = (audit: any) => { - if (!audit || typeof audit !== "object") { - return undefined; - } - return { - id: audit.id, - title: audit.title, - score: typeof audit.score === "number" ? audit.score : (audit.score ?? null), - displayValue: audit.displayValue, - numericValue: typeof audit.numericValue === "number" ? audit.numericValue : undefined, - numericUnit: audit.numericUnit, - }; - }; - check.accessibility = (categories?.accessibility?.score || 0) * 100; - check.bestPractices = (categories?.["best-practices"]?.score || 0) * 100; - check.seo = (categories?.seo?.score || 0) * 100; - check.performance = (categories?.performance?.score || 0) * 100; - check.audits = { - cls: mapAudit(audits?.["cumulative-layout-shift"]), - si: mapAudit(audits?.["speed-index"]), - fcp: mapAudit(audits?.["first-contentful-paint"]), - lcp: mapAudit(audits?.["largest-contentful-paint"]), - tbt: mapAudit(audits?.["total-blocking-time"]), - }; - } - - if (type === "hardware") { - const hardwarePayload = payload as HardwareStatusPayload | undefined; - const { cpu, memory, disk, host, net } = hardwarePayload?.data ?? {}; - const errorsSource = Array.isArray(hardwarePayload?.errors) - ? hardwarePayload?.errors - : (hardwarePayload?.errors as { errors?: CheckErrorInfo[] } | undefined)?.errors; - check.cpu = cpu ?? {}; - check.memory = memory ?? {}; - check.disk = disk ?? {}; - check.host = host ?? {}; - check.errors = errorsSource ?? []; - check.capture = hardwarePayload?.capture ?? {}; - check.net = net ?? {}; - } - return check; + createChecks = async (checks: Check[]) => { + return await CheckModel.insertMany(checks); }; findLatestChecksByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise => { diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index 8aea221f6..3e6c727b5 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -1,5 +1,6 @@ -import { IMonitorsRepository } from "@/repositories/index.js"; -import type { MonitorType } from "@/types/index.js"; +import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js"; +import type { MonitorType, MonitorStatusResponse, CheckErrorInfo, Check } from "@/types/index.js"; +import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; const SERVICE_NAME = "checkService"; @@ -11,32 +12,113 @@ class CheckService { private stringService: any; private errorService: any; private monitorsRepository: IMonitorsRepository; - + private checksRepository: IChecksRepository; + private logger: any; constructor({ db, settingsService, stringService, errorService, monitorsRepository, + logger, + checksRepository, }: { db: any; settingsService: any; stringService: any; errorService: any; monitorsRepository: IMonitorsRepository; + logger: any; + checksRepository: IChecksRepository; }) { this.db = db; this.settingsService = settingsService; this.stringService = stringService; this.errorService = errorService; this.monitorsRepository = monitorsRepository; + this.logger = logger; + this.checksRepository = checksRepository; } get serviceName() { return CheckService.SERVICE_NAME; } - buildCheck = async (statusResponse: any, type: MonitorType) => {}; + createChecks = async (checks: Check[]) => { + return this.checksRepository.createChecks(checks); + }; + + buildCheck = (statusResponse: MonitorStatusResponse) => { + const { monitorId, teamId, type, status, responseTime, code, message, payload, timings } = statusResponse; + + const check: Partial = { + metadata: { + monitorId, + teamId, + type, + }, + status, + statusCode: code, + responseTime: responseTime || 0, + timings: timings, + message, + }; + + if (type === "pagespeed") { + const pageSpeedPayload = payload as PageSpeedStatusPayload | undefined; + if (!pageSpeedPayload) { + this.logger.warn({ + message: "Failed to build check", + service: SERVICE_NAME, + method: "buildCheck", + details: "empty payload", + }); + return undefined; + } + const categories = pageSpeedPayload.lighthouseResult?.categories ?? {}; + const audits = pageSpeedPayload.lighthouseResult?.audits ?? {}; + const mapAudit = (audit: any) => { + if (!audit || typeof audit !== "object") { + return undefined; + } + return { + id: audit.id, + title: audit.title, + score: typeof audit.score === "number" ? audit.score : (audit.score ?? null), + displayValue: audit.displayValue, + numericValue: typeof audit.numericValue === "number" ? audit.numericValue : undefined, + numericUnit: audit.numericUnit, + }; + }; + check.accessibility = (categories?.accessibility?.score || 0) * 100; + check.bestPractices = (categories?.["best-practices"]?.score || 0) * 100; + check.seo = (categories?.seo?.score || 0) * 100; + check.performance = (categories?.performance?.score || 0) * 100; + check.audits = { + cls: mapAudit(audits?.["cumulative-layout-shift"]), + si: mapAudit(audits?.["speed-index"]), + fcp: mapAudit(audits?.["first-contentful-paint"]), + lcp: mapAudit(audits?.["largest-contentful-paint"]), + tbt: mapAudit(audits?.["total-blocking-time"]), + }; + } + + if (type === "hardware") { + const hardwarePayload = payload as HardwareStatusPayload | undefined; + const { cpu, memory, disk, host, net } = hardwarePayload?.data ?? {}; + const errorsSource = Array.isArray(hardwarePayload?.errors) + ? hardwarePayload?.errors + : (hardwarePayload?.errors as { errors?: CheckErrorInfo[] } | undefined)?.errors; + check.cpu = cpu; + check.memory = memory; + check.disk = disk; + check.host = host; + check.errors = errorsSource; + check.capture = hardwarePayload?.capture; + check.net = net; + } + return check; + }; getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => { if (!monitorId) { diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index aa4c4ab10..891635d56 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -10,6 +10,8 @@ class SuperSimpleQueueHelper { private networkService: INetworkService; private statusService: any; private notificationService: any; + private checkService: any; + private buffer: any; constructor({ db, @@ -17,18 +19,24 @@ class SuperSimpleQueueHelper { networkService, statusService, notificationService, + checkService, + buffer, }: { db: any; logger: any; networkService: INetworkService; statusService: any; notificationService: any; + checkService: any; + buffer: any; }) { this.db = db; this.logger = logger; this.networkService = networkService; this.statusService = statusService; this.notificationService = notificationService; + this.checkService = checkService; + this.buffer = buffer; } get serviceName() { @@ -61,7 +69,15 @@ class SuperSimpleQueueHelper { throw new Error("No network response"); } - const statusChangeResult = await this.statusService.updateMonitorStatus(status); + // Step 3. Build check + const check = await this.checkService.buildCheck(status); + + // Step 4 Add check to buffer + this.buffer.addToBuffer({ check }); + + // Step 4. Update monitor status + const statusChangeResult = await this.statusService.updateMonitorStatus(status, check); + this.notificationService .handleNotifications({ ...status, diff --git a/server/src/service/infrastructure/bufferService.js b/server/src/service/infrastructure/bufferService.ts similarity index 72% rename from server/src/service/infrastructure/bufferService.js rename to server/src/service/infrastructure/bufferService.ts index a218f56a0..e64a07077 100755 --- a/server/src/service/infrastructure/bufferService.js +++ b/server/src/service/infrastructure/bufferService.ts @@ -1,15 +1,38 @@ import { config } from "@/config/index.js"; +import type { Check } from "@/types/index.js"; const SERVICE_NAME = "BufferService"; class BufferService { static SERVICE_NAME = SERVICE_NAME; - constructor({ db, logger, envSettings, incidentService }) { - console.log(envSettings); + private BUFFER_TIMEOUT: number; + private db: any; + private logger: any; + private incidentService: any; + private SERVICE_NAME: string; + private buffer: any[]; + private incidentBuffer: any[]; + private bufferTimer: NodeJS.Timeout; + private checksService: any; + + constructor({ + db, + logger, + envSettings, + incidentService, + checkService, + }: { + db: any; + logger: any; + envSettings: any; + incidentService: any; + checkService: any; + }) { this.BUFFER_TIMEOUT = config.NODE_ENV === "development" ? 10 : 1000 * 60 * 1; // 1 minute this.db = db; this.logger = logger; this.incidentService = incidentService; + this.checksService = checkService; this.SERVICE_NAME = SERVICE_NAME; this.buffer = []; this.incidentBuffer = []; @@ -25,10 +48,10 @@ class BufferService { return BufferService.SERVICE_NAME; } - addToBuffer({ check }) { + addToBuffer({ check }: { check: Check }) { try { this.buffer.push(check); - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, service: this.SERVICE_NAME, @@ -38,7 +61,7 @@ class BufferService { } } - addIncidentToBuffer({ monitor, check, action = "create" }) { + addIncidentToBuffer({ monitor, check, action = "create" }: { monitor: any; check: Check; action?: string }) { try { if (!monitor || !check) { this.logger.warn({ @@ -50,7 +73,7 @@ class BufferService { } this.incidentBuffer.push({ monitor, check, action }); - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, service: this.SERVICE_NAME, @@ -60,20 +83,20 @@ class BufferService { } } - removeCheckFromBuffer(checkToRemove) { + removeCheckFromBuffer(checkToRemove: Check) { try { if (!checkToRemove) { return false; } const index = this.buffer.findIndex((check) => { - if (checkToRemove._id && check._id) { - return check._id.toString() === checkToRemove._id.toString(); + if (checkToRemove.id && check.id) { + return check.id.toString() === checkToRemove.id.toString(); } return ( - check.monitorId?.toString() === checkToRemove.monitorId?.toString() && - check.teamId?.toString() === checkToRemove.teamId?.toString() && - check.type === checkToRemove.type && + check.monitorId?.toString() === checkToRemove.metadata.monitorId && + check.teamId?.toString() === checkToRemove.metadata.teamId && + check.type === checkToRemove.metadata.type && check.status === checkToRemove.status && check.statusCode === checkToRemove.statusCode && check.responseTime === checkToRemove.responseTime && @@ -87,7 +110,7 @@ class BufferService { } return false; - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, service: this.SERVICE_NAME, @@ -102,7 +125,7 @@ class BufferService { this.bufferTimer = setTimeout(async () => { try { await this.flushBuffer(); - } catch (error) { + } catch (error: any) { this.logger.error({ message: `Error in flush cycle: ${error.message}`, service: this.SERVICE_NAME, @@ -118,9 +141,9 @@ class BufferService { async flushBuffer() { try { if (this.buffer.length > 0) { - await this.db.checkModule.createChecks(this.buffer); + await this.checksService.createChecks(this.buffer); } - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, service: this.SERVICE_NAME, @@ -133,7 +156,7 @@ class BufferService { if (this.incidentBuffer.length > 0 && this.incidentService) { await this.flushIncidentBuffer(); } - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, service: this.SERVICE_NAME, @@ -154,7 +177,7 @@ class BufferService { try { const itemsToProcess = [...this.incidentBuffer]; await this.incidentService.processIncidentsFromBuffer(itemsToProcess); - } catch (error) { + } catch (error: any) { this.logger.error({ message: `Error flushing incident buffer: ${error.message}`, service: this.SERVICE_NAME, diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index 23b691e0e..7d34017a5 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -1,8 +1,15 @@ import { IMonitorsRepository } from "@/repositories/index.js"; import MonitorStats from "../../db/models/MonitorStats.js"; import { CheckModel } from "@/db/models/index.js"; -import type { CheckErrorInfo, Monitor, MonitorStatusResponse, StatusChangeResult } from "@/types/index.js"; -import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; +import type { + CheckErrorInfo, + Monitor, + MonitorStatusResponse, + StatusChangeResult, + Check, + HardwareStatusPayload, + PageSpeedStatusPayload, +} from "@/types/index.js"; const SERVICE_NAME = "StatusService"; class StatusService { @@ -163,10 +170,9 @@ class StatusService { }; updateMonitorStatus = async ( - statusResponse: MonitorStatusResponse + statusResponse: MonitorStatusResponse, + check: Check ): Promise => { - const check = this.buildCheck(statusResponse); - await this.insertCheck(check); try { const { monitorId, teamId, status, code } = statusResponse; const monitor = await this.monitorsRepository.findById(monitorId, teamId); @@ -301,116 +307,7 @@ class StatusService { } }; - buildCheck = ( - networkResponse: MonitorStatusResponse - ) => { - const { - monitorId, - teamId, - type, - status, - responseTime, - code, - message, - payload, - first_byte_took, - body_read_took, - dns_took, - conn_took, - connect_took, - tls_took, - timings, - } = networkResponse; - - const check: any = { - metadata: { - monitorId, - teamId, - type, - }, - status, - statusCode: code, - responseTime, - timings: timings || {}, - message, - first_byte_took, - body_read_took, - dns_took, - conn_took, - connect_took, - tls_took, - }; - - if (type === "pagespeed") { - const pageSpeedPayload = payload as PageSpeedStatusPayload | undefined; - if (!pageSpeedPayload) { - this.logger.warn({ - message: "Failed to build check", - service: SERVICE_NAME, - method: "buildCheck", - details: "empty payload", - }); - return undefined; - } - const categories = pageSpeedPayload.lighthouseResult?.categories ?? {}; - const audits = pageSpeedPayload.lighthouseResult?.audits ?? {}; - const mapAudit = (audit: any) => { - if (!audit || typeof audit !== "object") { - return undefined; - } - return { - id: audit.id, - title: audit.title, - score: typeof audit.score === "number" ? audit.score : (audit.score ?? null), - displayValue: audit.displayValue, - numericValue: typeof audit.numericValue === "number" ? audit.numericValue : undefined, - numericUnit: audit.numericUnit, - }; - }; - check.accessibility = (categories?.accessibility?.score || 0) * 100; - check.bestPractices = (categories?.["best-practices"]?.score || 0) * 100; - check.seo = (categories?.seo?.score || 0) * 100; - check.performance = (categories?.performance?.score || 0) * 100; - check.audits = { - cls: mapAudit(audits?.["cumulative-layout-shift"]), - si: mapAudit(audits?.["speed-index"]), - fcp: mapAudit(audits?.["first-contentful-paint"]), - lcp: mapAudit(audits?.["largest-contentful-paint"]), - tbt: mapAudit(audits?.["total-blocking-time"]), - }; - } - - if (type === "hardware") { - const hardwarePayload = payload as HardwareStatusPayload | undefined; - const { cpu, memory, disk, host, net } = hardwarePayload?.data ?? {}; - const errorsSource = Array.isArray(hardwarePayload?.errors) - ? hardwarePayload?.errors - : (hardwarePayload?.errors as { errors?: CheckErrorInfo[] } | undefined)?.errors; - check.cpu = cpu ?? {}; - check.memory = memory ?? {}; - check.disk = disk ?? {}; - check.host = host ?? {}; - check.errors = errorsSource ?? []; - check.capture = hardwarePayload?.capture ?? {}; - check.net = net ?? {}; - } - return check; - }; - - /** - * Inserts a check into the database based on the network response. - * - * @param {Object} networkResponse - The network response object. - * @param {string} networkResponse.monitorId - The monitor ID. - * @param {string} networkResponse.type - The type of the response. - * @param {string} networkResponse.status - The status of the response. - * @param {number} networkResponse.responseTime - The response time. - * @param {number} networkResponse.code - The status code. - * @param {string} networkResponse.message - The message. - * @param {Object} networkResponse.payload - The payload of the response. - * @returns {Promise} A promise that resolves when the check is inserted. - */ - insertCheck = async (check: any) => { + insertCheck = async (check: Check) => { try { if (typeof check === "undefined") { this.logger.warn({ @@ -427,7 +324,7 @@ class StatusService { message: error.message, service: error.service || SERVICE_NAME, method: error.method || "insertCheck", - details: error.details || `Error inserting check for monitor: ${check?.monitorId}`, + details: error.details || `Error inserting check for monitor: ${check?.metadata.monitorId}`, stack: error.stack, }); } diff --git a/server/src/types/check.ts b/server/src/types/check.ts index e3f32280f..96c6f6e7f 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -9,40 +9,40 @@ export interface CheckMetadata { } export interface CheckCpuInfo { - physical_core: number; - logical_core: number; - frequency: number; - temperature: number[]; - free_percent: number; - usage_percent: number; + physical_core?: number; + logical_core?: number; + frequency?: number; + temperature?: number[]; + free_percent?: number; + usage_percent?: number; } export interface CheckMemoryInfo { - total_bytes: number; - available_bytes: number; - used_bytes: number; - usage_percent: number; + total_bytes?: number; + available_bytes?: number; + used_bytes?: number; + usage_percent?: number; } export interface CheckHostInfo { - os: string; - platform: string; - kernel_version: string; + os?: string; + platform?: string; + kernel_version?: string; } export interface CheckCaptureInfo { - version: string; - mode: string; + version?: string; + mode?: string; } export interface CheckDiskInfo { - device: string; - mountpoint: string; - read_speed_bytes: number; - write_speed_bytes: number; - total_bytes: number; - free_bytes: number; - usage_percent: number; + device?: string; + mountpoint?: string; + read_speed_bytes?: number; + write_speed_bytes?: number; + total_bytes?: number; + free_bytes?: number; + usage_percent?: number; } export interface CheckErrorInfo { @@ -65,11 +65,11 @@ export interface CheckNetworkInterfaceInfo { } export interface CheckAudits { - cls: ILighthouseAudit; - si: ILighthouseAudit; - fcp: ILighthouseAudit; - lcp: ILighthouseAudit; - tbt: ILighthouseAudit; + cls?: ILighthouseAudit; + si?: ILighthouseAudit; + fcp?: ILighthouseAudit; + lcp?: ILighthouseAudit; + tbt?: ILighthouseAudit; } export interface ILighthouseAudit { @@ -86,19 +86,19 @@ export interface Check { metadata: CheckMetadata; status: boolean; responseTime: number; - timings: GotTimings; + timings?: GotTimings; statusCode: number; message: string; ack: boolean; ackAt?: string | null; expiry: string; - cpu: CheckCpuInfo; - memory: CheckMemoryInfo; - disk: CheckDiskInfo[]; - host: CheckHostInfo; - errors: CheckErrorInfo[]; - capture: CheckCaptureInfo; - net: CheckNetworkInterfaceInfo[]; + cpu?: CheckCpuInfo; + memory?: CheckMemoryInfo; + disk?: CheckDiskInfo[]; + host?: CheckHostInfo; + errors?: CheckErrorInfo[]; + capture?: CheckCaptureInfo; + net?: CheckNetworkInterfaceInfo[]; accessibility?: number; bestPractices?: number; seo?: number; diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index 4c8dc7528..dc3bd8c09 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -1,4 +1,4 @@ -export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game"] as const; +export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "unknown"] as const; export type MonitorType = (typeof MonitorTypes)[number]; export interface MonitorThresholds { diff --git a/server/src/types/network.ts b/server/src/types/network.ts index 097cb928d..10f89842f 100644 --- a/server/src/types/network.ts +++ b/server/src/types/network.ts @@ -16,7 +16,7 @@ import type { export interface MonitorStatusResponse { monitorId: string; teamId: string; - type: MonitorType | "unknown"; + type: MonitorType; status: boolean; code: number; message: string; From 2055943876a8ea870c85d91c654bd0859312b08d Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 16 Jan 2026 13:10:45 -0500 Subject: [PATCH 243/400] fix: make dateRange optional for hardware details endpoint The Joi validation marks dateRange as optional, but the controller was using requireString() which throws a 400 error when dateRange is missing. Changed to optionalString() with "recent" as the default value, matching the behavior of other similar endpoints and the Joi validation schema. --- server/src/controllers/monitorController.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index d46845026..56a18b6b3 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -97,7 +97,7 @@ class MonitorController { await getHardwareDetailsByIdQueryValidation.validateAsync(req.query); const monitorId = requireString(req?.params?.monitorId, "Monitor ID"); - const dateRange = requireString(req?.query?.dateRange, "dateRange"); + const dateRange = optionalString(req?.query?.dateRange, "dateRange") || "recent"; const teamId = requireTeamId(req?.user?.teamId); const monitor = await this.monitorService.getHardwareDetailsById({ From 7436c2b5df6623b784a170e4c7c2cd8590458b9f Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 16 Jan 2026 13:12:00 -0500 Subject: [PATCH 244/400] fix: remove unnecessary dateRange param now that backend defaults it --- client/src/Pages/Infrastructure/Create/index.jsx | 1 - 1 file changed, 1 deletion(-) diff --git a/client/src/Pages/Infrastructure/Create/index.jsx b/client/src/Pages/Infrastructure/Create/index.jsx index e5cdbcd63..06a6a5716 100644 --- a/client/src/Pages/Infrastructure/Create/index.jsx +++ b/client/src/Pages/Infrastructure/Create/index.jsx @@ -44,7 +44,6 @@ const CreateInfrastructureMonitor = () => { // Fetch monitor details if editing const [monitor, isLoading] = useFetchHardwareMonitorById({ monitorId, - dateRange: "day", updateTrigger, }); const [deleteMonitor, isDeleting] = useDeleteMonitor(); From 5784fec3d126db67be1da05ce543ae117460783f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 18:17:55 +0000 Subject: [PATCH 245/400] format --- .../SuperSimpleQueue/SuperSimpleQueue.ts | 36 +++++++++---------- server/test/superSimpleQueueHelper.test.ts | 4 +-- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts index daf7e130f..8fd55ff70 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.ts @@ -217,24 +217,24 @@ class SuperSimpleQueue implements ISuperSimpleQueue { }; getJobs = async () => { - const jobs = await this.scheduler.getJobs(); - return jobs.map((job) => { - return { - monitorId: job.id, - monitorUrl: job?.data?.url || null, - monitorType: job?.data?.type || null, - monitorInterval: job?.data?.interval || null, - active: job.active, - lockedAt: job.lockedAt ?? null, - runCount: job.runCount ?? 0, - failCount: job.failCount ?? 0, - failReason: job.lastFailReason ?? null, - lastRunAt: job.lastRunAt ?? null, - lastFinishedAt: job.lastFinishedAt ?? null, - lastRunTook: job.lockedAt ? null : (job.lastFinishedAt ?? 0) - (job.lastRunAt ?? 0), - lastFailedAt: job.lastFailedAt ?? null, - }; - }); + const jobs = await this.scheduler.getJobs(); + return jobs.map((job) => { + return { + monitorId: job.id, + monitorUrl: job?.data?.url || null, + monitorType: job?.data?.type || null, + monitorInterval: job?.data?.interval || null, + active: job.active, + lockedAt: job.lockedAt ?? null, + runCount: job.runCount ?? 0, + failCount: job.failCount ?? 0, + failReason: job.lastFailReason ?? null, + lastRunAt: job.lastRunAt ?? null, + lastFinishedAt: job.lastFinishedAt ?? null, + lastRunTook: job.lockedAt ? null : (job.lastFinishedAt ?? 0) - (job.lastRunAt ?? 0), + lastFailedAt: job.lastFailedAt ?? null, + }; + }); }; flushQueues = async () => { diff --git a/server/test/superSimpleQueueHelper.test.ts b/server/test/superSimpleQueueHelper.test.ts index 6fba58eda..19caef876 100644 --- a/server/test/superSimpleQueueHelper.test.ts +++ b/server/test/superSimpleQueueHelper.test.ts @@ -42,9 +42,7 @@ describe("SuperSimpleQueueHelper", () => { const { helper } = createHelper({ networkService: { requestStatus: jest.fn().mockResolvedValue(networkResponse) }, statusService: { - updateMonitorStatus: jest - .fn() - .mockResolvedValue({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }), + updateMonitorStatus: jest.fn().mockResolvedValue({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }), }, notificationService: { handleNotifications: jest.fn().mockResolvedValue(undefined) }, }); From af141ec772d810dc155300b04a0f6c73cd6f0fb8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 18:25:28 +0000 Subject: [PATCH 246/400] build fix --- server/src/service/infrastructure/bufferService.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/src/service/infrastructure/bufferService.ts b/server/src/service/infrastructure/bufferService.ts index e64a07077..5a809eea5 100755 --- a/server/src/service/infrastructure/bufferService.ts +++ b/server/src/service/infrastructure/bufferService.ts @@ -12,7 +12,7 @@ class BufferService { private SERVICE_NAME: string; private buffer: any[]; private incidentBuffer: any[]; - private bufferTimer: NodeJS.Timeout; + private bufferTimer: NodeJS.Timeout | null = null; private checksService: any; constructor({ @@ -122,6 +122,9 @@ class BufferService { } scheduleNextFlush() { + if (this.bufferTimer) { + clearTimeout(this.bufferTimer); + } this.bufferTimer = setTimeout(async () => { try { await this.flushBuffer(); From d633fc5ccbf04d7e35bcd4d466da0d7c18d171ae Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 18:37:14 +0000 Subject: [PATCH 247/400] infra fix --- client/package-lock.json | 55 ++++++++++++------- .../Create/hooks/useInfrastructureSubmit.jsx | 2 +- .../src/Pages/Infrastructure/Create/index.jsx | 13 +++-- 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 176b2c5b4..7f5397b7f 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -97,6 +97,7 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", "license": "MIT", + "peer": true, "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", @@ -408,6 +409,7 @@ "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz", "integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -451,6 +453,7 @@ "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz", "integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1347,6 +1350,7 @@ "resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.11.tgz", "integrity": "sha512-k2D3FLJS+/qD0qnd6ZlAjGFvaaxe1Dl10NyvpeDzIebMuYdn8VqYe6XBgGueEAtnzSJM4V03VD9kb5Fi24dnTA==", "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.26.0", "@mui/core-downloads-tracker": "^6.4.11", @@ -1511,7 +1515,6 @@ "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.0.tgz", "integrity": "sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.1", "@mui/utils": "^7.1.0", @@ -1539,7 +1542,6 @@ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz", "integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.1", "@mui/types": "^7.4.2", @@ -1570,7 +1572,6 @@ "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.0.tgz", "integrity": "sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.1", "@emotion/cache": "^11.13.5", @@ -1646,7 +1647,6 @@ "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz", "integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.1", "@mui/types": "^7.4.2", @@ -2024,9 +2024,9 @@ } }, "node_modules/@remix-run/router": { - "version": "1.23.0", - "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz", - "integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==", + "version": "1.23.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz", + "integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==", "license": "MIT", "engines": { "node": ">=14.0.0" @@ -2712,6 +2712,7 @@ "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", "devOptional": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~7.12.0" } @@ -2733,6 +2734,7 @@ "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.22.tgz", "integrity": "sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==", "license": "MIT", + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -2796,6 +2798,7 @@ "integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "dev": true, "license": "MIT", + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3102,6 +3105,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "caniuse-lite": "^1.0.30001716", "electron-to-chromium": "^1.5.149", @@ -3514,7 +3518,8 @@ "version": "1.11.13", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/debug": { "version": "4.4.1", @@ -3921,6 +3926,7 @@ "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4728,6 +4734,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -5264,9 +5271,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -6004,6 +6011,7 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "license": "MIT", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -6017,6 +6025,7 @@ "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz", "integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==", "license": "MIT", + "peer": true, "engines": { "node": ">=18.0.0" }, @@ -6103,12 +6112,12 @@ } }, "node_modules/react-router": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz", - "integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz", + "integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0" + "@remix-run/router": "1.23.2" }, "engines": { "node": ">=14.0.0" @@ -6118,13 +6127,13 @@ } }, "node_modules/react-router-dom": { - "version": "6.30.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz", - "integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==", + "version": "6.30.3", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz", + "integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==", "license": "MIT", "dependencies": { - "@remix-run/router": "1.23.0", - "react-router": "6.30.1" + "@remix-run/router": "1.23.2", + "react-router": "6.30.3" }, "engines": { "node": ">=14.0.0" @@ -6220,7 +6229,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-persist": { "version": "6.0.0", @@ -6358,6 +6368,7 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz", "integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==", "license": "MIT", + "peer": true, "dependencies": { "@types/estree": "1.0.7" }, @@ -6993,6 +7004,7 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7112,6 +7124,7 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz", "integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==", "license": "MIT", + "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx index c5c7aacf2..04279cac4 100644 --- a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx +++ b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx @@ -61,7 +61,7 @@ const useInfrastructureSubmit = () => { }; const finalForm = { - ...(isCreate ? {} : { _id: monitorId }), + ...(isCreate ? {} : { id: monitorId }), ...rest, description: form.name, type: "hardware", diff --git a/client/src/Pages/Infrastructure/Create/index.jsx b/client/src/Pages/Infrastructure/Create/index.jsx index b27c11cd4..21d70720d 100644 --- a/client/src/Pages/Infrastructure/Create/index.jsx +++ b/client/src/Pages/Infrastructure/Create/index.jsx @@ -22,9 +22,9 @@ import { useTranslation } from "react-i18next"; import { useDeleteMonitor, useFetchGlobalSettings, - useFetchHardwareMonitorById, + useFetchMonitorById, usePauseMonitor, -} from "../../../Hooks/monitorHooks.js"; +} from "@/Hooks/monitorHooks.js"; import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm.jsx"; import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm.jsx"; import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit.jsx"; @@ -37,14 +37,16 @@ const CreateInfrastructureMonitor = () => { const { t } = useTranslation(); // State + const [monitor, setMonitor] = useState(null); const [https, setHttps] = useState(false); const [updateTrigger, setUpdateTrigger] = useState(false); const [availableDisks, setAvailableDisks] = useState([]); // Fetch monitor details if editing - const [monitor, isLoading] = useFetchHardwareMonitorById({ + const [isLoading] = useFetchMonitorById({ monitorId, - updateTrigger, + setMonitor, + updateTrigger: true, }); const [deleteMonitor, isDeleting] = useDeleteMonitor(); const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings(); @@ -150,8 +152,7 @@ const CreateInfrastructureMonitor = () => { const handleRemove = async (event) => { event.preventDefault(); - const TEMP_MONITOR = { id: monitor._id }; - await deleteMonitor({ monitor: TEMP_MONITOR, redirect: "/infrastructure" }); + await deleteMonitor({ monitor, redirect: "/infrastructure" }); }; const isBusy = From 62ea7b0e5b836d9aaeea0018eb39bcac681d4c71 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 16 Jan 2026 14:09:37 -0500 Subject: [PATCH 248/400] Fix horizontal overflow causing white space on narrow screens Add overflow-x: hidden and width: 100% to .home-layout to prevent horizontal scrollbar. Add min-width: 0 to .home-content-wrapper to allow flexbox child to shrink below content size at narrow widths. --- client/src/Components/v1/Layouts/HomeLayout/index.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/client/src/Components/v1/Layouts/HomeLayout/index.css b/client/src/Components/v1/Layouts/HomeLayout/index.css index 56606a935..953a2c682 100644 --- a/client/src/Components/v1/Layouts/HomeLayout/index.css +++ b/client/src/Components/v1/Layouts/HomeLayout/index.css @@ -3,6 +3,8 @@ min-height: 100vh; margin: 0 auto; padding: 0; + overflow-x: hidden; + width: 100%; } /* TODO go for this approach for responsiveness. The aside needs to be taken care of */ @@ -30,4 +32,6 @@ max-width: 1400px; margin: 0 auto; flex: 1; + min-width: 0; + overflow-x: hidden; } From af82dec2ad97fd10001e50cdd2e56b3e83f92910 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 19:12:21 +0000 Subject: [PATCH 249/400] separate user from token --- .../src/Components/v1/ActionsMenu/index.jsx | 1 - client/src/Features/Auth/authSlice.js | 29 ++++++++++--------- client/src/Hooks/monitorHooks.js | 1 - client/src/Pages/Auth/Register/index.jsx | 2 +- client/src/Pages/Uptime/Monitors/index.jsx | 1 - client/src/Utils/NetworkService.js | 13 ++------- 6 files changed, 19 insertions(+), 28 deletions(-) diff --git a/client/src/Components/v1/ActionsMenu/index.jsx b/client/src/Components/v1/ActionsMenu/index.jsx index b469e6764..ea0d029b8 100644 --- a/client/src/Components/v1/ActionsMenu/index.jsx +++ b/client/src/Components/v1/ActionsMenu/index.jsx @@ -32,7 +32,6 @@ const ActionsMenu = ({ event.preventDefault(); event.stopPropagation(); let monitor = { id: actions.id }; - console.log(monitor); await deleteMonitor({ monitor }); updateRowCallback(); }; diff --git a/client/src/Features/Auth/authSlice.js b/client/src/Features/Auth/authSlice.js index 5ee31944a..b78a8b47e 100644 --- a/client/src/Features/Auth/authSlice.js +++ b/client/src/Features/Auth/authSlice.js @@ -10,21 +10,24 @@ const initialState = { msg: null, }; -export const register = createAsyncThunk("auth/register", async (form, thunkApi) => { - try { - const res = await networkService.registerUser(form); - return res.data; - } catch (error) { - if (error.response.data) { - return thunkApi.rejectWithValue(error.response.data); +export const register = createAsyncThunk( + "auth/register", + async ({ user, token }, thunkApi) => { + try { + const res = await networkService.registerUser(user, token); + return res.data; + } catch (error) { + if (error.response.data) { + return thunkApi.rejectWithValue(error.response.data); + } + const payload = { + status: false, + msg: error.message ? error.message : "Unknown error", + }; + return thunkApi.rejectWithValue(payload); } - const payload = { - status: false, - msg: error.message ? error.message : "Unknown error", - }; - return thunkApi.rejectWithValue(payload); } -}); +); export const login = createAsyncThunk("auth/login", async (form, thunkApi) => { try { diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index d865ffab8..4fdedbcff 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -342,7 +342,6 @@ export const useDeleteMonitor = () => { const deleteMonitor = async ({ monitor, redirect }) => { try { setIsLoading(true); - console.log(monitor); await networkService.deleteMonitorById({ monitorId: monitor.id }); createToast({ body: "Monitor deleted successfully!" }); if (redirect) { diff --git a/client/src/Pages/Auth/Register/index.jsx b/client/src/Pages/Auth/Register/index.jsx index 3345d8aa0..821c56427 100644 --- a/client/src/Pages/Auth/Register/index.jsx +++ b/client/src/Pages/Auth/Register/index.jsx @@ -176,7 +176,7 @@ const Register = ({ superAdminExists }) => { delete toSubmit.confirm; - const action = await dispatch(register(toSubmit)); + const action = await dispatch(register({ user: toSubmit, token })); if (action.payload.success) { navigate("/uptime"); createToast({ diff --git a/client/src/Pages/Uptime/Monitors/index.jsx b/client/src/Pages/Uptime/Monitors/index.jsx index 101020eea..67019362f 100644 --- a/client/src/Pages/Uptime/Monitors/index.jsx +++ b/client/src/Pages/Uptime/Monitors/index.jsx @@ -143,7 +143,6 @@ const UptimeMonitors = () => { type: ["http", "ping", "docker", "port", "game"], }); - console.log(monitors); useEffect(() => { if (isSearching) { setPage(undefined); diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js index bd0e37b30..5b28281fe 100644 --- a/client/src/Utils/NetworkService.js +++ b/client/src/Utils/NetworkService.js @@ -347,17 +347,8 @@ class NetworkService { return this.axiosInstance.get(`/monitors/certificate/${config.monitorId}`); } - /** - * ************************************ - * Registers a new user - * ************************************ - * - * @async - * @param {Object} form - The form data for the new user to be registered. - * @returns {Promise} The response from the axios POST request. - */ - async registerUser(form) { - return this.axiosInstance.post(`/auth/register`, form); + async registerUser(user, token) { + return this.axiosInstance.post(`/auth/register`, { user, token }); } /** From 64dc381e0f527c7ec6f7214983d7c3a1674447c9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 19:14:03 +0000 Subject: [PATCH 250/400] separate user from token --- server/src/controllers/authController.ts | 10 +- server/src/db/models/User.js | 105 ------------------ server/src/db/models/User.ts | 95 ++++++++++++++++ server/src/db/models/index.ts | 3 + server/src/repositories/index.ts | 3 + .../src/repositories/users/IUserRepository.ts | 9 ++ .../repositories/users/MongoUserRepository.ts | 4 + server/src/service/business/userService.ts | 5 +- server/src/types/index.ts | 1 + server/src/types/user.ts | 24 ++++ 10 files changed, 148 insertions(+), 111 deletions(-) delete mode 100755 server/src/db/models/User.js create mode 100644 server/src/db/models/User.ts create mode 100644 server/src/repositories/users/IUserRepository.ts create mode 100644 server/src/repositories/users/MongoUserRepository.ts create mode 100644 server/src/types/user.ts diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts index d033aa39d..435813a36 100755 --- a/server/src/controllers/authController.ts +++ b/server/src/controllers/authController.ts @@ -32,11 +32,13 @@ class AuthController { registerUser = async (req: Request, res: Response, next: NextFunction) => { try { - if (req.body?.email) { - req.body.email = req.body.email?.toLowerCase(); + const newUser = req.body.user; + const newUserToken = req.body.token; + if (newUser?.email) { + newUser.email = newUser.email.toLowerCase(); } - await registrationBodyValidation.validateAsync(req.body); - const { user, token } = await this.userService.registerUser(req.body, req.file); + await registrationBodyValidation.validateAsync(newUser); + const { user, token } = await this.userService.registerUser(newUser, newUserToken, req.file); res.status(200).json({ success: true, msg: "User registered successfully", diff --git a/server/src/db/models/User.js b/server/src/db/models/User.js deleted file mode 100755 index bf3012b26..000000000 --- a/server/src/db/models/User.js +++ /dev/null @@ -1,105 +0,0 @@ -import mongoose from "mongoose"; -import bcrypt from "bcryptjs"; -import Monitor from "./Monitor.js"; -import Team from "./Team.js"; -import Notification from "./Notification.js"; - -const UserSchema = mongoose.Schema( - { - firstName: { - type: String, - required: true, - }, - lastName: { - type: String, - required: true, - }, - email: { - type: String, - required: true, - unique: true, - }, - password: { - type: String, - required: true, - }, - avatarImage: { - type: String, - }, - profileImage: { - data: Buffer, - contentType: String, - }, - isActive: { - type: Boolean, - default: true, - }, - isVerified: { - type: Boolean, - default: false, - }, - role: { - type: [String], - default: "user", - enum: ["user", "admin", "superadmin", "demo"], - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - }, - checkTTL: { - type: Number, - }, - }, - { - timestamps: true, - } -); - -UserSchema.pre("save", function (next) { - if (!this.isModified("password")) { - return next(); - } - const salt = bcrypt.genSaltSync(10); - this.password = bcrypt.hashSync(this.password, salt); - next(); -}); - -UserSchema.pre("findOneAndUpdate", function (next) { - const update = this.getUpdate(); - if ("password" in update) { - const salt = bcrypt.genSaltSync(10); - update.password = bcrypt.hashSync(update.password, salt); - } - - next(); -}); - -UserSchema.pre("findOneAndDelete", async function (next) { - try { - const userToDelete = await this.model.findOne(this.getFilter()); - if (!userToDelete) return next(); - if (userToDelete.role.includes("superadmin")) { - await Team.deleteOne({ _id: userToDelete.teamId }); - await Monitor.deleteMany({ userId: userToDelete._id }); - await this.model.deleteMany({ - teamId: userToDelete.teamId, - _id: { $ne: userToDelete._id }, - }); - await Notification.deleteMany({ teamId: userToDelete.teamId }); - } - next(); - } catch (error) { - next(error); - } -}); - -UserSchema.methods.comparePassword = async function (submittedPassword) { - const res = await bcrypt.compare(submittedPassword, this.password); - return res; -}; - -const User = mongoose.model("User", UserSchema); - -export default User; diff --git a/server/src/db/models/User.ts b/server/src/db/models/User.ts new file mode 100644 index 000000000..e00250340 --- /dev/null +++ b/server/src/db/models/User.ts @@ -0,0 +1,95 @@ +import { Schema, model, type Types } from "mongoose"; +import bcrypt from "bcryptjs"; +import type { User, UserProfileImage, UserRole } from "@/types/index.js"; +import { MonitorModel } from "@/db/models/index.js"; +import Team from "./Team.js"; +import Notification from "./Notification.js"; + +type UserDocumentBase = Omit & { + teamId?: Types.ObjectId; + profileImage?: Required; +}; + +interface UserDocument extends UserDocumentBase { + _id: Types.ObjectId; + teamId?: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const profileImageSchema = new Schema>( + { + data: { type: Buffer }, + contentType: { type: String }, + }, + { _id: false } +); + +const UserSchema = new Schema( + { + firstName: { type: String, required: true }, + lastName: { type: String, required: true }, + email: { type: String, required: true, unique: true }, + password: { type: String, required: true }, + avatarImage: { type: String }, + profileImage: { type: profileImageSchema }, + isActive: { type: Boolean, default: true }, + isVerified: { type: Boolean, default: false }, + role: { + type: [String], + enum: ["user", "admin", "superadmin", "demo" satisfies UserRole], + default: ["user"], + }, + teamId: { + type: Schema.Types.ObjectId, + ref: "Team", + immutable: true, + }, + checkTTL: { type: Number }, + }, + { timestamps: true } +); + +UserSchema.pre("save", function (next) { + if (!this.isModified("password")) { + return next(); + } + const salt = bcrypt.genSaltSync(10); + this.password = bcrypt.hashSync(this.password, salt); + next(); +}); + +UserSchema.pre("findOneAndUpdate", function (next) { + const update = this.getUpdate(); + if (update && "password" in update) { + const salt = bcrypt.genSaltSync(10); + (update as any).password = bcrypt.hashSync((update as any).password, salt); + } + next(); +}); + +UserSchema.pre("findOneAndDelete", async function (next) { + try { + const userToDelete = await this.model.findOne(this.getFilter()); + if (!userToDelete) return next(); + if (userToDelete.role.includes("superadmin")) { + await Team.deleteOne({ _id: userToDelete.teamId }); + await MonitorModel.deleteMany({ userId: userToDelete._id }); + await this.model.deleteMany({ teamId: userToDelete.teamId, _id: { $ne: userToDelete._id } }); + await Notification.deleteMany({ teamId: userToDelete.teamId }); + } + next(); + } catch (error) { + next(error as Error); + } +}); + +UserSchema.methods.comparePassword = async function (submittedPassword: string) { + return bcrypt.compare(submittedPassword, this.password); +}; + +const UserModel = model("User", UserSchema); + +export type { UserDocument }; +export { UserModel }; +export default UserModel; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index b92ae18b7..d91aa0778 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -9,3 +9,6 @@ export { default as MonitorStatsModel } from "@/db/models/MonitorStats.js"; export * from "@/db/models/StatusPage.js"; export { default as StatusPageModel } from "@/db/models/StatusPage.js"; + +export * from "@/db/models/User.js"; +export { default as UserModel } from "@/db/models/User.js"; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 2c27591f1..92d9b2371 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -9,3 +9,6 @@ export { default as MongoMonitorStatsRepository } from "@/repositories/monitor-s export * from "@/repositories/status-pages/IStatusPagesRepository.js"; export { default as MongoStatusPagesRepository } from "@/repositories/status-pages/MongoStatusPagesRepository.js"; + +export * from "@/repositories/users/IUserRepository.js"; +export { default as MongoUserRepository } from "@/repositories/users/MongoUserRepository.js"; diff --git a/server/src/repositories/users/IUserRepository.ts b/server/src/repositories/users/IUserRepository.ts new file mode 100644 index 000000000..259ba781b --- /dev/null +++ b/server/src/repositories/users/IUserRepository.ts @@ -0,0 +1,9 @@ +import type { User } from "@/types/index.js"; + +export interface IUsersRepository { + // create + // fetch + // update + // delete + // other +} diff --git a/server/src/repositories/users/MongoUserRepository.ts b/server/src/repositories/users/MongoUserRepository.ts new file mode 100644 index 000000000..82c5eb5b6 --- /dev/null +++ b/server/src/repositories/users/MongoUserRepository.ts @@ -0,0 +1,4 @@ +import { IUsersRepository } from "@/repositories/index.js"; +class MongoUserRepository implements IUsersRepository {} + +export default MongoUserRepository; diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index 129ed1e59..e409c9f98 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -1,4 +1,5 @@ import { IMonitorsRepository } from "@/repositories/index.js"; +import type { User } from "@/types/index.js"; const SERVICE_NAME = "userService"; @@ -62,12 +63,12 @@ class UserService { return this.jwt.sign(payloadData, tokenSecret, { expiresIn: tokenTTL }); }; - registerUser = async (user: any, file: any) => { + registerUser = async (user: Partial, inviteToken: string, file: any) => { // Create a new user // If superAdmin exists, a token should be attached to all further register requests const superAdminExists = await this.db.userModule.checkSuperadmin(); if (superAdminExists) { - const invitedUser = await this.db.inviteModule.getInviteTokenAndDelete(user.inviteToken); + const invitedUser = await this.db.inviteModule.getInviteTokenAndDelete(inviteToken); user.role = invitedUser.role; user.teamId = invitedUser.teamId; } else { diff --git a/server/src/types/index.ts b/server/src/types/index.ts index 4f71788f2..b6495497d 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -3,3 +3,4 @@ export * from "@/types/monitor.js"; export * from "@/types/monitorStats.js"; export * from "@/types/statusPage.js"; export * from "@/types/network.js"; +export * from "@/types/user.js"; diff --git a/server/src/types/user.ts b/server/src/types/user.ts new file mode 100644 index 000000000..41c009791 --- /dev/null +++ b/server/src/types/user.ts @@ -0,0 +1,24 @@ +export const UserRoles = ["user", "admin", "superadmin", "demo"] as const; +export type UserRole = (typeof UserRoles)[number]; + +export interface UserProfileImage { + data?: Buffer; + contentType?: string; +} + +export interface User { + id: string; + firstName: string; + lastName: string; + email: string; + password: string; + avatarImage?: string; + profileImage?: UserProfileImage; + isActive: boolean; + isVerified: boolean; + role: UserRole[]; + teamId: string; + checkTTL?: number; + createdAt: string; + updatedAt: string; +} From 5c52a208b9d33676cbcead52bdb8f4339c17abd3 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 16 Jan 2026 14:44:28 -0500 Subject: [PATCH 251/400] Fix build errors and formatting - Replace CheckIcon SVG import with Lucide Icon component in LatestIncidentsPanel/index.jsx - Format IncidentItem.jsx and StatisticsPanel.jsx with Prettier --- CLAUDE.md | 145 ++++++++++++++++++ .../LatestIncidentsPanel/IncidentItem.jsx | 5 +- .../Components/LatestIncidentsPanel/index.jsx | 21 +-- .../Components/StatisticsPanel/index.jsx | 15 +- 4 files changed, 169 insertions(+), 17 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 000000000..1535e84cf --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,145 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Checkmate is an open-source uptime and infrastructure monitoring application. It monitors server hardware, uptime, response times, and incidents with real-time alerts. The companion agent [Capture](https://github.com/bluewave-labs/capture) provides infrastructure metrics (CPU, RAM, disk, temperature). + +## Development Commands + +### Client (React/Vite) +```bash +cd client +npm install +npm run dev # Start dev server at http://localhost:5173 +npm run build # TypeScript check + production build +npm run lint # ESLint (strict, max-warnings 0) +npm run format # Prettier formatting +npm run format-check # Check formatting +``` + +### Server (Node.js/Express) +```bash +cd server +npm install +npm run dev # Start with hot-reload (nodemon + tsx) at http://localhost:52345 +npm run build # TypeScript compile + path alias resolution +npm run test # Run Mocha tests with c8 coverage +npm run lint # ESLint v9 +npm run lint-fix # Auto-fix lint issues +npm run format # Prettier formatting +``` + +### Docker Development +```bash +cd docker/dev +./build_images.sh +docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:6.0 +``` + +## Environment Setup + +### Server `.env` (minimum required) +```env +CLIENT_HOST="http://localhost:5173" +JWT_SECRET="my_secret_key_change_this" +DB_CONNECTION_STRING="mongodb://localhost:27017/uptime_db" +TOKEN_TTL="99d" +ORIGIN="localhost" +LOG_LEVEL="debug" +``` + +### Client `.env` +```env +VITE_APP_API_BASE_URL="http://localhost:52345/api/v1" +VITE_APP_LOG_LEVEL="debug" +``` + +## Architecture + +### Monorepo Structure +- `/client` - React 18 + TypeScript + Vite + MUI frontend +- `/server` - Node.js 20+ + Express + TypeScript backend +- `/docker` - Multi-environment Docker configs (dev, staging, prod, arm, mono) + +### Backend Layers +``` +server/src/ +├── controllers/ # Route handlers (authController, monitorController, etc.) +├── service/ # Business logic +│ ├── business/ # Core monitoring logic +│ ├── infrastructure/ # Server/system utilities +│ └── system/ # App-level settings +├── db/ +│ ├── models/ # Mongoose schemas (Monitor, Check, Incident, User, etc.) +│ ├── migration/ # Database migrations (run on startup) +│ └── modules/ # Database-specific modules +├── middleware/v1/ # verifyJWT, rateLimiter, sanitization, responseHandler +├── routes/v1/ # API route definitions +├── validation/ # Joi input validation schemas +└── repositories/ # Data access layer +``` + +### Frontend Structure +``` +client/src/ +├── Components/ # Reusable UI components +├── Pages/ # Page components (Auth, Uptime, Infrastructure, Incidents, etc.) +├── Features/ # Redux slices (Auth, UI) +├── Hooks/ # Custom React hooks +├── Utils/ # Utilities (NetworkService.js is main API client) +├── Validation/ # Input validation +└── locales/ # i18n translations +``` + +### API +- Base URL: `/api/v1` +- Documentation: `http://localhost:52345/api-docs` (Swagger UI) +- OpenAPI spec: `/server/openapi.json` + +### Key Technologies +- **State Management**: Redux Toolkit + Redux-Persist +- **Data Fetching**: SWR + Axios +- **Database**: MongoDB with Mongoose ODM +- **Queue/Cache**: Redis + BullMQ + Pulse (cron scheduling) +- **i18n**: i18next + react-i18next (translations via PoEditor) + +## Code Conventions + +### Internationalization +All user-facing strings must use the translation function: +```javascript +t('your.key') // Never hardcode UI strings +``` + +### Branching +- Always branch from `develop` (not master) +- Use descriptive names: `feat/add-alerts`, `fix/login-error` +- PRs target `develop` branch + +### Formatting +- **Client**: Prettier with `printWidth: 90`, tabs, double quotes +- **Server**: Prettier with `printWidth: 150`, tabs, double quotes +- Both use ESLint with strict settings + +### Testing +Server tests use Mocha + Chai + Sinon: +```bash +npm test # Run all tests with coverage +npm test -- --grep "pattern" # Run specific tests +``` +Test files: `server/tests/**/*.test.js` + +## Database Models + +Key Mongoose models in `/server/src/db/models/`: +- **Monitor** - Monitoring configuration (website, infrastructure, port, etc.) +- **Check** - Individual monitoring check results +- **Incident** - Downtime incidents +- **User** - User accounts +- **Team** - Team/workspace management +- **StatusPage** - Public status pages +- **Notification** - Alert configuration (email, Discord, Slack, webhooks) +- **MaintenanceWindow** - Scheduled maintenance periods +- **AppSettings** - Global application settings diff --git a/client/src/Pages/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx b/client/src/Pages/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx index 67254f548..0ecb34791 100644 --- a/client/src/Pages/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx +++ b/client/src/Pages/Incidents2/Components/LatestIncidentsPanel/IncidentItem.jsx @@ -67,7 +67,10 @@ const IncidentItem = ({ incident }) => { gap={theme.spacing(2)} > - + - - + + { sx={rowStyle} > - + { sx={rowStyle} > - + { sx={rowStyle} > - + Date: Fri, 16 Jan 2026 20:29:54 +0000 Subject: [PATCH 252/400] update user refactor --- server/src/config/services.ts | 17 ++- server/src/controllers/authController.ts | 1 + server/src/db/models/Invite.ts | 30 +++++ server/src/db/models/InviteToken.js | 35 ----- server/src/db/models/index.ts | 3 + server/src/repositories/index.ts | 7 +- .../invites/IInvitesRepository.ts | 11 ++ .../invites/MongoInviteRepository.ts | 45 +++++++ .../src/repositories/users/IUserRepository.ts | 9 -- .../repositories/users/IUsersRepository.ts | 12 ++ .../repositories/users/MongoUserRepository.ts | 4 - .../users/MongoUsersRepository.ts | 125 ++++++++++++++++++ server/src/service/business/userService.ts | 56 +++++--- server/src/types/index.ts | 1 + server/src/types/invite.ts | 12 ++ server/src/utils/utils.js | 5 - 16 files changed, 293 insertions(+), 80 deletions(-) create mode 100755 server/src/db/models/Invite.ts delete mode 100755 server/src/db/models/InviteToken.js create mode 100644 server/src/repositories/invites/IInvitesRepository.ts create mode 100644 server/src/repositories/invites/MongoInviteRepository.ts delete mode 100644 server/src/repositories/users/IUserRepository.ts create mode 100644 server/src/repositories/users/IUsersRepository.ts delete mode 100644 server/src/repositories/users/MongoUserRepository.ts create mode 100644 server/src/repositories/users/MongoUsersRepository.ts create mode 100644 server/src/types/invite.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index af49cc2d4..66fb18f47 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -37,9 +37,6 @@ import crypto from "crypto"; import { games, GameDig } from "gamedig"; import jmespath from "jmespath"; -import { fileURLToPath } from "url"; -import { ObjectId } from "mongodb"; - // DB Modules import { NormalizeData, NormalizeDataUptimeDetails } from "../utils/dataUtils.js"; import { GenerateAvatarImage } from "../utils/imageProcessing.js"; @@ -48,7 +45,7 @@ import { ParseBoolean } from "../utils/utils.js"; // Models import Monitor from "../db/models/Monitor.js"; import User from "../db/models/User.js"; -import InviteToken from "../db/models/InviteToken.js"; +import InviteToken from "../db/models/Invite.js"; import StatusPage from "../db/models/StatusPage.js"; import Team from "../db/models/Team.js"; import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; @@ -74,10 +71,14 @@ import { MongoChecksRepository, MongoMonitorStatsRepository, MongoStatusPagesRepository, + MongoUsersRepository, + MongoInvitesRepository, IMonitorsRepository, IChecksRepository, IMonitorStatsRepository, IStatusPagesRepository, + IUsersRepository, + IInvitesRepository, } from "@/repositories/index.js"; export type InitializedSerivces = { @@ -107,6 +108,8 @@ export type InitializedSerivces = { checksRepository: IChecksRepository; monitorStatsRepository: IMonitorStatsRepository; statusPagesRepository: IStatusPagesRepository; + usersRepository: IUsersRepository; + invitesRepository: IInvitesRepository; }; export const initializeServices = async ({ @@ -158,6 +161,8 @@ export const initializeServices = async ({ const checksRepository = new MongoChecksRepository(logger); const monitorStatsRepository = new MongoMonitorStatsRepository(); const statusPagesRepository = new MongoStatusPagesRepository(); + const usersRepository = new MongoUsersRepository(); + const invitesRepository = new MongoInvitesRepository(); const networkService = new NetworkService({ axios, @@ -240,6 +245,8 @@ export const initializeServices = async ({ errorService, jobQueue: superSimpleQueue, monitorsRepository, + usersRepository, + invitesRepository, }); const diagnosticService = new DiagnosticService(); @@ -298,6 +305,8 @@ export const initializeServices = async ({ checksRepository, monitorStatsRepository, statusPagesRepository, + usersRepository, + invitesRepository, }; Object.values(services).forEach((service) => { diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts index 435813a36..1b76b249e 100755 --- a/server/src/controllers/authController.ts +++ b/server/src/controllers/authController.ts @@ -38,6 +38,7 @@ class AuthController { newUser.email = newUser.email.toLowerCase(); } await registrationBodyValidation.validateAsync(newUser); + const { user, token } = await this.userService.registerUser(newUser, newUserToken, req.file); res.status(200).json({ success: true, diff --git a/server/src/db/models/Invite.ts b/server/src/db/models/Invite.ts new file mode 100755 index 000000000..25ddd9a72 --- /dev/null +++ b/server/src/db/models/Invite.ts @@ -0,0 +1,30 @@ +import { Schema, model, type Types } from "mongoose"; +import type { Invite } from "@/types/invite.js"; + +type InviteDocumentBase = Omit & { + teamId: Types.ObjectId; + expiry: Date; +}; + +interface InviteDocument extends InviteDocumentBase { + _id: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const InviteSchema = new Schema( + { + email: { type: String, required: true, unique: true }, + teamId: { type: Schema.Types.ObjectId, ref: "Team", immutable: true, required: true }, + role: { type: [String], required: true, default: ["user"] }, + token: { type: String, required: true }, + expiry: { type: Date, default: Date.now, expires: 3600 }, + }, + { timestamps: true } +); + +const InviteModel = model("Invite", InviteSchema); + +export type { InviteDocument }; +export { InviteModel }; +export default InviteModel; diff --git a/server/src/db/models/InviteToken.js b/server/src/db/models/InviteToken.js deleted file mode 100755 index 079600647..000000000 --- a/server/src/db/models/InviteToken.js +++ /dev/null @@ -1,35 +0,0 @@ -import mongoose from "mongoose"; -const InviteTokenSchema = mongoose.Schema( - { - email: { - type: String, - required: true, - unique: true, - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - required: true, - }, - role: { - type: Array, - required: true, - default: ["user"], - }, - token: { - type: String, - required: true, - }, - expiry: { - type: Date, - default: Date.now, - expires: 3600, - }, - }, - { - timestamps: true, - } -); - -export default mongoose.model("InviteToken", InviteTokenSchema); diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index d91aa0778..d378c6750 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -12,3 +12,6 @@ export { default as StatusPageModel } from "@/db/models/StatusPage.js"; export * from "@/db/models/User.js"; export { default as UserModel } from "@/db/models/User.js"; + +export * from "@/db/models/Invite.js"; +export { default as InviteModel } from "@/db/models/Invite.js"; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 92d9b2371..7afe3a9f4 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -10,5 +10,8 @@ export { default as MongoMonitorStatsRepository } from "@/repositories/monitor-s export * from "@/repositories/status-pages/IStatusPagesRepository.js"; export { default as MongoStatusPagesRepository } from "@/repositories/status-pages/MongoStatusPagesRepository.js"; -export * from "@/repositories/users/IUserRepository.js"; -export { default as MongoUserRepository } from "@/repositories/users/MongoUserRepository.js"; +export * from "@/repositories/users/IUsersRepository.js"; +export { default as MongoUsersRepository } from "@/repositories/users/MongoUsersRepository.js"; + +export * from "@/repositories/invites/IInvitesRepository.js"; +export { default as MongoInvitesRepository } from "@/repositories/invites/MongoInviteRepository.js"; diff --git a/server/src/repositories/invites/IInvitesRepository.ts b/server/src/repositories/invites/IInvitesRepository.ts new file mode 100644 index 000000000..066a51a0f --- /dev/null +++ b/server/src/repositories/invites/IInvitesRepository.ts @@ -0,0 +1,11 @@ +import type { Invite } from "@/types/index.js"; + +export interface IInvitesRepository { + // create + // fetch + findByTokenAndDelete(token: string): Promise; + // update + + // delete + // other +} diff --git a/server/src/repositories/invites/MongoInviteRepository.ts b/server/src/repositories/invites/MongoInviteRepository.ts new file mode 100644 index 000000000..16e56440b --- /dev/null +++ b/server/src/repositories/invites/MongoInviteRepository.ts @@ -0,0 +1,45 @@ +import { IInvitesRepository } from "@/repositories/index.js"; +import type { Invite } from "@/types/index.js"; +import { type InviteDocument, InviteModel } from "@/db/models/index.js"; +import { AppError } from "@/utils/AppError.js"; +import mongoose from "mongoose"; + +class MongoInvitesRepository implements IInvitesRepository { + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + protected toEntity = (doc: InviteDocument): Invite => { + return { + id: this.toStringId(doc._id), + email: doc.email, + teamId: this.toStringId(doc.teamId), + role: doc.role ?? [], + token: doc.token, + expiry: this.toDateString(doc.expiry), + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + findByTokenAndDelete = async (token: string) => { + const invite = await InviteModel.findOneAndDelete({ + token, + }); + if (invite === null) { + throw new AppError({ message: "Invite not found", status: 404 }); + } + return this.toEntity(invite); + }; +} +export default MongoInvitesRepository; diff --git a/server/src/repositories/users/IUserRepository.ts b/server/src/repositories/users/IUserRepository.ts deleted file mode 100644 index 259ba781b..000000000 --- a/server/src/repositories/users/IUserRepository.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type { User } from "@/types/index.js"; - -export interface IUsersRepository { - // create - // fetch - // update - // delete - // other -} diff --git a/server/src/repositories/users/IUsersRepository.ts b/server/src/repositories/users/IUsersRepository.ts new file mode 100644 index 000000000..9678de47a --- /dev/null +++ b/server/src/repositories/users/IUsersRepository.ts @@ -0,0 +1,12 @@ +import type { User } from "@/types/index.js"; +export interface IUsersRepository { + // create + create(user: Partial, imageFile?: Express.Multer.File | null): Promise; + // fetch + findByEmail(email: string): Promise; + // update + updateById(id: string, patch: Partial, file?: Express.Multer.File | null): Promise; + // delete + // other + findSuperAdmin(): Promise; +} diff --git a/server/src/repositories/users/MongoUserRepository.ts b/server/src/repositories/users/MongoUserRepository.ts deleted file mode 100644 index 82c5eb5b6..000000000 --- a/server/src/repositories/users/MongoUserRepository.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { IUsersRepository } from "@/repositories/index.js"; -class MongoUserRepository implements IUsersRepository {} - -export default MongoUserRepository; diff --git a/server/src/repositories/users/MongoUsersRepository.ts b/server/src/repositories/users/MongoUsersRepository.ts new file mode 100644 index 000000000..b2687ff4a --- /dev/null +++ b/server/src/repositories/users/MongoUsersRepository.ts @@ -0,0 +1,125 @@ +import mongoose from "mongoose"; +import { IUsersRepository } from "@/repositories/index.js"; +import { UserModel, type UserDocument } from "@/db/models/index.js"; +import type { User, UserProfileImage } from "@/types/index.js"; +import { GenerateAvatarImage } from "@/utils/imageProcessing.js"; +import { ParseBoolean } from "@/utils/utils.js"; +import { AppError } from "@/utils/AppError.js"; +const SERVICE_NAME = "MongoUsersRepository"; + +class MongoUsersRepository implements IUsersRepository { + static SERVICE_NAME = SERVICE_NAME; + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + private mapProfileImage = (image?: (UserProfileImage & { data?: Buffer }) | null) => { + if (!image) { + return undefined; + } + return { + data: image.data, + contentType: image.contentType, + }; + }; + + protected toEntity = (doc: UserDocument): User => { + return { + id: this.toStringId(doc._id), + firstName: doc.firstName, + lastName: doc.lastName, + email: doc.email, + password: doc.password, + avatarImage: doc.avatarImage ?? undefined, + profileImage: this.mapProfileImage(doc.profileImage), + isActive: doc.isActive ?? false, + isVerified: doc.isVerified ?? false, + role: doc.role ?? [], + teamId: this.toStringId(doc.teamId), + checkTTL: doc.checkTTL ?? undefined, + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + create = async (user: Partial, imageFile: Express.Multer.File | null) => { + if (imageFile) { + // 1. Save the full size image + user.profileImage = { + data: imageFile.buffer, + contentType: imageFile.mimetype, + }; + + // 2. Get the avatar sized image + const avatar = await GenerateAvatarImage(imageFile); + user.avatarImage = avatar; + } + + const newUser = new UserModel(user); + await newUser.save(); + const sanitizedUser = await UserModel.findOne({ _id: newUser._id }).select("-password").select("-profileImage"); + if (!sanitizedUser) { + throw new AppError({ message: "Failed to create user", service: SERVICE_NAME, status: 500 }); + } + return this.toEntity(sanitizedUser); + }; + + findByEmail = async (email: string) => { + const user = await UserModel.findOne({ email: email }).select("-profileImage"); + if (!user) { + throw new AppError({ message: "User not found", service: SERVICE_NAME, status: 404 }); + } + return this.toEntity(user); + }; + + updateById = async (id: string, patch: Partial, file?: Express.Multer.File | null): Promise => { + const candidateUser = { ...patch }; + + if (ParseBoolean(candidateUser.deleteProfileImage) === true) { + candidateUser.profileImage = undefined; + candidateUser.avatarImage = undefined; + } else if (file) { + // 1. Save the full size image + candidateUser.profileImage = { + data: file.buffer, + contentType: file.mimetype, + }; + + // 2. Get the avatar sized image + const avatar = await GenerateAvatarImage(file); + candidateUser.avatarImage = avatar; + } + + const updatedUser = await UserModel.findOneAndUpdate( + { _id: id }, + candidateUser, + { new: true } // Returns updated user instead of pre-update user + ) + .select("-password") + .select("-profileImage"); + if (!updatedUser) { + throw new AppError({ message: "User not found", service: SERVICE_NAME, status: 404 }); + } + return this.toEntity(updatedUser); + }; + + findSuperAdmin = async () => { + const superAdmin = await UserModel.findOne({ role: "superadmin" }); + if (superAdmin !== null) { + return true; + } + return false; + }; +} + +export default MongoUsersRepository; diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index e409c9f98..dca362ebe 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -1,5 +1,7 @@ -import { IMonitorsRepository } from "@/repositories/index.js"; +import { IInvitesRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js"; +import Team from "@/db/models/Team.js"; import type { User } from "@/types/index.js"; +import bcrypt from "bcryptjs"; const SERVICE_NAME = "userService"; @@ -16,6 +18,8 @@ class UserService { private jobQueue: any; private crypto: any; private monitorsRepository: IMonitorsRepository; + private usersRepository: IUsersRepository; + private invitesRepository: IInvitesRepository; constructor({ crypto, @@ -28,6 +32,8 @@ class UserService { errorService, jobQueue, monitorsRepository, + usersRepository, + invitesRepository, }: { crypto: any; db: any; @@ -39,6 +45,8 @@ class UserService { errorService: any; jobQueue: any; monitorsRepository: IMonitorsRepository; + usersRepository: IUsersRepository; + invitesRepository: IInvitesRepository; }) { this.db = db; this.emailService = emailService; @@ -50,6 +58,8 @@ class UserService { this.jobQueue = jobQueue; this.crypto = crypto; this.monitorsRepository = monitorsRepository; + this.usersRepository = usersRepository; + this.invitesRepository = invitesRepository; } get serviceName() { @@ -66,33 +76,37 @@ class UserService { registerUser = async (user: Partial, inviteToken: string, file: any) => { // Create a new user // If superAdmin exists, a token should be attached to all further register requests - const superAdminExists = await this.db.userModule.checkSuperadmin(); + const superAdminExists = await this.usersRepository.findSuperAdmin(); if (superAdminExists) { - const invitedUser = await this.db.inviteModule.getInviteTokenAndDelete(inviteToken); - user.role = invitedUser.role; - user.teamId = invitedUser.teamId; + const invite = await this.invitesRepository.findByTokenAndDelete(inviteToken); + user.role = invite.role; + user.teamId = invite.teamId; } else { // This is the first account, create JWT secret to use if one is not supplied by env const jwtSecret = this.crypto.randomBytes(64).toString("hex"); await this.db.settingsModule.updateAppSettings({ jwtSecret }); + // Create a new team + const team = new Team({ + email: user.email, + }); + user.teamId = team._id; } - const newUser = await this.db.userModule.insertUser({ ...user }, file); + const newUser = await this.usersRepository.create({ ...user }, file); this.logger.debug({ message: "New user created", service: SERVICE_NAME, method: "registerUser", - details: newUser._id, + details: newUser.id, }); - const userForToken = { ...newUser._doc }; - delete userForToken.profileImage; - delete userForToken.avatarImage; + delete newUser.profileImage; + delete newUser.avatarImage; const appSettings = await this.settingsService.getSettings(); - const token = this.issueToken(userForToken, appSettings); + const token = this.issueToken(newUser, appSettings); try { const html = await this.emailService.buildEmail("welcomeEmailTemplate", { @@ -120,17 +134,18 @@ class UserService { loginUser = async (email: string, password: string) => { // Check if user exists - const user = await this.db.userModule.getUserByEmail(email); + const user = await this.usersRepository.findByEmail(email); // Compare password - const match = await user.comparePassword(password); + const match = await bcrypt.compare(password, user.password); + if (match !== true) { throw this.errorService.createAuthenticationError(this.stringService.authIncorrectPassword); } // Remove password from user object. Should this be abstracted to DB layer? - const userWithoutPassword = { ...user._doc }; - delete userWithoutPassword.password; - delete userWithoutPassword.avatarImage; + const userWithoutPassword = { ...user }; + userWithoutPassword.password = ""; + userWithoutPassword.avatarImage = ""; // Happy path, return token const appSettings = await this.settingsService.getSettings(); @@ -140,16 +155,16 @@ class UserService { return { user: userWithoutPassword, token }; }; - editUser = async (updates: any, file: any, currentUser: any) => { + editUser = async (updates: Partial, file: any, currentUser: any) => { // Change Password check if (updates?.password && updates?.newPassword) { // Get user's email // Add user email to body for DB operation updates.email = currentUser.email; // Get user - const user = await this.db.userModule.getUserByEmail(currentUser.email); + const user = await this.usersRepository.findByEmail(currentUser.email); // Compare passwords - const match = await user.comparePassword(updates?.password); + const match = await bcrypt.compare(updates?.password, user.password); // If not a match, throw a 403 // 403 instead of 401 to avoid triggering axios interceptor if (!match) { @@ -159,8 +174,7 @@ class UserService { updates.password = updates.newPassword; } - const updatedUser = await this.db.userModule.updateUser({ userId: currentUser?._id, user: updates, file: file }); - return updatedUser; + return await this.usersRepository.updateById(currentUser.id, updates, file); }; checkSuperadminExists = async () => { diff --git a/server/src/types/index.ts b/server/src/types/index.ts index b6495497d..9416de9fa 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -4,3 +4,4 @@ export * from "@/types/monitorStats.js"; export * from "@/types/statusPage.js"; export * from "@/types/network.js"; export * from "@/types/user.js"; +export * from "@/types/invite.js"; diff --git a/server/src/types/invite.ts b/server/src/types/invite.ts new file mode 100644 index 000000000..8e65d54e9 --- /dev/null +++ b/server/src/types/invite.ts @@ -0,0 +1,12 @@ +import { UserRole } from "./user.js"; + +export interface Invite { + id: string; + email: string; + teamId: string; + role: UserRole[]; + token: string; + expiry: string; + createdAt: string; + updatedAt: string; +} diff --git a/server/src/utils/utils.js b/server/src/utils/utils.js index bcb4d8e5e..ee4ee1c6d 100755 --- a/server/src/utils/utils.js +++ b/server/src/utils/utils.js @@ -1,8 +1,3 @@ -/** - * Converts a request body parameter to a boolean. - * @param {string | boolean} value - * @returns {boolean} - */ const ParseBoolean = (value) => { if (value === true || value === "true") { return true; From 6447c315ce28175cef7698b880c440878c1aa138 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 21:04:41 +0000 Subject: [PATCH 253/400] stats fixes --- client/src/Hooks/monitorHooks.js | 6 ++++-- .../Components/PageSpeedStatusBoxes/index.jsx | 20 +++++++++++++++---- client/src/Pages/PageSpeed/Details/index.jsx | 4 ++-- .../Components/UptimeStatusBoxes/index.jsx | 5 ++++- server/src/controllers/monitorController.ts | 4 ++-- server/src/service/business/monitorService.ts | 10 ++++++++-- 6 files changed, 36 insertions(+), 13 deletions(-) diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index d865ffab8..997eae249 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -243,6 +243,7 @@ export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigg const [isLoading, setIsLoading] = useState(true); const [networkError, setNetworkError] = useState(false); const [monitor, setMonitor] = useState(undefined); + const [monitorStats, setMonitorStats] = useState(undefined); useEffect(() => { const fetchMonitor = async () => { @@ -254,7 +255,8 @@ export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigg monitorId: monitorId, dateRange: dateRange, }); - setMonitor(response.data.data); + setMonitor(response.data.data.monitor); + setMonitorStats(response.data.data.monitorStats); } catch (error) { setNetworkError(true); } finally { @@ -263,7 +265,7 @@ export const useFetchPageSpeedMonitorById = ({ monitorId, dateRange, updateTrigg }; fetchMonitor(); }, [monitorId, dateRange, updateTrigger]); - return [monitor, isLoading, networkError]; + return [monitor, monitorStats, isLoading, networkError]; }; export const useFetchUptimeMonitorById = ({ monitorId, dateRange, trigger }) => { diff --git a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx b/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx index 493d1ece6..872b1e293 100644 --- a/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx +++ b/client/src/Pages/PageSpeed/Details/Components/PageSpeedStatusBoxes/index.jsx @@ -4,9 +4,21 @@ import { Typography } from "@mui/material"; import { getHumanReadableDuration } from "../../../../../Utils/timeUtils.js"; import { useTranslation } from "react-i18next"; -const PageSpeedStatusBoxes = ({ shouldRender, monitor }) => { - const uptimeDuration = getHumanReadableDuration(monitor?.uptimeDuration); - const time = getHumanReadableDuration(monitor?.lastChecked); +const PageSpeedStatusBoxes = ({ shouldRender, monitorStats }) => { + const lastChecked = monitorStats?.lastCheckTimestamp + ? Date.now() - monitorStats.lastCheckTimestamp + : 0; + + // Determine time since last failure + const timeOfLastFailure = monitorStats?.timeOfLastFailure; + const timeSinceLastFailure = + timeOfLastFailure > 0 + ? Date.now() - timeOfLastFailure + : Date.now() - new Date(monitorStats?.createdAt); + + const streakTime = getHumanReadableDuration(timeSinceLastFailure); + + const time = getHumanReadableDuration(lastChecked); const { t } = useTranslation(); @@ -16,7 +28,7 @@ const PageSpeedStatusBoxes = ({ shouldRender, monitor }) => { heading="checks since" subHeading={ <> - {uptimeDuration} + {streakTime} {t("ago")} } diff --git a/client/src/Pages/PageSpeed/Details/index.jsx b/client/src/Pages/PageSpeed/Details/index.jsx index df411048a..3190787b3 100644 --- a/client/src/Pages/PageSpeed/Details/index.jsx +++ b/client/src/Pages/PageSpeed/Details/index.jsx @@ -35,7 +35,7 @@ const PageSpeedDetails = () => { }); const [trigger, setTrigger] = useState(false); // Network - const [monitor, isLoading, networkError] = useFetchPageSpeedMonitorById({ + const [monitor, monitorStats, isLoading, networkError] = useFetchPageSpeedMonitorById({ monitorId, dateRange: "day", updateTrigger: trigger, @@ -95,7 +95,7 @@ const PageSpeedDetails = () => { /> 0 ? Date.now() - timeOfLastFailure : 0; + const timeSinceLastFailure = + timeOfLastFailure > 0 + ? Date.now() - timeOfLastFailure + : Date.now() - new Date(monitorStats?.createdAt); // Determine time since last check const timeOfLastCheck = monitorStats?.lastCheckTimestamp; diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index d46845026..32c4310f3 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -124,7 +124,7 @@ class MonitorController { const dateRange = requireString(req?.query?.dateRange, "dateRange"); const teamId = requireTeamId(req?.user?.teamId); - const monitor = await this.monitorService.getPageSpeedDetailsById({ + const data = await this.monitorService.getPageSpeedDetailsById({ teamId, monitorId, dateRange, @@ -133,7 +133,7 @@ class MonitorController { return res.status(200).json({ success: true, msg: "Page speed details retrieved successfully", - data: monitor, + data, }); } catch (error) { next(error); diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index ecdf8ca02..36f6b34d9 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -330,9 +330,15 @@ export class MonitorService implements IMonitorService { if (checksData.monitorType !== "pagespeed") { throw new AppError({ message: "Unable to load pagespeed stats for this monitor", status: 500 }); } + + const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id); + return { - ...monitor, - checks: checksData.checks, + monitor: { + ...monitor, + checks: checksData.checks, + }, + monitorStats, }; }; getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { From 11dcccc085c1da9c4c57ebe8d68c8c4f884ebf46 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 21:16:05 +0000 Subject: [PATCH 254/400] remove pagespeed from snapshot array, full check list required --- server/src/service/business/monitorService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 36f6b34d9..7c8ad199e 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -388,7 +388,7 @@ export class MonitorService implements IMonitorService { }); const monitorsList = (monitors ?? []) as Monitor[]; - const snapshotTypes: MonitorType[] = ["hardware", "pagespeed"]; + const snapshotTypes: MonitorType[] = ["hardware"]; const requestedTypes = Array.isArray(type) ? type : type ? [type] : []; const snapshotOnlyRequest = requestedTypes.length > 0 && requestedTypes.every((requestedType) => snapshotTypes.includes(requestedType as MonitorType)); From 9c92542653a0e040e1a771e20e777ff7f8ed9732 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 22:03:35 +0000 Subject: [PATCH 255/400] recovery --- server/src/config/services.ts | 6 ++ server/src/db/models/RecoveryToken.js | 25 -------- server/src/db/models/RecoveryToken.ts | 27 ++++++++ server/src/db/models/index.ts | 3 + server/src/repositories/index.ts | 3 + .../IRecoveryTokensRepository.ts | 12 ++++ .../MongoRecoveryTokensRepository.ts | 61 +++++++++++++++++++ server/src/service/business/userService.ts | 40 +++++++++--- .../service/infrastructure/emailService.js | 2 +- server/src/types/index.ts | 1 + server/src/types/invite.ts | 2 +- server/src/types/recoveryToken.ts | 8 +++ 12 files changed, 154 insertions(+), 36 deletions(-) delete mode 100755 server/src/db/models/RecoveryToken.js create mode 100644 server/src/db/models/RecoveryToken.ts create mode 100644 server/src/repositories/recovery-tokens/IRecoveryTokensRepository.ts create mode 100644 server/src/repositories/recovery-tokens/MongoRecoveryTokensRepository.ts create mode 100644 server/src/types/recoveryToken.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 66fb18f47..aef32440f 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -73,12 +73,14 @@ import { MongoStatusPagesRepository, MongoUsersRepository, MongoInvitesRepository, + MongoRecoveryTokensRepository, IMonitorsRepository, IChecksRepository, IMonitorStatsRepository, IStatusPagesRepository, IUsersRepository, IInvitesRepository, + IRecoveryTokensRepository, } from "@/repositories/index.js"; export type InitializedSerivces = { @@ -110,6 +112,7 @@ export type InitializedSerivces = { statusPagesRepository: IStatusPagesRepository; usersRepository: IUsersRepository; invitesRepository: IInvitesRepository; + recoveryTokensRepository: IRecoveryTokensRepository; }; export const initializeServices = async ({ @@ -163,6 +166,7 @@ export const initializeServices = async ({ const statusPagesRepository = new MongoStatusPagesRepository(); const usersRepository = new MongoUsersRepository(); const invitesRepository = new MongoInvitesRepository(); + const recoveryTokensRepository = new MongoRecoveryTokensRepository(); const networkService = new NetworkService({ axios, @@ -247,6 +251,7 @@ export const initializeServices = async ({ monitorsRepository, usersRepository, invitesRepository, + recoveryTokensRepository, }); const diagnosticService = new DiagnosticService(); @@ -307,6 +312,7 @@ export const initializeServices = async ({ statusPagesRepository, usersRepository, invitesRepository, + recoveryTokensRepository, }; Object.values(services).forEach((service) => { diff --git a/server/src/db/models/RecoveryToken.js b/server/src/db/models/RecoveryToken.js deleted file mode 100755 index 2219a4bca..000000000 --- a/server/src/db/models/RecoveryToken.js +++ /dev/null @@ -1,25 +0,0 @@ -import mongoose from "mongoose"; - -const RecoveryTokenSchema = mongoose.Schema( - { - email: { - type: String, - required: true, - unique: true, - }, - token: { - type: String, - required: true, - }, - expiry: { - type: Date, - default: Date.now, - expires: 600, - }, - }, - { - timestamps: true, - } -); - -export default mongoose.model("RecoveryToken", RecoveryTokenSchema); diff --git a/server/src/db/models/RecoveryToken.ts b/server/src/db/models/RecoveryToken.ts new file mode 100644 index 000000000..07b697d9e --- /dev/null +++ b/server/src/db/models/RecoveryToken.ts @@ -0,0 +1,27 @@ +import { Schema, model, type Types } from "mongoose"; +import type { RecoveryToken as RecoveryTokenEntity } from "@/types/recoveryToken.js"; + +type RecoveryTokenDocumentBase = Omit & { + expiry: Date; +}; + +interface RecoveryTokenDocument extends RecoveryTokenDocumentBase { + _id: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const RecoveryTokenSchema = new Schema( + { + email: { type: String, required: true, unique: true }, + token: { type: String, required: true }, + expiry: { type: Date, default: Date.now, expires: 600 }, + }, + { timestamps: true } +); + +const RecoveryTokenModel = model("RecoveryToken", RecoveryTokenSchema); + +export type { RecoveryTokenDocument }; +export { RecoveryTokenModel }; +export default RecoveryTokenModel; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index d378c6750..1e5024b1c 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -15,3 +15,6 @@ export { default as UserModel } from "@/db/models/User.js"; export * from "@/db/models/Invite.js"; export { default as InviteModel } from "@/db/models/Invite.js"; + +export * from "@/db/models/RecoveryToken.js"; +export { default as RecoveryTokenModel } from "@/db/models/RecoveryToken.js"; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 7afe3a9f4..7a3c08c81 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -15,3 +15,6 @@ export { default as MongoUsersRepository } from "@/repositories/users/MongoUsers export * from "@/repositories/invites/IInvitesRepository.js"; export { default as MongoInvitesRepository } from "@/repositories/invites/MongoInviteRepository.js"; + +export * from "@/repositories/recovery-tokens/IRecoveryTokensRepository.js"; +export { default as MongoRecoveryTokensRepository } from "@/repositories/recovery-tokens/MongoRecoveryTokensRepository.js"; diff --git a/server/src/repositories/recovery-tokens/IRecoveryTokensRepository.ts b/server/src/repositories/recovery-tokens/IRecoveryTokensRepository.ts new file mode 100644 index 000000000..9df1fa793 --- /dev/null +++ b/server/src/repositories/recovery-tokens/IRecoveryTokensRepository.ts @@ -0,0 +1,12 @@ +import type { RecoveryToken } from "@/types/recoveryToken.js"; + +export interface IRecoveryTokensRepository { + // create + create(email: string): Promise; + // fetch + findByToken(token: string): Promise; + // update + // delete + deleteManyByEmail(email: string): Promise; + // other +} diff --git a/server/src/repositories/recovery-tokens/MongoRecoveryTokensRepository.ts b/server/src/repositories/recovery-tokens/MongoRecoveryTokensRepository.ts new file mode 100644 index 000000000..4c9f9ebb6 --- /dev/null +++ b/server/src/repositories/recovery-tokens/MongoRecoveryTokensRepository.ts @@ -0,0 +1,61 @@ +import type { RecoveryToken } from "@/types/index.js"; +import type { IRecoveryTokensRepository } from "./IRecoveryTokensRepository.js"; +import type { RecoveryTokenDocument } from "@/db/models/RecoveryToken.js"; +import { RecoveryTokenModel } from "@/db/models/RecoveryToken.js"; +import mongoose from "mongoose"; +import crypto from "crypto"; +import { AppError } from "@/utils/AppError.js"; +const SERVICE_NAME = "MongoRecoveryTokensRepository"; + +class MongoRecoveryTokensRepository implements IRecoveryTokensRepository { + static SERVICE_NAME = SERVICE_NAME; + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + protected toEntity = (doc: RecoveryTokenDocument): RecoveryToken => { + return { + id: this.toStringId(doc._id), + email: doc.email, + token: doc.token, + expiry: this.toDateString(doc.expiry), + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + create = async (email: string): Promise => { + const token = await RecoveryTokenModel.create({ + email, + token: crypto.randomBytes(32).toString("hex"), + }); + return this.toEntity(token); + }; + + findByToken = async (token: string): Promise => { + const recoveryToken = await RecoveryTokenModel.findOne({ token }); + if (!recoveryToken) { + throw new AppError({ message: "Recovery token not found", service: SERVICE_NAME, status: 404 }); + } + return this.toEntity(recoveryToken); + }; + + deleteManyByEmail = async (email: string) => { + const result = await RecoveryTokenModel.deleteMany({ + email, + }); + return Promise.resolve(result.deletedCount || 0); + }; +} + +export default MongoRecoveryTokensRepository; diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index dca362ebe..efd599299 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -1,7 +1,8 @@ -import { IInvitesRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js"; +import { IInvitesRepository, IMonitorsRepository, IRecoveryTokensRepository, IUsersRepository } from "@/repositories/index.js"; import Team from "@/db/models/Team.js"; import type { User } from "@/types/index.js"; import bcrypt from "bcryptjs"; +import { AppError } from "@/utils/AppError.js"; const SERVICE_NAME = "userService"; @@ -20,6 +21,7 @@ class UserService { private monitorsRepository: IMonitorsRepository; private usersRepository: IUsersRepository; private invitesRepository: IInvitesRepository; + private recoveryTokensRepository: IRecoveryTokensRepository; constructor({ crypto, @@ -34,6 +36,7 @@ class UserService { monitorsRepository, usersRepository, invitesRepository, + recoveryTokensRepository, }: { crypto: any; db: any; @@ -47,6 +50,7 @@ class UserService { monitorsRepository: IMonitorsRepository; usersRepository: IUsersRepository; invitesRepository: IInvitesRepository; + recoveryTokensRepository: IRecoveryTokensRepository; }) { this.db = db; this.emailService = emailService; @@ -60,6 +64,7 @@ class UserService { this.monitorsRepository = monitorsRepository; this.usersRepository = usersRepository; this.invitesRepository = invitesRepository; + this.recoveryTokensRepository = recoveryTokensRepository; } get serviceName() { @@ -178,13 +183,15 @@ class UserService { }; checkSuperadminExists = async () => { - const superAdminExists = await this.db.userModule.checkSuperadmin(); - return superAdminExists; + return await this.usersRepository.findSuperAdmin(); }; requestRecovery = async (email: string) => { const user = await this.db.userModule.getUserByEmail(email); - const recoveryToken = await this.db.recoveryModule.requestRecoveryToken(email); + + // Delete existing tokens + await this.recoveryTokensRepository.deleteManyByEmail(email); + const recoveryToken = await this.recoveryTokensRepository.create(email); const name = user.firstName; const { clientHost } = this.settingsService.getSettings(); const url = `${clientHost}/set-new-password/${recoveryToken.token}`; @@ -199,14 +206,29 @@ class UserService { }; validateRecovery = async (recoveryToken: string) => { - await this.db.recoveryModule.validateRecoveryToken(recoveryToken); + // Throws if token not found, validating + await this.recoveryTokensRepository.findByToken(recoveryToken); }; resetPassword = async (password: string, recoveryToken: string) => { - const user = await this.db.recoveryModule.resetPassword(password, recoveryToken); - const appSettings = await this.settingsService.getSettings(); - const token = this.issueToken(user._doc, appSettings); - return { user, token }; + const existingToken = await this.recoveryTokensRepository.findByToken(recoveryToken); + const existingUser = await this.usersRepository.findByEmail(existingToken.email); + + const match = await bcrypt.compare(password, existingUser.password); + if (match === true) { + throw new AppError({ message: "New password cannot be same as old password", service: SERVICE_NAME, status: 400 }); + } + + existingUser.password = password; + await this.usersRepository.updateById(existingUser.id, existingUser, null); + await this.recoveryTokensRepository.deleteManyByEmail(existingUser.email); + + existingUser.password = ""; + existingUser.profileImage = undefined; + + const token = this.issueToken(existingUser, await this.settingsService.getSettings()); + + return { user: existingUser, token }; }; deleteUser = async (user: any) => { diff --git a/server/src/service/infrastructure/emailService.js b/server/src/service/infrastructure/emailService.js index ed661ff16..bc040d51a 100755 --- a/server/src/service/infrastructure/emailService.js +++ b/server/src/service/infrastructure/emailService.js @@ -46,7 +46,7 @@ class EmailService { */ this.loadTemplate = (templateName) => { try { - const templatePath = this.path.join(__dirname, `../../../templates/${templateName}.mjml`); + const templatePath = this.path.join(__dirname, `../../templates/${templateName}.mjml`); const templateContent = this.fs.readFileSync(templatePath, "utf8"); return this.compile(templateContent); } catch (error) { diff --git a/server/src/types/index.ts b/server/src/types/index.ts index 9416de9fa..1a43d4849 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -5,3 +5,4 @@ export * from "@/types/statusPage.js"; export * from "@/types/network.js"; export * from "@/types/user.js"; export * from "@/types/invite.js"; +export * from "@/types/recoveryToken.js"; diff --git a/server/src/types/invite.ts b/server/src/types/invite.ts index 8e65d54e9..cdbdb37e5 100644 --- a/server/src/types/invite.ts +++ b/server/src/types/invite.ts @@ -1,4 +1,4 @@ -import { UserRole } from "./user.js"; +import type { UserRole } from "@/types/user.js"; export interface Invite { id: string; diff --git a/server/src/types/recoveryToken.ts b/server/src/types/recoveryToken.ts new file mode 100644 index 000000000..488e1f4c1 --- /dev/null +++ b/server/src/types/recoveryToken.ts @@ -0,0 +1,8 @@ +export interface RecoveryToken { + id: string; + email: string; + token: string; + expiry: string; + createdAt: string; + updatedAt: string; +} From b47f3710fe6dac3abb27d2bf7e51ea2bad78009b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 16 Jan 2026 22:41:12 +0000 Subject: [PATCH 256/400] refactor user-service --- server/src/controllers/authController.ts | 7 +++-- server/src/controllers/incidentController.ts | 2 +- .../maintenanceWindowController.ts | 4 +-- server/src/controllers/monitorController.ts | 8 ++--- .../src/controllers/notificationController.ts | 2 +- .../src/controllers/statusPageController.ts | 6 ++-- .../v1/{verifyJWT.js => verifyJWT.ts} | 18 ++++-------- .../repositories/users/IUsersRepository.ts | 3 ++ .../users/MongoUsersRepository.ts | 29 +++++++++++++++++++ server/src/service/business/userService.ts | 24 ++++++++------- server/src/types/express.d.ts | 4 +-- 11 files changed, 69 insertions(+), 38 deletions(-) rename server/src/middleware/v1/{verifyJWT.js => verifyJWT.ts} (74%) diff --git a/server/src/controllers/authController.ts b/server/src/controllers/authController.ts index 1b76b249e..23ea6e42e 100755 --- a/server/src/controllers/authController.ts +++ b/server/src/controllers/authController.ts @@ -192,7 +192,8 @@ class AuthController { editUserById = async (req: Request, res: Response, next: NextFunction) => { try { const roles = req?.user?.role; - if (!roles.includes("superadmin")) { + + if (!roles || !roles.includes("superadmin")) { throw new AppError({ message: "Unauthorized", status: 403 }); } @@ -201,7 +202,7 @@ class AuthController { await editUserByIdParamValidation.validateAsync(req.params); // If this is superadmin self edit, allow "superadmin" role - if (userId === req.user._id) { + if (userId === req.user?.id) { await editSuperadminUserByIdBodyValidation.validateAsync(req.body); } else { await editUserByIdBodyValidation.validateAsync(req.body); @@ -217,7 +218,7 @@ class AuthController { editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => { try { const roles = req?.user?.role; - if (!roles.includes("superadmin")) { + if (!roles || !roles.includes("superadmin")) { throw new AppError({ message: "Unauthorized", status: 403 }); } diff --git a/server/src/controllers/incidentController.ts b/server/src/controllers/incidentController.ts index 8a56b5ef8..b17788d80 100644 --- a/server/src/controllers/incidentController.ts +++ b/server/src/controllers/incidentController.ts @@ -69,7 +69,7 @@ class IncidentController { try { const resolvedIncident = await this.incidentService.resolveIncidentManually({ incidentId: req?.params?.incidentId, - userId: req?.user?._id, + userId: req?.user?.id, teamId: req?.user?.teamId, comment: req?.body?.comment, }); diff --git a/server/src/controllers/maintenanceWindowController.ts b/server/src/controllers/maintenanceWindowController.ts index afeba9219..3d212769f 100644 --- a/server/src/controllers/maintenanceWindowController.ts +++ b/server/src/controllers/maintenanceWindowController.ts @@ -46,7 +46,7 @@ class MaintenanceWindowController { try { await getMaintenanceWindowByIdParamValidation.validateAsync(req.params); - const teamId = req.user.teamId; + const teamId = req.user?.teamId; if (!teamId) { throw new AppError({ message: "Team ID is required", status: 400 }); } @@ -130,7 +130,7 @@ class MaintenanceWindowController { await editMaintenanceWindowByIdParamValidation.validateAsync(req.params); await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body); - const teamId = req.user.teamId; + const teamId = req.user?.teamId; if (!teamId) { throw new AppError({ message: "Team ID is required", status: 400 }); } diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index d46845026..0b0184bae 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -164,7 +164,7 @@ class MonitorController { try { await createMonitorBodyValidation.validateAsync(req.body); - const userId = requireString(req?.user?._id, "User ID"); + const userId = requireString(req?.user?.id, "User ID"); const teamId = requireTeamId(req?.user?.teamId); const monitor = await this.monitorService.createMonitor(teamId, userId, req.body); @@ -193,7 +193,7 @@ class MonitorController { throw new AppError({ message: "File is empty", status: 400 }); } - const userId = requireString(req?.user?._id, "User ID"); + const userId = requireString(req?.user?.id, "User ID"); const teamId = requireTeamId(req?.user?.teamId); const fileData = req?.file?.buffer?.toString("utf-8"); @@ -286,9 +286,9 @@ class MonitorController { addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => { try { - const _id = requireString(req?.user?._id, "User ID"); + const id = requireString(req?.user?.id, "User ID"); const teamId = requireTeamId(req?.user?.teamId); - const demoMonitors = await this.monitorService.addDemoMonitors({ userId: _id, teamId }); + const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId }); return res.status(200).json({ success: true, diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index defae2fd3..6f4a7f164 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -53,7 +53,7 @@ class NotificationController { throw new AppError({ message: "Team ID is required", status: 400 }); } - const userId = req?.user?._id; + const userId = req?.user?.id; if (!userId) { throw new AppError({ message: "User ID is required", status: 400 }); } diff --git a/server/src/controllers/statusPageController.ts b/server/src/controllers/statusPageController.ts index 105beb230..b63e925c0 100644 --- a/server/src/controllers/statusPageController.ts +++ b/server/src/controllers/statusPageController.ts @@ -21,11 +21,11 @@ class StatusPageController { await createStatusPageBodyValidation.validateAsync(req.body); await imageValidation.validateAsync(req.file); - const { _id, teamId } = req.user; + const { id, teamId } = req.user ?? {}; const statusPage = await this.db.statusPageModule.createStatusPage({ statusPageData: req.body, image: req.file, - userId: _id, + userId: id, teamId, }); return res.status(200).json({ @@ -87,7 +87,7 @@ class StatusPageController { }; getStatusPagesByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = req.user.teamId; + const teamId = req.user?.teamId; const statusPages = await this.db.statusPageModule.getStatusPagesByTeamId(teamId); return res.status(200).json({ diff --git a/server/src/middleware/v1/verifyJWT.js b/server/src/middleware/v1/verifyJWT.ts similarity index 74% rename from server/src/middleware/v1/verifyJWT.js rename to server/src/middleware/v1/verifyJWT.ts index cf983a6c8..b3211d7b6 100755 --- a/server/src/middleware/v1/verifyJWT.js +++ b/server/src/middleware/v1/verifyJWT.ts @@ -1,3 +1,5 @@ +import { NextFunction, Request, Response } from "express"; + import jwt from "jsonwebtoken"; import ServiceRegistry from "../../service/system/serviceRegistry.js"; import SettingsService from "../../service/system/settingsService.js"; @@ -5,20 +7,12 @@ import StringService from "../../service/system/stringService.js"; const SERVICE_NAME = "verifyJWT"; const TOKEN_PREFIX = "Bearer "; -/** - * Verifies the JWT token - * @function - * @param {express.Request} req - * @param {express.Response} res - * @param {express.NextFunction} next - * @returns {express.Response} - */ -const verifyJWT = (req, res, next) => { +const verifyJWT = (req: Request, res: Response, next: NextFunction) => { const stringService = ServiceRegistry.get(StringService.SERVICE_NAME); const token = req.headers["authorization"]; // Make sure a token is provided if (!token) { - const error = new Error(stringService.noAuthToken); + const error: any = new Error(stringService.noAuthToken); error.status = 401; error.service = SERVICE_NAME; next(error); @@ -26,7 +20,7 @@ const verifyJWT = (req, res, next) => { } // Make sure it is properly formatted if (!token.startsWith(TOKEN_PREFIX)) { - const error = new Error(stringService.invalidAuthToken); // Instantiate a new Error object for improperly formatted token + const error: any = new Error(stringService.invalidAuthToken); // Instantiate a new Error object for improperly formatted token error.status = 401; error.service = SERVICE_NAME; error.method = "verifyJWT"; @@ -37,7 +31,7 @@ const verifyJWT = (req, res, next) => { const parsedToken = token.slice(TOKEN_PREFIX.length, token.length); // Verify the token's authenticity const { jwtSecret } = ServiceRegistry.get(SettingsService.SERVICE_NAME).getSettings(); - jwt.verify(parsedToken, jwtSecret, (err, decoded) => { + jwt.verify(parsedToken, jwtSecret, (err: any, decoded: any) => { if (err) { const errorMessage = err.name === "TokenExpiredError" ? stringService.expiredAuthToken : stringService.invalidAuthToken; err.details = { msg: errorMessage }; diff --git a/server/src/repositories/users/IUsersRepository.ts b/server/src/repositories/users/IUsersRepository.ts index 9678de47a..3fa094c54 100644 --- a/server/src/repositories/users/IUsersRepository.ts +++ b/server/src/repositories/users/IUsersRepository.ts @@ -4,9 +4,12 @@ export interface IUsersRepository { create(user: Partial, imageFile?: Express.Multer.File | null): Promise; // fetch findByEmail(email: string): Promise; + findById(id: string): Promise; + findAll(): Promise; // update updateById(id: string, patch: Partial, file?: Express.Multer.File | null): Promise; // delete + deleteById(id: string): Promise; // other findSuperAdmin(): Promise; } diff --git a/server/src/repositories/users/MongoUsersRepository.ts b/server/src/repositories/users/MongoUsersRepository.ts index b2687ff4a..023e8aefb 100644 --- a/server/src/repositories/users/MongoUsersRepository.ts +++ b/server/src/repositories/users/MongoUsersRepository.ts @@ -82,6 +82,20 @@ class MongoUsersRepository implements IUsersRepository { return this.toEntity(user); }; + findById = async (id: string) => { + const user = await UserModel.findById(id).select("-password").select("-profileImage"); + if (!user) { + throw new Error("User not found"); + } + + return this.toEntity(user); + }; + + findAll = async () => { + const users = await UserModel.find().select("-password").select("-profileImage"); + return this.mapDocuments(users); + }; + updateById = async (id: string, patch: Partial, file?: Express.Multer.File | null): Promise => { const candidateUser = { ...patch }; @@ -113,6 +127,14 @@ class MongoUsersRepository implements IUsersRepository { return this.toEntity(updatedUser); }; + deleteById = async (id: string) => { + const deletedUser = await UserModel.findByIdAndDelete(id); + if (!deletedUser) { + throw new AppError({ message: "User not found", service: SERVICE_NAME, status: 404 }); + } + return this.toEntity(deletedUser); + }; + findSuperAdmin = async () => { const superAdmin = await UserModel.findOne({ role: "superadmin" }); if (superAdmin !== null) { @@ -120,6 +142,13 @@ class MongoUsersRepository implements IUsersRepository { } return false; }; + + private mapDocuments = (documents: UserDocument[]): User[] => { + if (!documents?.length) { + return []; + } + return documents.map((doc) => this.toEntity(doc)); + }; } export default MongoUsersRepository; diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index efd599299..7d9e5811e 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -187,7 +187,7 @@ class UserService { }; requestRecovery = async (email: string) => { - const user = await this.db.userModule.getUserByEmail(email); + const user = await this.usersRepository.findByEmail(email); // Delete existing tokens await this.recoveryTokensRepository.deleteManyByEmail(email); @@ -231,14 +231,14 @@ class UserService { return { user: existingUser, token }; }; - deleteUser = async (user: any) => { + deleteUser = async (user: User) => { const email = user?.email; if (!email) { throw this.errorService.createBadRequestError("No email in request"); } const teamId = user?.teamId; - const userId = user?._id; + const userId = user?.id; if (!teamId) { throw this.errorService.createBadRequestError("No team ID in request"); @@ -267,24 +267,28 @@ class UserService { )); } // 6. Delete the user by id - await this.db.userModule.deleteUser(userId); + await this.usersRepository.deleteById(userId); }; getAllUsers = async () => { - const users = await this.db.userModule.getAllUsers(); - return users; + return await this.usersRepository.findAll(); }; getUserById = async (roles: any, userId: any) => { - const user = await this.db.userModule.getUserById(roles, userId); + if (!roles.includes("superadmin")) { + throw new AppError({ message: "User is not a superadmin", service: SERVICE_NAME, status: 403 }); + } + const user = await this.usersRepository.findById(userId); + return user; }; - editUserById = async (userId: any, user: any) => { - await this.db.userModule.editUserById(userId, user); + editUserById = async (userId: any, patch: Partial) => { + await this.usersRepository.updateById(userId, patch, null); }; + setPasswordByUserId = async (userId: any, password: string) => { - const updatedUser = await this.db.userModule.updateUser({ userId: userId, user: { password: password }, file: null }); + const updatedUser = await this.usersRepository.updateById(userId, { password }, null); return updatedUser; }; } diff --git a/server/src/types/express.d.ts b/server/src/types/express.d.ts index b9f7386f6..66755659c 100644 --- a/server/src/types/express.d.ts +++ b/server/src/types/express.d.ts @@ -1,10 +1,10 @@ -import { ITokenizedUser } from "../db/models/index.ts"; +import type { User } from "@/types/index.js"; declare global { namespace Express { interface Request { file?: Multer.File; - user?: ITokenizedUser; + user?: User | undefined; resource?: any; } } From 3a77c6d7fad51e06a860328c7730ce62c0db989b Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 16 Jan 2026 22:26:43 -0500 Subject: [PATCH 257/400] Fix sidebar gap when scrolling on pages with tall content The sidebar was showing a visible gap at the bottom when scrolling on pages with tall tables (e.g., /checks). This occurred because the parent container's overflow-x: hidden was breaking sticky positioning. Changes: - Changed sidebar from position: sticky to position: fixed - Added a spacer Box element to maintain layout spacing - Moved border and background styles to the sidebar component - Cleaned up unused CSS selectors --- .../Components/v1/Layouts/HomeLayout/index.css | 10 +--------- .../Components/v1/Layouts/HomeLayout/index.jsx | 16 ++++++++++++++-- client/src/Components/v1/Sidebar/index.jsx | 7 +++++-- 3 files changed, 20 insertions(+), 13 deletions(-) diff --git a/client/src/Components/v1/Layouts/HomeLayout/index.css b/client/src/Components/v1/Layouts/HomeLayout/index.css index 953a2c682..61808aa57 100644 --- a/client/src/Components/v1/Layouts/HomeLayout/index.css +++ b/client/src/Components/v1/Layouts/HomeLayout/index.css @@ -14,15 +14,7 @@ } } */ -/* .home-layout aside { - position: sticky; - top: 0; - left: 0; - height: 100vh; - max-width: var(--env-var-side-bar-width); -} */ - -.home-layout > div { +.home-layout > .home-content-wrapper { min-height: calc(100vh - var(--env-var-spacing-2) * 2); flex: 1; } diff --git a/client/src/Components/v1/Layouts/HomeLayout/index.jsx b/client/src/Components/v1/Layouts/HomeLayout/index.jsx index 3e63aff37..825157333 100644 --- a/client/src/Components/v1/Layouts/HomeLayout/index.jsx +++ b/client/src/Components/v1/Layouts/HomeLayout/index.jsx @@ -1,17 +1,29 @@ import Sidebar from "../../Sidebar/index.jsx"; import { Outlet } from "react-router"; -import { Stack } from "@mui/material"; +import { Box, Stack } from "@mui/material"; +import { useSelector } from "react-redux"; import "./index.css"; const HomeLayout = () => { + const collapsed = useSelector((state) => state.ui.sidebar?.collapsed ?? false); + return ( + {/* Spacer for fixed sidebar */} + diff --git a/client/src/Components/v1/Sidebar/index.jsx b/client/src/Components/v1/Sidebar/index.jsx index b66f081fb..f39121987 100644 --- a/client/src/Components/v1/Sidebar/index.jsx +++ b/client/src/Components/v1/Sidebar/index.jsx @@ -81,14 +81,17 @@ const Sidebar = () => { : "var(--env-var-side-bar-width)" } component="aside" - position="sticky" + position="fixed" top={0} - borderRight={`1px solid ${theme.palette.primary.lowContrast}`} + left={0} paddingTop={theme.spacing(6)} paddingBottom={theme.spacing(6)} gap={theme.spacing(6)} sx={{ transition: "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)", + backgroundColor: "#000000", + borderRight: "1px solid #344054", + zIndex: 1000, }} > From a4cf53ee1549f510787d71d0233e99f517a19022 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 16 Jan 2026 22:52:11 -0500 Subject: [PATCH 258/400] Set html/body background to black and disable overscroll bounce - Added background-color: #000000 to prevent white flash on overscroll - Added overscroll-behavior: none to disable elastic bounce effect --- client/src/index.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/src/index.css b/client/src/index.css index d58a7e872..b29c17960 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -4,6 +4,12 @@ box-sizing: border-box; } +html, +body { + background-color: #000000; + overscroll-behavior: none; +} + html { scroll-behavior: smooth; } From e498bd60b17e9404cd51bc251d7b37eb73c2c06e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sat, 17 Jan 2026 05:41:52 +0000 Subject: [PATCH 259/400] team panel fix --- client/src/Pages/Account/components/TeamPanel.jsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/Pages/Account/components/TeamPanel.jsx b/client/src/Pages/Account/components/TeamPanel.jsx index 03a6145a5..bb40a5e4f 100644 --- a/client/src/Pages/Account/components/TeamPanel.jsx +++ b/client/src/Pages/Account/components/TeamPanel.jsx @@ -104,7 +104,7 @@ const TeamPanel = () => { team = team.map((member) => ({ ...member, - id: member._id, + id: member.id, role: member.role.map((role) => ROLE_MAP[role]).join(","), })); setData(team); @@ -268,6 +268,7 @@ const TeamPanel = () => { cursor: isSuperAdmin ? "pointer" : "default", }, onRowClick: (row) => { + console.log(row); if (isSuperAdmin) { navigate(`/account/team/${row.id}`); } From 048e53ea1556fe45434c8b52ab68e7583639b17b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sat, 17 Jan 2026 18:29:42 +0000 Subject: [PATCH 260/400] setting service --- server/src/db/models/AppSettings.js | 84 ------------------- server/src/db/models/AppSettings.ts | 49 +++++++++++ server/src/db/models/index.ts | 3 + server/src/repositories/index.ts | 3 + .../settings/ISettingsRepository.ts | 8 ++ .../settings/MongoSettingsRepository.ts | 74 ++++++++++++++++ server/src/types/index.ts | 1 + server/src/types/settings.ts | 31 +++++++ 8 files changed, 169 insertions(+), 84 deletions(-) delete mode 100755 server/src/db/models/AppSettings.js create mode 100644 server/src/db/models/AppSettings.ts create mode 100644 server/src/repositories/settings/ISettingsRepository.ts create mode 100644 server/src/repositories/settings/MongoSettingsRepository.ts create mode 100644 server/src/types/settings.ts diff --git a/server/src/db/models/AppSettings.js b/server/src/db/models/AppSettings.js deleted file mode 100755 index a1a9964f2..000000000 --- a/server/src/db/models/AppSettings.js +++ /dev/null @@ -1,84 +0,0 @@ -import mongoose from "mongoose"; - -const AppSettingsSchema = mongoose.Schema( - { - checkTTL: { - type: Number, - default: 30, - }, - language: { - type: String, - default: "gb", - }, - pagespeedApiKey: { - type: String, - }, - systemEmailHost: { - type: String, - }, - systemEmailPort: { - type: Number, - }, - systemEmailAddress: { - type: String, - }, - systemEmailPassword: { - type: String, - }, - systemEmailUser: { - type: String, - }, - systemEmailConnectionHost: { - type: String, - default: "localhost", - }, - systemEmailTLSServername: { - type: String, - }, - systemEmailSecure: { - type: Boolean, - default: false, - }, - systemEmailPool: { - type: Boolean, - default: false, - }, - systemEmailIgnoreTLS: { - type: Boolean, - default: false, - }, - systemEmailRequireTLS: { - type: Boolean, - default: false, - }, - systemEmailRejectUnauthorized: { - type: Boolean, - default: true, - }, - showURL: { - type: Boolean, - default: false, - }, - singleton: { - type: Boolean, - required: true, - unique: true, - default: true, - }, - version: { - type: Number, - default: 1, - }, - globalThresholds: { - cpu: { type: Number }, - memory: { type: Number }, - disk: { type: Number }, - temperature: { type: Number }, - }, - }, - { - timestamps: true, - } -); - -export default mongoose.model("AppSettings", AppSettingsSchema); diff --git a/server/src/db/models/AppSettings.ts b/server/src/db/models/AppSettings.ts new file mode 100644 index 000000000..22d1369e5 --- /dev/null +++ b/server/src/db/models/AppSettings.ts @@ -0,0 +1,49 @@ +import { Schema, model, type Types } from "mongoose"; +import type { Settings, SettingsThresholds } from "@/types/settings.js"; + +interface AppSettingsDocument extends Omit { + _id: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const thresholdsSchema = new Schema( + { + cpu: { type: Number }, + memory: { type: Number }, + disk: { type: Number }, + temperature: { type: Number }, + }, + { _id: false } +); + +const AppSettingsSchema = new Schema( + { + checkTTL: { type: Number, default: 30 }, + language: { type: String, default: "gb" }, + pagespeedApiKey: { type: String }, + systemEmailHost: { type: String }, + systemEmailPort: { type: Number }, + systemEmailAddress: { type: String }, + systemEmailPassword: { type: String }, + systemEmailUser: { type: String }, + systemEmailConnectionHost: { type: String, default: "localhost" }, + systemEmailTLSServername: { type: String }, + systemEmailSecure: { type: Boolean, default: false }, + systemEmailPool: { type: Boolean, default: false }, + systemEmailIgnoreTLS: { type: Boolean, default: false }, + systemEmailRequireTLS: { type: Boolean, default: false }, + systemEmailRejectUnauthorized: { type: Boolean, default: true }, + showURL: { type: Boolean, default: false }, + singleton: { type: Boolean, required: true, unique: true, default: true }, + version: { type: Number, default: 1 }, + globalThresholds: { type: thresholdsSchema }, + }, + { timestamps: true } +); + +const AppSettingsModel = model("AppSettings", AppSettingsSchema); + +export type { AppSettingsDocument }; +export { AppSettingsModel }; +export default AppSettingsModel; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index 1e5024b1c..59ed8125d 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -16,5 +16,8 @@ export { default as UserModel } from "@/db/models/User.js"; export * from "@/db/models/Invite.js"; export { default as InviteModel } from "@/db/models/Invite.js"; +export * from "@/db/models/AppSettings.js"; +export { default as AppSettingsModel } from "@/db/models/AppSettings.js"; + export * from "@/db/models/RecoveryToken.js"; export { default as RecoveryTokenModel } from "@/db/models/RecoveryToken.js"; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 7a3c08c81..09a686595 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -18,3 +18,6 @@ export { default as MongoInvitesRepository } from "@/repositories/invites/MongoI export * from "@/repositories/recovery-tokens/IRecoveryTokensRepository.js"; export { default as MongoRecoveryTokensRepository } from "@/repositories/recovery-tokens/MongoRecoveryTokensRepository.js"; + +export * from "@/repositories/settings/ISettingsRepository.js"; +export { default as MongoSettingsRepository } from "@/repositories/settings/MongoSettingsRepository.js"; diff --git a/server/src/repositories/settings/ISettingsRepository.ts b/server/src/repositories/settings/ISettingsRepository.ts new file mode 100644 index 000000000..111a92a55 --- /dev/null +++ b/server/src/repositories/settings/ISettingsRepository.ts @@ -0,0 +1,8 @@ +import type { Settings } from "@/types/index.js"; +export interface ISettingsRepository { + // create + // fetch + // update + update(settings: Partial): Promise; + // delete +} diff --git a/server/src/repositories/settings/MongoSettingsRepository.ts b/server/src/repositories/settings/MongoSettingsRepository.ts new file mode 100644 index 000000000..f4d71d56e --- /dev/null +++ b/server/src/repositories/settings/MongoSettingsRepository.ts @@ -0,0 +1,74 @@ +import mongoose from "mongoose"; +import { ISettingsRepository } from "@/repositories/settings/ISettingsRepository.js"; +import type { Settings } from "@/types/index.js"; +import { AppSettingsModel, type AppSettingsDocument } from "@/db/models/AppSettings.js"; +import { AppError } from "@/utils/AppError.js"; + +class MongoSettingsRepository implements ISettingsRepository { + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + protected toEntity = (doc: AppSettingsDocument): Settings => { + return { + id: this.toStringId(doc._id), + checkTTL: doc.checkTTL, + language: doc.language, + pagespeedApiKey: doc.pagespeedApiKey ?? undefined, + systemEmailHost: doc.systemEmailHost ?? undefined, + systemEmailPort: doc.systemEmailPort ?? undefined, + systemEmailAddress: doc.systemEmailAddress ?? undefined, + systemEmailPassword: doc.systemEmailPassword ?? undefined, + systemEmailUser: doc.systemEmailUser ?? undefined, + systemEmailConnectionHost: doc.systemEmailConnectionHost ?? undefined, + systemEmailTLSServername: doc.systemEmailTLSServername ?? undefined, + systemEmailSecure: doc.systemEmailSecure ?? false, + systemEmailPool: doc.systemEmailPool ?? false, + systemEmailIgnoreTLS: doc.systemEmailIgnoreTLS ?? false, + systemEmailRequireTLS: doc.systemEmailRequireTLS ?? false, + systemEmailRejectUnauthorized: doc.systemEmailRejectUnauthorized ?? true, + showURL: doc.showURL ?? false, + singleton: doc.singleton, + version: doc.version ?? 1, + globalThresholds: doc.globalThresholds ?? undefined, + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + update = async (settings: Partial) => { + const update: Record = { $set: { ...settings } }; + + if (settings.pagespeedApiKey === "") { + update.$unset = { pagespeedApiKey: "" }; + delete update.$set.pagespeedApiKey; + } + + if (settings.systemEmailPassword === "") { + update.$unset = { systemEmailPassword: "" }; + delete update.$set.systemEmailPassword; + } + + await AppSettingsModel.findOneAndUpdate({}, update, { + upsert: true, + }); + + const currentSettings = await AppSettingsModel.findOne().select("-__v -_id -createdAt -updatedAt -singleton").lean(); + if (!currentSettings) { + throw new AppError({ message: "Settings not found after update", status: 500 }); + } + return this.toEntity(currentSettings); + }; +} + +export default MongoSettingsRepository; diff --git a/server/src/types/index.ts b/server/src/types/index.ts index 1a43d4849..fcfb6fc37 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -6,3 +6,4 @@ export * from "@/types/network.js"; export * from "@/types/user.js"; export * from "@/types/invite.js"; export * from "@/types/recoveryToken.js"; +export * from "@/types/settings.js"; diff --git a/server/src/types/settings.ts b/server/src/types/settings.ts new file mode 100644 index 000000000..78284a451 --- /dev/null +++ b/server/src/types/settings.ts @@ -0,0 +1,31 @@ +export interface SettingsThresholds { + cpu?: number; + memory?: number; + disk?: number; + temperature?: number; +} + +export interface Settings { + id: string; + checkTTL: number; + language: string; + pagespeedApiKey?: string; + systemEmailHost?: string; + systemEmailPort?: number; + systemEmailAddress?: string; + systemEmailPassword?: string; + systemEmailUser?: string; + systemEmailConnectionHost?: string; + systemEmailTLSServername?: string; + systemEmailSecure: boolean; + systemEmailPool: boolean; + systemEmailIgnoreTLS: boolean; + systemEmailRequireTLS: boolean; + systemEmailRejectUnauthorized: boolean; + showURL: boolean; + singleton: boolean; + version: number; + globalThresholds?: SettingsThresholds; + createdAt: string; + updatedAt: string; +} From ba352309195b4458bed20b0fcc2e7b7d3a8d46e5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sat, 17 Jan 2026 19:08:12 +0000 Subject: [PATCH 261/400] remove db references from userService --- server/src/config/services.ts | 7 ++++++- server/src/db/models/AppSettings.ts | 1 + .../settings/MongoSettingsRepository.ts | 14 ++++++++------ server/src/service/business/userService.ts | 12 ++++++------ server/src/types/settings.ts | 1 + 5 files changed, 22 insertions(+), 13 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index aef32440f..6b91bcf63 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -74,6 +74,7 @@ import { MongoUsersRepository, MongoInvitesRepository, MongoRecoveryTokensRepository, + MongoSettingsRepository, IMonitorsRepository, IChecksRepository, IMonitorStatsRepository, @@ -81,6 +82,7 @@ import { IUsersRepository, IInvitesRepository, IRecoveryTokensRepository, + ISettingsRepository, } from "@/repositories/index.js"; export type InitializedSerivces = { @@ -113,6 +115,7 @@ export type InitializedSerivces = { usersRepository: IUsersRepository; invitesRepository: IInvitesRepository; recoveryTokensRepository: IRecoveryTokensRepository; + settingsRepository: ISettingsRepository; }; export const initializeServices = async ({ @@ -167,6 +170,7 @@ export const initializeServices = async ({ const usersRepository = new MongoUsersRepository(); const invitesRepository = new MongoInvitesRepository(); const recoveryTokensRepository = new MongoRecoveryTokensRepository(); + const settingsRepository = new MongoSettingsRepository(); const networkService = new NetworkService({ axios, @@ -240,7 +244,6 @@ export const initializeServices = async ({ // Business services const userService = new UserService({ crypto, - db, emailService, settingsService, logger, @@ -252,6 +255,7 @@ export const initializeServices = async ({ usersRepository, invitesRepository, recoveryTokensRepository, + settingsRepository, }); const diagnosticService = new DiagnosticService(); @@ -313,6 +317,7 @@ export const initializeServices = async ({ usersRepository, invitesRepository, recoveryTokensRepository, + settingsRepository, }; Object.values(services).forEach((service) => { diff --git a/server/src/db/models/AppSettings.ts b/server/src/db/models/AppSettings.ts index 22d1369e5..9eeaa640a 100644 --- a/server/src/db/models/AppSettings.ts +++ b/server/src/db/models/AppSettings.ts @@ -21,6 +21,7 @@ const AppSettingsSchema = new Schema( { checkTTL: { type: Number, default: 30 }, language: { type: String, default: "gb" }, + jwtSecret: { type: String }, pagespeedApiKey: { type: String }, systemEmailHost: { type: String }, systemEmailPort: { type: Number }, diff --git a/server/src/repositories/settings/MongoSettingsRepository.ts b/server/src/repositories/settings/MongoSettingsRepository.ts index f4d71d56e..2cc700525 100644 --- a/server/src/repositories/settings/MongoSettingsRepository.ts +++ b/server/src/repositories/settings/MongoSettingsRepository.ts @@ -2,7 +2,6 @@ import mongoose from "mongoose"; import { ISettingsRepository } from "@/repositories/settings/ISettingsRepository.js"; import type { Settings } from "@/types/index.js"; import { AppSettingsModel, type AppSettingsDocument } from "@/db/models/AppSettings.js"; -import { AppError } from "@/utils/AppError.js"; class MongoSettingsRepository implements ISettingsRepository { private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { @@ -24,6 +23,7 @@ class MongoSettingsRepository implements ISettingsRepository { id: this.toStringId(doc._id), checkTTL: doc.checkTTL, language: doc.language, + jwtSecret: doc.jwtSecret ?? undefined, pagespeedApiKey: doc.pagespeedApiKey ?? undefined, systemEmailHost: doc.systemEmailHost ?? undefined, systemEmailPort: doc.systemEmailPort ?? undefined, @@ -63,11 +63,13 @@ class MongoSettingsRepository implements ISettingsRepository { upsert: true, }); - const currentSettings = await AppSettingsModel.findOne().select("-__v -_id -createdAt -updatedAt -singleton").lean(); - if (!currentSettings) { - throw new AppError({ message: "Settings not found after update", status: 500 }); - } - return this.toEntity(currentSettings); + const updatedSettings = await AppSettingsModel.findOneAndUpdate({}, update, { + upsert: true, + new: true, + projection: "-__v -_id -createdAt -updatedAt -singleton", + }); + + return this.toEntity(updatedSettings); }; } diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index 7d9e5811e..5f73508dd 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -1,4 +1,4 @@ -import { IInvitesRepository, IMonitorsRepository, IRecoveryTokensRepository, IUsersRepository } from "@/repositories/index.js"; +import { IInvitesRepository, IMonitorsRepository, IRecoveryTokensRepository, IUsersRepository, ISettingsRepository } from "@/repositories/index.js"; import Team from "@/db/models/Team.js"; import type { User } from "@/types/index.js"; import bcrypt from "bcryptjs"; @@ -9,7 +9,6 @@ const SERVICE_NAME = "userService"; class UserService { static SERVICE_NAME = SERVICE_NAME; - private db: any; private emailService: any; private settingsService: any; private logger: any; @@ -22,10 +21,10 @@ class UserService { private usersRepository: IUsersRepository; private invitesRepository: IInvitesRepository; private recoveryTokensRepository: IRecoveryTokensRepository; + private settingsRepository: ISettingsRepository; constructor({ crypto, - db, emailService, settingsService, logger, @@ -37,9 +36,9 @@ class UserService { usersRepository, invitesRepository, recoveryTokensRepository, + settingsRepository, }: { crypto: any; - db: any; emailService: any; settingsService: any; logger: any; @@ -51,8 +50,8 @@ class UserService { usersRepository: IUsersRepository; invitesRepository: IInvitesRepository; recoveryTokensRepository: IRecoveryTokensRepository; + settingsRepository: ISettingsRepository; }) { - this.db = db; this.emailService = emailService; this.settingsService = settingsService; this.logger = logger; @@ -65,6 +64,7 @@ class UserService { this.usersRepository = usersRepository; this.invitesRepository = invitesRepository; this.recoveryTokensRepository = recoveryTokensRepository; + this.settingsRepository = settingsRepository; } get serviceName() { @@ -89,7 +89,7 @@ class UserService { } else { // This is the first account, create JWT secret to use if one is not supplied by env const jwtSecret = this.crypto.randomBytes(64).toString("hex"); - await this.db.settingsModule.updateAppSettings({ jwtSecret }); + await this.settingsRepository.update({ jwtSecret }); // Create a new team const team = new Team({ email: user.email, diff --git a/server/src/types/settings.ts b/server/src/types/settings.ts index 78284a451..108cdcb72 100644 --- a/server/src/types/settings.ts +++ b/server/src/types/settings.ts @@ -9,6 +9,7 @@ export interface Settings { id: string; checkTTL: number; language: string; + jwtSecret?: string; pagespeedApiKey?: string; systemEmailHost?: string; systemEmailPort?: number; From 3e8d4cb49bb47123ee4390feec240cc12863d26e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 18 Jan 2026 05:38:50 +0000 Subject: [PATCH 262/400] convert to ts --- server/src/config/services.ts | 3 +- .../invites/IInvitesRepository.ts | 2 + .../invites/MongoInviteRepository.ts | 18 ++++++ server/src/service/business/inviteService.ts | 60 +++++++++++++++++++ 4 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 server/src/service/business/inviteService.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 6b91bcf63..bbb42984e 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -260,10 +260,9 @@ export const initializeServices = async ({ const diagnosticService = new DiagnosticService(); const inviteService = new InviteService({ - db, + invitesRepository, settingsService, emailService, - stringService, errorService, }); const maintenanceWindowService = new MaintenanceWindowService({ diff --git a/server/src/repositories/invites/IInvitesRepository.ts b/server/src/repositories/invites/IInvitesRepository.ts index 066a51a0f..74a7ad68c 100644 --- a/server/src/repositories/invites/IInvitesRepository.ts +++ b/server/src/repositories/invites/IInvitesRepository.ts @@ -2,7 +2,9 @@ import type { Invite } from "@/types/index.js"; export interface IInvitesRepository { // create + create(invite: Partial): Promise; // fetch + findByToken(token: string): Promise; findByTokenAndDelete(token: string): Promise; // update diff --git a/server/src/repositories/invites/MongoInviteRepository.ts b/server/src/repositories/invites/MongoInviteRepository.ts index 16e56440b..e9243e96e 100644 --- a/server/src/repositories/invites/MongoInviteRepository.ts +++ b/server/src/repositories/invites/MongoInviteRepository.ts @@ -3,6 +3,7 @@ import type { Invite } from "@/types/index.js"; import { type InviteDocument, InviteModel } from "@/db/models/index.js"; import { AppError } from "@/utils/AppError.js"; import mongoose from "mongoose"; +import crypto from "crypto"; class MongoInvitesRepository implements IInvitesRepository { private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { @@ -32,6 +33,23 @@ class MongoInvitesRepository implements IInvitesRepository { }; }; + create = async (invite: Partial) => { + await InviteModel.deleteMany({ email: invite.email }); + invite.token = crypto.randomBytes(32).toString("hex"); + let inviteToken = await InviteModel.create(invite); + return this.toEntity(inviteToken); + }; + + findByToken = async (token: string) => { + const invite = await InviteModel.findOne({ + token, + }); + if (invite === null) { + throw new AppError({ message: "Invite not found", status: 404 }); + } + return this.toEntity(invite); + }; + findByTokenAndDelete = async (token: string) => { const invite = await InviteModel.findOneAndDelete({ token, diff --git a/server/src/service/business/inviteService.ts b/server/src/service/business/inviteService.ts new file mode 100644 index 000000000..cdb860fba --- /dev/null +++ b/server/src/service/business/inviteService.ts @@ -0,0 +1,60 @@ +import type { Invite } from "@/types/index.js"; +import type { IInvitesRepository } from "@/repositories/index.js"; + +const SERVICE_NAME = "inviteService"; + +class InviteService { + static SERVICE_NAME = SERVICE_NAME; + + private settingsService: any; + private emailService: any; + private errorService: any; + private invitesRepository: IInvitesRepository; + + constructor({ + invitesRepository, + settingsService, + emailService, + errorService, + }: { + invitesRepository: IInvitesRepository; + settingsService: any; + emailService: any; + errorService: any; + }) { + this.invitesRepository = invitesRepository; + this.settingsService = settingsService; + this.emailService = emailService; + this.errorService = errorService; + } + + get serviceName() { + return InviteService.SERVICE_NAME; + } + + getInviteToken = async ({ invite, teamId }: { invite: Partial; teamId: string }) => { + invite.teamId = teamId; + const inviteToken = await this.invitesRepository.create(invite); + return inviteToken; + }; + + sendInviteEmail = async ({ invite, firstName }: { invite: Partial; firstName: any }) => { + const inviteToken = await this.invitesRepository.create(invite); + const { clientHost } = this.settingsService.getSettings(); + + const html = await this.emailService.buildEmail("employeeActivationTemplate", { + name: firstName, + link: `${clientHost}/register/${inviteToken.token}`, + }); + const result = await this.emailService.sendEmail(invite.email, "Welcome to Uptime Monitor", html); + if (!result) { + throw this.errorService.createServerError("Failed to send invite e-mail... Please verify your settings."); + } + }; + + verifyInviteToken = async ({ inviteToken }: { inviteToken: string }) => { + return await this.invitesRepository.findByToken(inviteToken); + }; +} + +export default InviteService; From a0ebf7d224bd06c2561a446c1f6e043293145104 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 18 Jan 2026 06:02:07 +0000 Subject: [PATCH 263/400] initial commit --- server/src/db/models/Notification.js | 74 ------------------- server/src/db/models/Notification.ts | 50 +++++++++++++ server/src/db/models/User.ts | 4 +- server/src/db/models/index.ts | 3 + .../INotificationProvider.ts | 4 + .../infrastructure/notificationsService.ts | 4 + server/src/types/index.ts | 1 + server/src/types/notification.ts | 17 +++++ 8 files changed, 81 insertions(+), 76 deletions(-) delete mode 100755 server/src/db/models/Notification.js create mode 100755 server/src/db/models/Notification.ts create mode 100644 server/src/service/infrastructure/notificationProviders/INotificationProvider.ts create mode 100644 server/src/service/infrastructure/notificationsService.ts create mode 100644 server/src/types/notification.ts diff --git a/server/src/db/models/Notification.js b/server/src/db/models/Notification.js deleted file mode 100755 index 72c716a26..000000000 --- a/server/src/db/models/Notification.js +++ /dev/null @@ -1,74 +0,0 @@ -import mongoose from "mongoose"; - -const NotificationSchema = mongoose.Schema( - { - userId: { - type: mongoose.Schema.Types.ObjectId, - ref: "User", - immutable: true, - required: true, - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - required: true, - }, - type: { - type: String, - enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix"], - }, - notificationName: { - type: String, - required: true, - }, - address: { - type: String, - }, - phone: { - type: String, - }, - // Matrix-specific fields - homeserverUrl: { - type: String, - }, - roomId: { - type: String, - }, - accessToken: { - type: String, - }, - }, - { - timestamps: true, - } -); - -NotificationSchema.pre("save", function (next) { - if (!this.cpuAlertThreshold || this.isModified("alertThreshold")) { - this.cpuAlertThreshold = this.alertThreshold; - } - if (!this.memoryAlertThreshold || this.isModified("alertThreshold")) { - this.memoryAlertThreshold = this.alertThreshold; - } - if (!this.diskAlertThreshold || this.isModified("alertThreshold")) { - this.diskAlertThreshold = this.alertThreshold; - } - if (!this.tempAlertThreshold || this.isModified("alertThreshold")) { - this.tempAlertThreshold = this.alertThreshold; - } - next(); -}); - -NotificationSchema.pre("findOneAndUpdate", function (next) { - const update = this.getUpdate(); - if (update.alertThreshold) { - update.cpuAlertThreshold = update.alertThreshold; - update.memoryAlertThreshold = update.alertThreshold; - update.diskAlertThreshold = update.alertThreshold; - update.tempAlertThreshold = update.alertThreshold; - } - next(); -}); - -export default mongoose.model("Notification", NotificationSchema); diff --git a/server/src/db/models/Notification.ts b/server/src/db/models/Notification.ts new file mode 100755 index 000000000..97e2d43fb --- /dev/null +++ b/server/src/db/models/Notification.ts @@ -0,0 +1,50 @@ +import { Schema, model, type Types } from "mongoose"; +import type { Notification, NotificationChannel } from "@/types/notification.js"; + +interface NotificationDocument extends Omit { + _id: Types.ObjectId; + userId: Types.ObjectId; + teamId: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const NotificationSchema = new Schema( + { + userId: { + type: Schema.Types.ObjectId, + ref: "User", + immutable: true, + required: true, + }, + teamId: { + type: Schema.Types.ObjectId, + ref: "Team", + immutable: true, + required: true, + }, + type: { + type: String, + enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as NotificationChannel[], + required: true, + }, + notificationName: { + type: String, + required: true, + }, + address: { type: String }, + phone: { type: String }, + homeserverUrl: { type: String }, + roomId: { type: String }, + accessToken: { type: String }, + }, + { + timestamps: true, + } +); + +const NotificationModel = model("Notification", NotificationSchema); + +export type { NotificationDocument }; +export { NotificationModel }; +export default NotificationModel; diff --git a/server/src/db/models/User.ts b/server/src/db/models/User.ts index e00250340..6aa3835a6 100644 --- a/server/src/db/models/User.ts +++ b/server/src/db/models/User.ts @@ -3,7 +3,7 @@ import bcrypt from "bcryptjs"; import type { User, UserProfileImage, UserRole } from "@/types/index.js"; import { MonitorModel } from "@/db/models/index.js"; import Team from "./Team.js"; -import Notification from "./Notification.js"; +import NotificationModel from "./Notification.js"; type UserDocumentBase = Omit & { teamId?: Types.ObjectId; @@ -76,7 +76,7 @@ UserSchema.pre("findOneAndDelete", async function (next) { await Team.deleteOne({ _id: userToDelete.teamId }); await MonitorModel.deleteMany({ userId: userToDelete._id }); await this.model.deleteMany({ teamId: userToDelete.teamId, _id: { $ne: userToDelete._id } }); - await Notification.deleteMany({ teamId: userToDelete.teamId }); + await NotificationModel.deleteMany({ teamId: userToDelete.teamId }); } next(); } catch (error) { diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index 59ed8125d..18a3a3b19 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -21,3 +21,6 @@ export { default as AppSettingsModel } from "@/db/models/AppSettings.js"; export * from "@/db/models/RecoveryToken.js"; export { default as RecoveryTokenModel } from "@/db/models/RecoveryToken.js"; + +export * from "@/db/models/Notification.js"; +export { default as NotificationModel } from "@/db/models/Notification.js"; diff --git a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts new file mode 100644 index 000000000..c9a20ff5f --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts @@ -0,0 +1,4 @@ +import type { Monitor } from "@/types/index.js"; + +export interface IAlert {} +export interface IMessageProvider {} diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts new file mode 100644 index 000000000..e9a6e8e2d --- /dev/null +++ b/server/src/service/infrastructure/notificationsService.ts @@ -0,0 +1,4 @@ +import type { Monitor } from "@/types/index.js"; +interface INotificationSerivce { + handleNotifications: (moniotr: Monitor) => Promise; +} diff --git a/server/src/types/index.ts b/server/src/types/index.ts index fcfb6fc37..9bffb3816 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -7,3 +7,4 @@ export * from "@/types/user.js"; export * from "@/types/invite.js"; export * from "@/types/recoveryToken.js"; export * from "@/types/settings.js"; +export * from "@/types/notification.js"; diff --git a/server/src/types/notification.ts b/server/src/types/notification.ts new file mode 100644 index 000000000..669c840ca --- /dev/null +++ b/server/src/types/notification.ts @@ -0,0 +1,17 @@ +export const NotificationChannels = ["email", "slack", "discord", "webhook", "pager_duty", "matrix"] as const; +export type NotificationChannel = (typeof NotificationChannels)[number]; + +export interface Notification { + id: string; + userId: string; + teamId: string; + type: NotificationChannel; + notificationName: string; + address?: string; + phone?: string; + homeserverUrl?: string; + roomId?: string; + accessToken?: string; + createdAt: string; + updatedAt: string; +} From cff5508614deb9a4270683092d85a8d4104a3dee Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 18 Jan 2026 16:34:20 +0000 Subject: [PATCH 264/400] notification provider --- server/src/config/services.ts | 4 +-- server/src/db/modules/notificationModule.js | 2 -- .../INotificationProvider.ts | 9 ++++-- server/src/types/alert.ts | 29 +++++++++++++++++++ server/src/types/index.ts | 1 + 5 files changed, 38 insertions(+), 7 deletions(-) create mode 100644 server/src/types/alert.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index bbb42984e..49e6f3d5e 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -50,7 +50,7 @@ import StatusPage from "../db/models/StatusPage.js"; import Team from "../db/models/Team.js"; import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; import MonitorStats from "../db/models/MonitorStats.js"; -import Notification from "../db/models/Notification.js"; +import NotificationModel from "../db/models/Notification.js"; import RecoveryToken from "../db/models/RecoveryToken.js"; import AppSettings from "../db/models/AppSettings.js"; import Incident from "../db/models/Incident.js"; @@ -141,7 +141,7 @@ export const initializeServices = async ({ const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService, AppSettings }); const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); - const notificationModule = new NotificationModule({ Notification, Monitor }); + const notificationModule = new NotificationModule({ Notification: NotificationModel, Monitor }); const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService }); const settingsModule = new SettingsModule({ AppSettings }); const incidentModule = new IncidentModule({ logger, Incident, Monitor, User }); diff --git a/server/src/db/modules/notificationModule.js b/server/src/db/modules/notificationModule.js index 3c6edb54a..f45900184 100755 --- a/server/src/db/modules/notificationModule.js +++ b/server/src/db/modules/notificationModule.js @@ -1,5 +1,3 @@ -// import Notification from "../../models/Notification.js"; -// import Monitor from "../../models/Monitor.js"; const SERVICE_NAME = "notificationModule"; class NotificationModule { diff --git a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts index c9a20ff5f..12b28d907 100644 --- a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts +++ b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts @@ -1,4 +1,7 @@ -import type { Monitor } from "@/types/index.js"; +import type { Monitor, Notification, Alert } from "@/types/index.js"; -export interface IAlert {} -export interface IMessageProvider {} +export interface IMessageProvider { + buildAlert: (monitor: Monitor) => Alert; + sendAlert: (alert: Alert, notification: Notification) => Promise; + sendTestAlert(notification: Notification): Promise; +} diff --git a/server/src/types/alert.ts b/server/src/types/alert.ts new file mode 100644 index 000000000..4725ebee8 --- /dev/null +++ b/server/src/types/alert.ts @@ -0,0 +1,29 @@ +import { NotificationChannel } from "@/types/index.js"; +export interface AlertWebhookPayload { + body: Record; +} + +export interface AlertPagerDutyPayload { + routingKey: string; + monitorUrl?: string; +} + +export interface AlertMatrixPayload { + friendlyName?: string; + homeserverUrl: string; + accessToken: string; + roomId: string; + monitorName: string; +} + +export interface Alert { + channel: NotificationChannel; + address?: string; + subject: string; + message: string; + html?: string; + discordContent?: Record | null; + webhook?: AlertWebhookPayload | null; + pagerDuty?: AlertPagerDutyPayload | null; + matrix?: AlertMatrixPayload | null; +} diff --git a/server/src/types/index.ts b/server/src/types/index.ts index 9bffb3816..fda48f0c6 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -8,3 +8,4 @@ export * from "@/types/invite.js"; export * from "@/types/recoveryToken.js"; export * from "@/types/settings.js"; export * from "@/types/notification.js"; +export * from "@/types/alert.js"; From 391f42ee53dd62e2a093122236df320dc8cf2151 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 18 Jan 2026 16:59:38 +0000 Subject: [PATCH 265/400] add webhook provider and utils --- server/src/service/index.ts | 2 + .../INotificationProvider.ts | 6 +- .../notificationProviders/utils.ts | 91 +++++++++++++++++++ .../notificationProviders/webhook.ts | 15 +++ 4 files changed, 111 insertions(+), 3 deletions(-) create mode 100644 server/src/service/infrastructure/notificationProviders/utils.ts create mode 100644 server/src/service/infrastructure/notificationProviders/webhook.ts diff --git a/server/src/service/index.ts b/server/src/service/index.ts index a094f8b01..4dba8f6f4 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -1,2 +1,4 @@ export * from "@/service/business/monitorService.js"; export * from "@/service/infrastructure/networkService.js"; +export * from "@/service/infrastructure/notificationProviders/webhook.js"; +export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; diff --git a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts index 12b28d907..2743c40bb 100644 --- a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts +++ b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts @@ -1,7 +1,7 @@ -import type { Monitor, Notification, Alert } from "@/types/index.js"; +import type { Monitor, Notification, Alert, MonitorStatusResponse } from "@/types/index.js"; -export interface IMessageProvider { - buildAlert: (monitor: Monitor) => Alert; +export interface INotificationProvider { + buildAlert: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => Alert; sendAlert: (alert: Alert, notification: Notification) => Promise; sendTestAlert(notification: Notification): Promise; } diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts new file mode 100644 index 000000000..9a1b2e7f8 --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -0,0 +1,91 @@ +import { Monitor, HardwareStatusPayload, MonitorStatusResponse } from "@/types/index.js"; + +const buildHardwareAlerts = async (clientHost: string, monitor: Monitor, networkResponse: MonitorStatusResponse) => { + const thresholds = monitor.thresholds || {}; + const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; + + const payload = networkResponse?.payload as HardwareStatusPayload; + const metrics = payload.data || {}; + const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics; + + const alerts = { + cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false, + memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false, + disk: disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold) ?? false, + }; + + const alertsToSend = []; + const discordEmbeds = []; + const monitorInfoFields = [ + { name: "Monitor", value: monitor.name, inline: true }, + { name: "URL", value: monitor.url, inline: false }, + ]; + const goToIncidentField = { name: `Go to incident`, value: `${clientHost}/infrastructure/${monitor.id}` }; + const formatDiscordAlert = { + cpu: () => ({ + title: "CPU alert", + description: `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, + color: 15548997, + fields: [...monitorInfoFields, goToIncidentField], + footer: { text: "Checkmate" }, + }), + + memory: () => ({ + title: "Memory alert", + description: `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, + color: 15548997, + fields: [...monitorInfoFields, goToIncidentField], + footer: { text: "Checkmate" }, + }), + + disk: () => ({ + title: "Disk alert", + description: `Your current disk usage is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, + color: 15548997, + footer: { text: "Checkmate" }, + fields: [ + ...monitorInfoFields, + ...(Array.isArray(disk) ? disk : []).map((d: any, idx: number) => ({ + name: `Disk ${idx}`, + value: `${(d?.usage_percent * 100).toFixed(0)}%`, + inline: true, + })), + goToIncidentField, + ], + }), + }; + const alertTypes = ["cpu", "memory", "disk"]; + for (const type of alertTypes) { + // Iterate over each alert type to see if any need to be decremented + if (alerts[type] === true) { + monitor[`${type}AlertThreshold`]--; // Decrement threshold if an alert is triggered + + if (monitor[`${type}AlertThreshold`] <= 0) { + // If threshold drops below 0, reset and send notification + monitor[`${type}AlertThreshold`] = monitor.alertThreshold; + + const formatAlert = { + cpu: () => `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, + memory: () => + `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, + disk: () => + `Your current disk usage: ${disk + .map((d, idx) => `(Disk${idx}: ${(d.usage_percent * 100).toFixed(0)}%)`) + .join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, + }; + alertsToSend.push(formatAlert[type]()); + discordEmbeds.push(formatDiscordAlert[type]()); + } + } + } + const discordPayload = discordEmbeds.length ? { embeds: discordEmbeds } : null; + return [alertsToSend, discordPayload]; +}; + +const buildHardwareNotificationMessage = (alerts: any, monitor: Monitor) => { + const { clientHost } = this.settingsService.getSettings(); + const alertsHeader = [`Monitor: ${monitor.name}`, `URL: ${monitor.url}`]; + const alertFooter = [`Go to incident: ${clientHost}/infrastructure/${monitor._id}`]; + const alertText = alerts.length > 0 ? [...alertsHeader, ...alerts, ...alertFooter] : []; + return alertText.map((alert) => alert).join("\n"); +}; diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts new file mode 100644 index 000000000..4266bbf3b --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -0,0 +1,15 @@ +import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; +export class WebhookProvider implements INotificationProvider { + buildAlert = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + return null; + }; + + sendAlert = async (alert: Alert) => { + return false; + }; + + sendTestAlert(notification: Notification): Promise { + return false; + } +} From 487f6b222d7ad0e919435348f21eb24067f6def6 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Sun, 18 Jan 2026 14:11:05 -0500 Subject: [PATCH 266/400] refactor: extract sidebar width logic into useSidebar hook Create shared useSidebar hook to centralize sidebar width and transition logic, addressing code review feedback about duplicated logic between Sidebar and HomeLayout components. Changes: - Add useSidebar hook with collapsed state, width, and transition values - Update Sidebar to use the hook instead of inline selectors and values - Update HomeLayout to use the hook for spacer element styling This ensures sidebar width logic is defined in one place and stays in sync. --- .../v1/Layouts/HomeLayout/index.jsx | 10 +++---- client/src/Components/v1/Sidebar/index.jsx | 13 +++------ client/src/Hooks/useSidebar.js | 29 +++++++++++++++++++ 3 files changed, 37 insertions(+), 15 deletions(-) create mode 100644 client/src/Hooks/useSidebar.js diff --git a/client/src/Components/v1/Layouts/HomeLayout/index.jsx b/client/src/Components/v1/Layouts/HomeLayout/index.jsx index 825157333..b078fe048 100644 --- a/client/src/Components/v1/Layouts/HomeLayout/index.jsx +++ b/client/src/Components/v1/Layouts/HomeLayout/index.jsx @@ -1,12 +1,12 @@ import Sidebar from "../../Sidebar/index.jsx"; import { Outlet } from "react-router"; import { Box, Stack } from "@mui/material"; -import { useSelector } from "react-redux"; +import { useSidebar } from "@/Hooks/useSidebar.js"; import "./index.css"; const HomeLayout = () => { - const collapsed = useSelector((state) => state.ui.sidebar?.collapsed ?? false); + const { width, transition } = useSidebar(); return ( { {/* Spacer for fixed sidebar */} diff --git a/client/src/Components/v1/Sidebar/index.jsx b/client/src/Components/v1/Sidebar/index.jsx index f39121987..06caede5a 100644 --- a/client/src/Components/v1/Sidebar/index.jsx +++ b/client/src/Components/v1/Sidebar/index.jsx @@ -12,9 +12,9 @@ import Icon from "../Icon"; // Utils import { useTheme } from "@mui/material/styles"; -import { useSelector } from "react-redux"; import { useTranslation } from "react-i18next"; import { useNavigate } from "react-router"; +import { useSidebar } from "@/Hooks/useSidebar.js"; const URL_MAP = { support: "https://discord.com/invite/NAb6H3UTjK", @@ -65,8 +65,7 @@ const Sidebar = () => { const theme = useTheme(); const { t } = useTranslation(); const navigate = useNavigate(); - // Redux state - const collapsed = useSelector((state) => state.ui.sidebar?.collapsed ?? false); + const { collapsed, width, transition } = useSidebar(); const menu = getMenu(t); const otherMenuItems = getOtherMenuItems(t); @@ -75,11 +74,7 @@ const Sidebar = () => { return ( { paddingBottom={theme.spacing(6)} gap={theme.spacing(6)} sx={{ - transition: "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)", + transition, backgroundColor: "#000000", borderRight: "1px solid #344054", zIndex: 1000, diff --git a/client/src/Hooks/useSidebar.js b/client/src/Hooks/useSidebar.js new file mode 100644 index 000000000..97724b417 --- /dev/null +++ b/client/src/Hooks/useSidebar.js @@ -0,0 +1,29 @@ +import { useSelector } from "react-redux"; + +// CSS variable names for sidebar widths +const SIDEBAR_WIDTH_VAR = "var(--env-var-side-bar-width)"; +const SIDEBAR_COLLAPSED_WIDTH_VAR = "var(--env-var-side-bar-collapsed-width)"; + +// Transition timing for sidebar width changes +const SIDEBAR_TRANSITION = "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)"; + +/** + * Hook to get sidebar state and computed width + * Centralizes sidebar width logic to avoid duplication between Sidebar and HomeLayout + * + * @returns {Object} Sidebar state and styles + * @returns {boolean} collapsed - Whether the sidebar is collapsed + * @returns {string} width - CSS width value based on collapsed state + * @returns {string} transition - CSS transition for width changes + */ +export const useSidebar = () => { + const collapsed = useSelector((state) => state.ui.sidebar?.collapsed ?? false); + + return { + collapsed, + width: collapsed ? SIDEBAR_COLLAPSED_WIDTH_VAR : SIDEBAR_WIDTH_VAR, + transition: SIDEBAR_TRANSITION, + }; +}; + +export default useSidebar; From 4e9839e2e5a3bd27968660c660f497d2e69193ea Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 18 Jan 2026 22:52:59 +0000 Subject: [PATCH 267/400] notificationsService --- server/src/config/services.ts | 9 +++- server/src/service/index.ts | 1 + .../SuperSimpleQueueHelper.ts | 43 +++++++++++-------- .../service/infrastructure/bufferService.ts | 16 +------ .../infrastructure/notificationsService.ts | 13 ++++-- .../service/infrastructure/statusService.ts | 16 +------ 6 files changed, 46 insertions(+), 52 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 49e6f3d5e..92acd8868 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -8,6 +8,7 @@ import BufferService from "../service/infrastructure/bufferService.js"; import StatusService from "../service/infrastructure/statusService.js"; import NotificationUtils from "../service/infrastructure/notificationUtils.js"; import NotificationService from "../service/infrastructure/notificationService.js"; +import { NotificationsService } from "@/service/index.js"; import ErrorService from "../service/infrastructure/errorService.js"; import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; @@ -206,9 +207,9 @@ export const initializeServices = async ({ checksRepository, }); - const bufferService = new BufferService({ db, logger, envSettings, incidentService, checkService }); + const bufferService = new BufferService({ logger, incidentService, checkService }); - const statusService = new StatusService({ db, logger, buffer: bufferService, incidentService, monitorsRepository }); + const statusService = new StatusService({ db, logger, buffer: bufferService, monitorsRepository }); const notificationUtils = new NotificationUtils({ stringService, @@ -225,12 +226,15 @@ export const initializeServices = async ({ notificationUtils, }); + const notificationsService = new NotificationsService(); + const superSimpleQueueHelper = new SuperSimpleQueueHelper({ db, logger, networkService, statusService, notificationService, + notificationsService, checkService, buffer: bufferService, }); @@ -317,6 +321,7 @@ export const initializeServices = async ({ invitesRepository, recoveryTokensRepository, settingsRepository, + notificationsService, }; Object.values(services).forEach((service) => { diff --git a/server/src/service/index.ts b/server/src/service/index.ts index 4dba8f6f4..08c02f397 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -2,3 +2,4 @@ export * from "@/service/business/monitorService.js"; export * from "@/service/infrastructure/networkService.js"; export * from "@/service/infrastructure/notificationProviders/webhook.js"; export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; +export * from "@/service/infrastructure/notificationsService.js"; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 891635d56..c2a9e1267 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -1,7 +1,7 @@ const SERVICE_NAME = "JobQueueHelper"; import type { Monitor } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; -import { INetworkService } from "@/service/index.js"; +import { INetworkService, INotificationsService } from "@/service/index.js"; class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; @@ -10,6 +10,7 @@ class SuperSimpleQueueHelper { private networkService: INetworkService; private statusService: any; private notificationService: any; + private notificationsService: INotificationsService; private checkService: any; private buffer: any; @@ -19,6 +20,7 @@ class SuperSimpleQueueHelper { networkService, statusService, notificationService, + notificationsService, checkService, buffer, }: { @@ -27,6 +29,7 @@ class SuperSimpleQueueHelper { networkService: INetworkService; statusService: any; notificationService: any; + notificationsService: INotificationsService; checkService: any; buffer: any; }) { @@ -37,6 +40,7 @@ class SuperSimpleQueueHelper { this.notificationService = notificationService; this.checkService = checkService; this.buffer = buffer; + this.notificationsService = notificationsService; } get serviceName() { @@ -78,22 +82,27 @@ class SuperSimpleQueueHelper { // Step 4. Update monitor status const statusChangeResult = await this.statusService.updateMonitorStatus(status, check); - this.notificationService - .handleNotifications({ - ...status, - monitor: statusChangeResult.monitor, - prevStatus: statusChangeResult.prevStatus, - statusChanged: statusChangeResult.statusChanged, - }) - .catch((error: any) => { - this.logger.error({ - message: error.message, - service: SERVICE_NAME, - method: "getMonitorJob", - details: `Error sending notifications for job ${monitor.id}: ${error.message}`, - stack: error.stack, - }); - }); + // Step 5 handle incidents and notifications + if (statusChangeResult.statusChanged === true) { + this.notificationsService.handleNotifications(monitor, status); + } + + // this.notificationService + // .handleNotifications({ + // ...status, + // monitor: statusChangeResult.monitor, + // prevStatus: statusChangeResult.prevStatus, + // statusChanged: statusChangeResult.statusChanged, + // }) + // .catch((error: any) => { + // this.logger.error({ + // message: error.message, + // service: SERVICE_NAME, + // method: "getMonitorJob", + // details: `Error sending notifications for job ${monitor.id}: ${error.message}`, + // stack: error.stack, + // }); + // }); } catch (error: any) { this.logger.warn({ message: error.message, diff --git a/server/src/service/infrastructure/bufferService.ts b/server/src/service/infrastructure/bufferService.ts index 5a809eea5..0363aa362 100755 --- a/server/src/service/infrastructure/bufferService.ts +++ b/server/src/service/infrastructure/bufferService.ts @@ -6,7 +6,6 @@ const SERVICE_NAME = "BufferService"; class BufferService { static SERVICE_NAME = SERVICE_NAME; private BUFFER_TIMEOUT: number; - private db: any; private logger: any; private incidentService: any; private SERVICE_NAME: string; @@ -15,21 +14,8 @@ class BufferService { private bufferTimer: NodeJS.Timeout | null = null; private checksService: any; - constructor({ - db, - logger, - envSettings, - incidentService, - checkService, - }: { - db: any; - logger: any; - envSettings: any; - incidentService: any; - checkService: any; - }) { + constructor({ logger, incidentService, checkService }: { logger: any; incidentService: any; checkService: any }) { this.BUFFER_TIMEOUT = config.NODE_ENV === "development" ? 10 : 1000 * 60 * 1; // 1 minute - this.db = db; this.logger = logger; this.incidentService = incidentService; this.checksService = checkService; diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index e9a6e8e2d..f9fdbfea8 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,4 +1,11 @@ -import type { Monitor } from "@/types/index.js"; -interface INotificationSerivce { - handleNotifications: (moniotr: Monitor) => Promise; +import type { Monitor, MonitorStatusResponse } from "@/types/index.js"; +export interface INotificationsService { + handleNotifications: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => Promise; +} + +export class NotificationsService implements INotificationsService { + handleNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + console.log(JSON.stringify(monitor, null, 2)); + console.log(JSON.stringify(monitorStatusResponse, null, 2)); + }; } diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index 7d34017a5..c9aab706b 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -17,26 +17,12 @@ class StatusService { private db: any; private logger: any; private buffer: any; - private incidentService: any; private monitorsRepository: IMonitorsRepository; - constructor({ - db, - logger, - buffer, - incidentService, - monitorsRepository, - }: { - db: any; - logger: any; - buffer: any; - incidentService: any; - monitorsRepository: IMonitorsRepository; - }) { + constructor({ db, logger, buffer, monitorsRepository }: { db: any; logger: any; buffer: any; monitorsRepository: IMonitorsRepository }) { this.db = db; this.logger = logger; this.buffer = buffer; - this.incidentService = incidentService; this.monitorsRepository = monitorsRepository; } From a86588f080b03ce88664b86517e1dc9d380dc7d0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 18 Jan 2026 23:38:40 +0000 Subject: [PATCH 268/400] refactoring notifications --- server/src/config/services.ts | 3 +- server/src/service/index.ts | 1 + .../SuperSimpleQueueHelper.ts | 17 ++++--- .../infrastructure/notificationsService.ts | 48 +++++++++++++++++-- .../service/infrastructure/statusService.ts | 31 +++++++----- server/src/types/network.ts | 2 +- 6 files changed, 74 insertions(+), 28 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 92acd8868..ed1224ec7 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -5,10 +5,9 @@ import MongoDB from "../db/MongoDB.js"; import NetworkService from "../service/infrastructure/networkService.js"; import EmailService from "../service/infrastructure/emailService.js"; import BufferService from "../service/infrastructure/bufferService.js"; -import StatusService from "../service/infrastructure/statusService.js"; import NotificationUtils from "../service/infrastructure/notificationUtils.js"; import NotificationService from "../service/infrastructure/notificationService.js"; -import { NotificationsService } from "@/service/index.js"; +import { NotificationsService, StatusService } from "@/service/index.js"; import ErrorService from "../service/infrastructure/errorService.js"; import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; diff --git a/server/src/service/index.ts b/server/src/service/index.ts index 08c02f397..fbba7714d 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -3,3 +3,4 @@ export * from "@/service/infrastructure/networkService.js"; export * from "@/service/infrastructure/notificationProviders/webhook.js"; export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; export * from "@/service/infrastructure/notificationsService.js"; +export * from "@/service/infrastructure/statusService.js"; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index c2a9e1267..21ee6e388 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -1,18 +1,19 @@ const SERVICE_NAME = "JobQueueHelper"; import type { Monitor } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; -import { INetworkService, INotificationsService } from "@/service/index.js"; +import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js"; class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; private db: any; private logger: any; private networkService: INetworkService; - private statusService: any; + private statusService: IStatusService; private notificationService: any; private notificationsService: INotificationsService; private checkService: any; private buffer: any; + private incidentService: any; constructor({ db, @@ -23,15 +24,17 @@ class SuperSimpleQueueHelper { notificationsService, checkService, buffer, + incidentService, }: { db: any; logger: any; networkService: INetworkService; - statusService: any; + statusService: IStatusService; notificationService: any; notificationsService: INotificationsService; checkService: any; buffer: any; + incidentService: any; }) { this.db = db; this.logger = logger; @@ -41,6 +44,7 @@ class SuperSimpleQueueHelper { this.checkService = checkService; this.buffer = buffer; this.notificationsService = notificationsService; + this.incidentService = incidentService; } get serviceName() { @@ -82,10 +86,9 @@ class SuperSimpleQueueHelper { // Step 4. Update monitor status const statusChangeResult = await this.statusService.updateMonitorStatus(status, check); - // Step 5 handle incidents and notifications - if (statusChangeResult.statusChanged === true) { - this.notificationsService.handleNotifications(monitor, status); - } + // Step 5 handle notifications + + this.notificationsService.handleNotifications(monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged); // this.notificationService // .handleNotifications({ diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index f9fdbfea8..3dc5fe65e 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,11 +1,49 @@ -import type { Monitor, MonitorStatusResponse } from "@/types/index.js"; +import type { HardwareStatusPayload, Monitor, MonitorStatusResponse } from "@/types/index.js"; export interface INotificationsService { - handleNotifications: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => Promise; + handleNotifications: ( + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + prevStatus: boolean | undefined, + statusChanged: boolean + ) => Promise; } export class NotificationsService implements INotificationsService { - handleNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - console.log(JSON.stringify(monitor, null, 2)); - console.log(JSON.stringify(monitorStatusResponse, null, 2)); + handleNotifications = async ( + monitor: Monitor, + monitorStatusResponse: MonitorStatusResponse, + prevStatus: boolean | undefined, + statusChanged: boolean + ) => { + const { type } = monitor; + const payload = monitorStatusResponse.payload as HardwareStatusPayload; + // If this is a non-hardeware type monitor and status did not change, we're done + if (type !== "hardware" && statusChanged === false) return false; + // if prevStatus is undefined, monitor is resuming, we're done + if (type !== "hardware" && prevStatus === undefined) return false; + + // Deal with hardware thresholds + if (type === "hardware") { + const thresholds = monitor.thresholds; + + if (thresholds === undefined) return false; // No thresholds set, we're done + const metrics = payload?.data ?? null; + if (metrics === null) return false; // No metrics, we're done + + // We shoul dsend a notificaiton + + console.log(JSON.stringify(monitor, null, 2)); + console.log(JSON.stringify(monitorStatusResponse, null, 2)); + // const [alerts, discordContent] = await this.notificationUtils.buildHardwareAlerts(networkResponse); + // if (alerts.length === 0) return false; + + // const { subject, html } = await this.notificationUtils.buildHardwareEmail(networkResponse, alerts); + // const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts, monitor); + // const webhookBody = await this.notificationUtils.buildHardwareWebhookBody(alerts, monitor); + // const success = await this.notifyAll({ notificationIDs, subject, html, content, discordContent, webhookBody }); + return true; + } + + return false; }; } diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index c9aab706b..3d9591c02 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -12,7 +12,17 @@ import type { } from "@/types/index.js"; const SERVICE_NAME = "StatusService"; -class StatusService { +export interface IStatusService { + updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }): Promise; + getStatusString(status: boolean | undefined): string; + handleIncidentForCheck(check: any, monitor: Monitor, action: any, errorContext?: string): Promise; + updateMonitorStatus( + statusResponse: MonitorStatusResponse, + check: Check + ): Promise; +} + +export class StatusService implements IStatusService { static SERVICE_NAME = SERVICE_NAME; private db: any; private logger: any; @@ -108,11 +118,10 @@ class StatusService { return "unknown"; }; - handleIncidentForCheck = async (check: any, monitor: Monitor, action: any, errorContext = "incident handling") => { + handleIncidentForCheck = async (check: Check, monitor: Monitor, action: any, errorContext = "incident handling") => { try { - let savedCheck = check; - - if (!check._id) { + let savedCheck; + if (!check.id) { try { const checkModel = new CheckModel(check); savedCheck = await checkModel.save(); @@ -130,7 +139,7 @@ class StatusService { } } - if (savedCheck && savedCheck._id) { + if (savedCheck && savedCheck.id) { try { this.buffer.addIncidentToBuffer({ monitor, check: savedCheck, action }); } catch (incidentError: any) { @@ -167,13 +176,10 @@ class StatusService { this.updateRunningStats({ monitor, networkResponse: statusResponse }); // If the status window size has changed, empty - while (monitor.statusWindow.length > monitor.statusWindowSize) { - monitor.statusWindow.shift(); - } - // Update status sliding window + monitor.statusWindow = monitor.statusWindow || []; monitor.statusWindow.push(status); - if (monitor.statusWindow.length > monitor.statusWindowSize) { + while (monitor.statusWindow.length > monitor.statusWindowSize) { monitor.statusWindow.shift(); } @@ -181,9 +187,9 @@ class StatusService { monitor.status = status; } + const prevStatus = monitor.status; let newStatus = monitor.status; let statusChanged = false; - const prevStatus = monitor.status; // Return early if not enough data points if (monitor.statusWindow.length < monitor.statusWindowSize) { @@ -316,4 +322,3 @@ class StatusService { } }; } -export default StatusService; diff --git a/server/src/types/network.ts b/server/src/types/network.ts index 10f89842f..6bed40316 100644 --- a/server/src/types/network.ts +++ b/server/src/types/network.ts @@ -13,7 +13,7 @@ import type { MonitorType, } from "@/types/index.js"; -export interface MonitorStatusResponse { +export interface MonitorStatusResponse { monitorId: string; teamId: string; type: MonitorType; From c2487f013437b1c6e85adedf2d029c40eb3fb78f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 18 Jan 2026 23:54:42 +0000 Subject: [PATCH 269/400] alerts --- .../infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts | 3 --- .../src/service/infrastructure/notificationProviders/utils.ts | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 21ee6e388..d1315e151 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -24,7 +24,6 @@ class SuperSimpleQueueHelper { notificationsService, checkService, buffer, - incidentService, }: { db: any; logger: any; @@ -34,7 +33,6 @@ class SuperSimpleQueueHelper { notificationsService: INotificationsService; checkService: any; buffer: any; - incidentService: any; }) { this.db = db; this.logger = logger; @@ -44,7 +42,6 @@ class SuperSimpleQueueHelper { this.checkService = checkService; this.buffer = buffer; this.notificationsService = notificationsService; - this.incidentService = incidentService; } get serviceName() { diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index 9a1b2e7f8..ad8dc14ae 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -8,7 +8,7 @@ const buildHardwareAlerts = async (clientHost: string, monitor: Monitor, network const metrics = payload.data || {}; const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics; - const alerts = { + const alerts: Record = { cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false, memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false, disk: disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold) ?? false, From 7d6bc4b2dc80a25ef31840c9595974a9f1345029 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 00:48:20 +0000 Subject: [PATCH 270/400] notificaitons provider --- server/src/config/services.ts | 7 ++- server/src/repositories/index.ts | 3 + .../notifications/INotificationsRepository.ts | 7 +++ .../MongoNotificationsRepository.ts | 40 +++++++++++++ .../src/service/business/incidentService.js | 12 ++-- .../SuperSimpleQueueHelper.ts | 1 - .../notificationProviders/utils.ts | 56 +++++++++++++++---- .../infrastructure/notificationsService.ts | 33 +++++++++-- 8 files changed, 134 insertions(+), 25 deletions(-) create mode 100644 server/src/repositories/notifications/INotificationsRepository.ts create mode 100644 server/src/repositories/notifications/MongoNotificationsRepository.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index ed1224ec7..e25ee1706 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -75,6 +75,7 @@ import { MongoInvitesRepository, MongoRecoveryTokensRepository, MongoSettingsRepository, + MongoNotificationsRepository, IMonitorsRepository, IChecksRepository, IMonitorStatsRepository, @@ -83,6 +84,7 @@ import { IInvitesRepository, IRecoveryTokensRepository, ISettingsRepository, + INotificationsRepository, } from "@/repositories/index.js"; export type InitializedSerivces = { @@ -116,6 +118,7 @@ export type InitializedSerivces = { invitesRepository: IInvitesRepository; recoveryTokensRepository: IRecoveryTokensRepository; settingsRepository: ISettingsRepository; + notificationsRepository: INotificationsRepository; }; export const initializeServices = async ({ @@ -171,7 +174,7 @@ export const initializeServices = async ({ const invitesRepository = new MongoInvitesRepository(); const recoveryTokensRepository = new MongoRecoveryTokensRepository(); const settingsRepository = new MongoSettingsRepository(); - + const notificationsRepository = new MongoNotificationsRepository(); const networkService = new NetworkService({ axios, got, @@ -225,7 +228,7 @@ export const initializeServices = async ({ notificationUtils, }); - const notificationsService = new NotificationsService(); + const notificationsService = new NotificationsService(notificationsRepository); const superSimpleQueueHelper = new SuperSimpleQueueHelper({ db, diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 09a686595..1a3199081 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -21,3 +21,6 @@ export { default as MongoRecoveryTokensRepository } from "@/repositories/recover export * from "@/repositories/settings/ISettingsRepository.js"; export { default as MongoSettingsRepository } from "@/repositories/settings/MongoSettingsRepository.js"; + +export * from "@/repositories/notifications/INotificationsRepository.js"; +export { default as MongoNotificationsRepository } from "@/repositories/notifications/MongoNotificationsRepository.js"; diff --git a/server/src/repositories/notifications/INotificationsRepository.ts b/server/src/repositories/notifications/INotificationsRepository.ts new file mode 100644 index 000000000..0244972e4 --- /dev/null +++ b/server/src/repositories/notifications/INotificationsRepository.ts @@ -0,0 +1,7 @@ +import type { Notification } from "@/types/index.js"; +export interface INotificationsRepository { + // create + // fetch + // update + // delete +} diff --git a/server/src/repositories/notifications/MongoNotificationsRepository.ts b/server/src/repositories/notifications/MongoNotificationsRepository.ts new file mode 100644 index 000000000..51839c125 --- /dev/null +++ b/server/src/repositories/notifications/MongoNotificationsRepository.ts @@ -0,0 +1,40 @@ +import mongoose from "mongoose"; +import { NotificationModel, type NotificationDocument } from "@/db/models/index.js"; +import { INotificationsRepository } from "@/repositories/index.js"; +import type { Notification } from "@/types/index.js"; + +class MongoNotificationsRepository implements INotificationsRepository { + private mapDocuments = (documents: NotificationDocument[]): Notification[] => { + if (!documents?.length) { + return []; + } + return documents.map((doc) => this.toEntity(doc)); + }; + + private toEntity = (doc: NotificationDocument): Notification => { + const toStringId = (value: mongoose.Types.ObjectId | string): string => { + return value instanceof mongoose.Types.ObjectId ? value.toString() : value; + }; + + const toDateString = (value: Date | string): string => { + return value instanceof Date ? value.toISOString() : value; + }; + + return { + id: toStringId(doc._id), + userId: toStringId(doc.userId), + teamId: toStringId(doc.teamId), + type: doc.type, + notificationName: doc.notificationName, + address: doc.address ?? undefined, + phone: doc.phone ?? undefined, + homeserverUrl: doc.homeserverUrl ?? undefined, + roomId: doc.roomId ?? undefined, + accessToken: doc.accessToken ?? undefined, + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), + }; + }; +} + +export default MongoNotificationsRepository; diff --git a/server/src/service/business/incidentService.js b/server/src/service/business/incidentService.js index 71e60d279..db89caa68 100644 --- a/server/src/service/business/incidentService.js +++ b/server/src/service/business/incidentService.js @@ -305,7 +305,7 @@ class IncidentService { const groupedByMonitor = {}; for (const item of createItems) { - if (!item.monitor || !item.monitor._id || !item.check || !item.check._id) { + if (!item.monitor || !item.monitor.id || !item.check || !item.check.id) { this.logger.warn({ service: this.SERVICE_NAME, method: "processIncidentsFromBuffer", @@ -315,7 +315,7 @@ class IncidentService { continue; } - const monitorId = item.monitor._id.toString(); + const monitorId = item.monitor.id.toString(); if (!groupedByMonitor[monitorId]) { groupedByMonitor[monitorId] = []; } @@ -341,20 +341,20 @@ class IncidentService { for (const item of items) { checksToAddToIncidents.push({ incidentId, - checkId: item.check._id.toString(), + checkId: item.check.id.toString(), }); } } else { const firstItem = items[0]; const incidentData = { - monitorId: firstItem.monitor._id, + monitorId: firstItem.monitor.id, teamId: firstItem.monitor.teamId, type: firstItem.monitor.type, startTime: new Date(), status: true, message: firstItem.check.message || null, statusCode: firstItem.check.statusCode || null, - checks: [firstItem.check._id], + checks: [firstItem.check.id], }; newIncidentsToCreate.push({ @@ -380,7 +380,7 @@ class IncidentService { for (const remainingItem of item.remainingChecks) { checksToAddToIncidents.push({ incidentId, - checkId: remainingItem.check._id.toString(), + checkId: remainingItem.check.id, }); } } diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index d1315e151..41885a146 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -84,7 +84,6 @@ class SuperSimpleQueueHelper { const statusChangeResult = await this.statusService.updateMonitorStatus(status, check); // Step 5 handle notifications - this.notificationsService.handleNotifications(monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged); // this.notificationService diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index ad8dc14ae..7cba00a5d 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -1,6 +1,10 @@ import { Monitor, HardwareStatusPayload, MonitorStatusResponse } from "@/types/index.js"; -const buildHardwareAlerts = async (clientHost: string, monitor: Monitor, networkResponse: MonitorStatusResponse) => { +export const buildHardwareAlerts = ( + clientHost: string, + monitor: Monitor, + networkResponse: MonitorStatusResponse +): { alertsToSend: string[]; discordPayload: any } => { const thresholds = monitor.thresholds || {}; const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; @@ -54,15 +58,22 @@ const buildHardwareAlerts = async (clientHost: string, monitor: Monitor, network ], }), }; - const alertTypes = ["cpu", "memory", "disk"]; + const alertTypes = ["cpu", "memory", "disk"] as const; + const alertThresholdKeyMap: Record<(typeof alertTypes)[number], "cpuAlertThreshold" | "memoryAlertThreshold" | "diskAlertThreshold"> = { + cpu: "cpuAlertThreshold", + memory: "memoryAlertThreshold", + disk: "diskAlertThreshold", + }; for (const type of alertTypes) { + const thresholdKey = alertThresholdKeyMap[type]; // Iterate over each alert type to see if any need to be decremented if (alerts[type] === true) { - monitor[`${type}AlertThreshold`]--; // Decrement threshold if an alert is triggered + const nextValue = ((monitor[thresholdKey] ?? monitor.alertThreshold) as number) - 1; + monitor[thresholdKey] = nextValue; // Decrement threshold if an alert is triggered - if (monitor[`${type}AlertThreshold`] <= 0) { + if (monitor[thresholdKey] <= 0) { // If threshold drops below 0, reset and send notification - monitor[`${type}AlertThreshold`] = monitor.alertThreshold; + monitor[thresholdKey] = monitor.alertThreshold; const formatAlert = { cpu: () => `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, @@ -70,7 +81,7 @@ const buildHardwareAlerts = async (clientHost: string, monitor: Monitor, network `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, disk: () => `Your current disk usage: ${disk - .map((d, idx) => `(Disk${idx}: ${(d.usage_percent * 100).toFixed(0)}%)`) + .map((d, idx) => `(Disk${idx}: ${(d?.usage_percent ?? 0 * 100).toFixed(0)}%)`) .join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, }; alertsToSend.push(formatAlert[type]()); @@ -79,13 +90,38 @@ const buildHardwareAlerts = async (clientHost: string, monitor: Monitor, network } } const discordPayload = discordEmbeds.length ? { embeds: discordEmbeds } : null; - return [alertsToSend, discordPayload]; + return { alertsToSend, discordPayload }; }; -const buildHardwareNotificationMessage = (alerts: any, monitor: Monitor) => { - const { clientHost } = this.settingsService.getSettings(); +const buildHardwareNotificationMessage = (clientHost: string, alerts: any, monitor: Monitor) => { const alertsHeader = [`Monitor: ${monitor.name}`, `URL: ${monitor.url}`]; - const alertFooter = [`Go to incident: ${clientHost}/infrastructure/${monitor._id}`]; + const alertFooter = [`Go to incident: ${clientHost}/infrastructure/${monitor.id}`]; const alertText = alerts.length > 0 ? [...alertsHeader, ...alerts, ...alertFooter] : []; return alertText.map((alert) => alert).join("\n"); }; + +export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: MonitorStatusResponse): boolean => { + const thresholds = monitor.thresholds || {}; + const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; + + const payload = networkResponse?.payload as HardwareStatusPayload; + const metrics = payload.data || {}; + const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics; + + const cpuBreach = cpuThreshold !== -1 && cpuUsage > cpuThreshold; + if (cpuBreach && ((monitor.cpuAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) { + return true; + } + + const memoryBreach = memoryThreshold !== -1 && memoryUsage > memoryThreshold; + if (memoryBreach && ((monitor.memoryAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) { + return true; + } + + const diskBreach = disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold); + if (diskBreach && ((monitor.diskAlertThreshold ?? monitor.alertThreshold) as number) - 1 <= 0) { + return true; + } + + return false; +}; diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 3dc5fe65e..2070b0c4f 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,4 +1,7 @@ -import type { HardwareStatusPayload, Monitor, MonitorStatusResponse } from "@/types/index.js"; +import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification } from "@/types/index.js"; +import { NotificationModel } from "@/db/models/index.js"; +import { buildHardwareAlerts, shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js"; +import { INotificationsRepository } from "@/repositories/index.js"; export interface INotificationsService { handleNotifications: ( monitor: Monitor, @@ -9,6 +12,24 @@ export interface INotificationsService { } export class NotificationsService implements INotificationsService { + private notificationsRepository: INotificationsRepository; + constructor(notificationsRepository: INotificationsRepository) { + this.notificationsRepository = notificationsRepository; + } + + private send = (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + console.log(notification); + }; + + private sendNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const notifications = await this.notificationsRepository.findByMonitorId(monitor.id); + console.log({ notifications }); + const tasks = notifications.map((notification) => { + this.send(notification, monitor, monitorStatusResponse); + }); + return true; + }; + handleNotifications = async ( monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, @@ -32,18 +53,18 @@ export class NotificationsService implements INotificationsService { // We shoul dsend a notificaiton + const shouldSend = shouldSendHardwareAlert(monitor, monitorStatusResponse); + if (shouldSend === false) return false; console.log(JSON.stringify(monitor, null, 2)); console.log(JSON.stringify(monitorStatusResponse, null, 2)); - // const [alerts, discordContent] = await this.notificationUtils.buildHardwareAlerts(networkResponse); - // if (alerts.length === 0) return false; - // const { subject, html } = await this.notificationUtils.buildHardwareEmail(networkResponse, alerts); // const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts, monitor); // const webhookBody = await this.notificationUtils.buildHardwareWebhookBody(alerts, monitor); // const success = await this.notifyAll({ notificationIDs, subject, html, content, discordContent, webhookBody }); - return true; + return await this.sendNotifications(monitor, monitorStatusResponse); } - return false; + // We should send a notification for non-hardware monitor status change + return await this.sendNotifications(monitor, monitorStatusResponse); }; } From 3b0326457ffd100999a867fbb398ab5ea77df8e6 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Mon, 19 Jan 2026 13:49:08 -0500 Subject: [PATCH 271/400] docs: update OpenAPI specification to match current API ## Changes ### New Endpoints Added - `/incidents/team` - Get incidents by team with filtering - `/incidents/team/summary` - Get incident summary statistics - `/incidents/{incidentId}` - Get incident by ID - `/incidents/{incidentId}/resolve` - Manually resolve incident - `/queue/jobs` (GET) - List queue jobs (admin only) - `/queue/jobs` (POST) - Add job to queue (admin only) - `/queue/metrics` - Get queue metrics (admin only) - `/monitors/team/groups` - Get monitor groups by team - `/monitors/pagespeed/details/{monitorId}` - Get PageSpeed details - `/checks/{action}` - Acknowledge all checks for all monitors - `/checks/{action}/{monitorId}` - Acknowledge checks for specific monitor ### Endpoints Fixed - `/monitors/export` changed to `/monitors/export/json` (matches actual route) - Removed non-existent `/monitors/stats/{monitorId}` endpoint - Removed non-existent `/monitors/team/summary` endpoint - Split optional path parameter into two valid endpoints for checks ### New Schemas Added - `Incident` - Complete incident model - `IncidentSummary` - Incident statistics summary - `QueueJob` - Queue job information - `QueueMetrics` - Queue metrics data - `Check` - Complete check result model - `CheckTimings` - HTTP timing information - `CheckCpuInfo` - CPU metrics for infrastructure checks - `CheckMemoryInfo` - Memory metrics for infrastructure checks - `CheckDiskInfo` - Disk metrics for infrastructure checks - `CheckHostInfo` - Host information for infrastructure checks - `CheckErrorInfo` - Error information from checks - `CheckCaptureInfo` - Capture agent information - `CheckNetworkInfo` - Network interface metrics - `CheckAudits` - PageSpeed audit results - `LighthouseAudit` - Individual Lighthouse audit result ### Responses Added - `BadRequest` - 400 error response ### Other Fixes - Simplified server variables to fix OpenAPI 3.1.0 validation - Added request body to `/checks/check/{checkId}` endpoint - Added "incidents" tag to tags array ## Benefits - OpenAPI specification now accurately reflects the actual API - All endpoints have proper request/response schemas - Validation passes with swagger-cli - Better API documentation for consumers --- server/openapi.json | 1327 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 1293 insertions(+), 34 deletions(-) diff --git a/server/openapi.json b/server/openapi.json index 6cc2e7de0..d104501d0 100644 --- a/server/openapi.json +++ b/server/openapi.json @@ -21,13 +21,9 @@ "description": "Local Development Server", "variables": { "PORT": { - "description": "API Port", - "enum": ["52345"], "default": "52345" }, "API_PATH": { - "description": "API Base Path", - "enum": ["api/v1"], "default": "api/v1" } } @@ -37,8 +33,6 @@ "description": "Checkmate Demo Server", "variables": { "API_PATH": { - "description": "API Base Path", - "enum": ["api/v1"], "default": "api/v1" } } @@ -88,6 +82,10 @@ { "name": "diagnostic", "description": "System health and performance diagnostics" + }, + { + "name": "incidents", + "description": "Incident management and resolution" } ], "paths": { @@ -731,28 +729,54 @@ ] } }, - "/monitors/team/summary": { + "/monitors/team/groups": { "get": { "tags": ["monitors"], - "summary": "Get monitors summary", - "description": "Get team monitors with summary statistics", - "parameters": [ - { - "name": "type", - "in": "query", - "description": "Filter by monitor type", - "schema": { - "type": "array", - "items": { - "type": "string", - "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"] - } - } - } - ], + "summary": "Get monitor groups by team", + "description": "Get all monitor groups for the authenticated user's team", "responses": { "200": { - "description": "Monitor summary retrieved successfully" + "description": "Monitor groups retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Monitor groups retrieved successfully" + }, + "data": { + "type": "array", + "items": { + "type": "object", + "properties": { + "_id": { + "type": "string" + }, + "name": { + "type": "string" + }, + "monitors": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" }, "500": { "$ref": "#/components/responses/InternalServerError" @@ -992,24 +1016,78 @@ ] } }, - "/monitors/stats/{monitorId}": { + "/monitors/pagespeed/details/{monitorId}": { "get": { "tags": ["monitors"], - "summary": "Get monitor statistics", - "description": "Get comprehensive statistics for a monitor", + "summary": "Get PageSpeed monitor details", + "description": "Get detailed PageSpeed monitoring data including performance metrics and Core Web Vitals", "parameters": [ { "name": "monitorId", "in": "path", "required": true, + "description": "The PageSpeed monitor ID", "schema": { "type": "string" } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Sort order for results", + "schema": { + "type": "string", + "enum": ["asc", "desc"], + "default": "desc" + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of results to return", + "schema": { + "type": "integer", + "default": 50 + } } ], "responses": { "200": { - "description": "Monitor statistics retrieved successfully" + "description": "PageSpeed details retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "PageSpeed details retrieved successfully" + }, + "data": { + "type": "object", + "properties": { + "monitor": { + "$ref": "#/components/schemas/Monitor" + }, + "checks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Check" + } + } + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" }, "404": { "$ref": "#/components/responses/NotFound" @@ -1078,22 +1156,44 @@ ] } }, - "/monitors/export": { + "/monitors/export/json": { "get": { "tags": ["monitors"], - "summary": "Export monitors to CSV", - "description": "Export all monitors to CSV format", + "summary": "Export monitors to JSON", + "description": "Export all monitors to JSON format (admin/superadmin only)", "responses": { "200": { - "description": "CSV file generated successfully", + "description": "JSON file generated successfully", "content": { - "text/csv": { + "application/json": { "schema": { - "type": "string" + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Monitors exported successfully" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Monitor" + } + } + } } } } }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, "500": { "$ref": "#/components/responses/InternalServerError" } @@ -2030,6 +2130,21 @@ } } ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "ack": { + "type": "boolean", + "description": "Acknowledgement status" + } + } + } + } + } + }, "responses": { "200": { "$ref": "#/components/responses/Success" @@ -2048,6 +2163,164 @@ ] } }, + "/checks/{action}": { + "put": { + "tags": ["checks"], + "summary": "Acknowledge all checks for all monitors", + "description": "Acknowledge all checks across all monitors for the team", + "parameters": [ + { + "name": "action", + "in": "path", + "required": true, + "description": "Action to perform (e.g., 'ack')", + "schema": { + "type": "string", + "enum": ["ack"] + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["ack"], + "properties": { + "ack": { + "type": "boolean", + "description": "Acknowledgement status to set" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Checks acknowledged successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "All checks acknowledged successfully" + }, + "data": { + "type": "object", + "description": "Updated checks information" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/checks/{action}/{monitorId}": { + "put": { + "tags": ["checks"], + "summary": "Acknowledge all checks for a specific monitor", + "description": "Acknowledge all checks for a specific monitor", + "parameters": [ + { + "name": "action", + "in": "path", + "required": true, + "description": "Action to perform (e.g., 'ack')", + "schema": { + "type": "string", + "enum": ["ack"] + } + }, + { + "name": "monitorId", + "in": "path", + "required": true, + "description": "Monitor ID to acknowledge checks for", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["ack"], + "properties": { + "ack": { + "type": "boolean", + "description": "Acknowledgement status to set" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Checks acknowledged successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "All checks acknowledged successfully" + }, + "data": { + "type": "object", + "description": "Updated checks information" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "/maintenance-window": { "post": { "tags": ["maintenance-window"], @@ -2291,6 +2564,142 @@ ] } }, + "/queue/jobs": { + "get": { + "tags": ["queue"], + "summary": "Get queue jobs", + "description": "Retrieve all jobs in the queue (admin/superadmin only)", + "responses": { + "200": { + "description": "Queue jobs retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Queue jobs fetched successfully" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/QueueJob" + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + }, + "post": { + "tags": ["queue"], + "summary": "Add job to queue", + "description": "Add a new job to the queue (admin/superadmin only)", + "responses": { + "200": { + "description": "Job added successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Job added to queue successfully" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/queue/metrics": { + "get": { + "tags": ["queue"], + "summary": "Get queue metrics", + "description": "Retrieve queue metrics (admin/superadmin only)", + "responses": { + "200": { + "description": "Queue metrics retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Queue metrics fetched successfully" + }, + "data": { + "$ref": "#/components/schemas/QueueMetrics" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "/queue/health": { "get": { "tags": ["queue"], @@ -2618,6 +3027,286 @@ } ] } + }, + "/incidents/team": { + "get": { + "tags": ["incidents"], + "summary": "Get incidents by team", + "description": "Retrieve all incidents for the authenticated user's team with optional filtering", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Filter by incident status (true = ongoing, false = resolved)", + "schema": { + "type": "boolean" + } + }, + { + "name": "monitorId", + "in": "query", + "description": "Filter by monitor ID", + "schema": { + "type": "string" + } + }, + { + "name": "page", + "in": "query", + "description": "Page number for pagination", + "schema": { + "type": "integer", + "default": 1 + } + }, + { + "name": "limit", + "in": "query", + "description": "Number of items per page", + "schema": { + "type": "integer", + "default": 10 + } + }, + { + "name": "sortOrder", + "in": "query", + "description": "Sort order for results", + "schema": { + "type": "string", + "enum": ["asc", "desc"], + "default": "desc" + } + } + ], + "responses": { + "200": { + "description": "Incidents retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Incidents retrieved successfully" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Incident" + } + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/incidents/team/summary": { + "get": { + "tags": ["incidents"], + "summary": "Get incident summary", + "description": "Retrieve a summary of incidents for the authenticated user's team", + "parameters": [ + { + "name": "dateRange", + "in": "query", + "description": "Date range for summary", + "schema": { + "type": "string", + "enum": ["day", "week", "month"], + "default": "week" + } + } + ], + "responses": { + "200": { + "description": "Incident summary retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Incident summary retrieved successfully" + }, + "data": { + "$ref": "#/components/schemas/IncidentSummary" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/incidents/{incidentId}": { + "get": { + "tags": ["incidents"], + "summary": "Get incident by ID", + "description": "Retrieve a specific incident by its ID", + "parameters": [ + { + "name": "incidentId", + "in": "path", + "required": true, + "description": "The incident ID", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Incident retrieved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Incident retrieved successfully" + }, + "data": { + "$ref": "#/components/schemas/Incident" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/incidents/{incidentId}/resolve": { + "put": { + "tags": ["incidents"], + "summary": "Resolve incident manually", + "description": "Manually resolve an ongoing incident. Requires admin or superadmin role.", + "parameters": [ + { + "name": "incidentId", + "in": "path", + "required": true, + "description": "The incident ID to resolve", + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "comment": { + "type": "string", + "description": "Optional comment about the resolution", + "example": "Issue resolved by restarting the service" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Incident resolved successfully", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "msg": { + "type": "string", + "example": "Incident resolved successfully" + }, + "data": { + "$ref": "#/components/schemas/Incident" + } + } + } + } + } + }, + "401": { + "$ref": "#/components/responses/Unauthorized" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } } }, "components": { @@ -2680,6 +3369,16 @@ } } }, + "BadRequest": { + "description": "Invalid request parameters", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ErrorResponse" + } + } + } + }, "InternalServerError": { "description": "Internal server error", "content": { @@ -3375,6 +4074,566 @@ } } } + }, + "Incident": { + "type": "object", + "properties": { + "_id": { + "type": "string", + "description": "Unique incident identifier" + }, + "monitorId": { + "type": "string", + "description": "ID of the associated monitor" + }, + "teamId": { + "type": "string", + "description": "ID of the team" + }, + "startTime": { + "type": "string", + "format": "date-time", + "description": "When the incident started" + }, + "endTime": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the incident was resolved (null if ongoing)" + }, + "status": { + "type": "boolean", + "description": "Incident status (true = ongoing, false = resolved)" + }, + "message": { + "type": "string", + "nullable": true, + "description": "Error message or description" + }, + "statusCode": { + "type": "integer", + "nullable": true, + "description": "HTTP status code if applicable" + }, + "resolutionType": { + "type": "string", + "enum": ["automatic", "manual"], + "nullable": true, + "description": "How the incident was resolved" + }, + "resolvedBy": { + "type": "string", + "nullable": true, + "description": "User ID who resolved the incident (for manual resolution)" + }, + "comment": { + "type": "string", + "nullable": true, + "description": "Resolution comment" + }, + "checks": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of check IDs associated with this incident" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "IncidentSummary": { + "type": "object", + "properties": { + "totalIncidents": { + "type": "integer", + "description": "Total number of incidents" + }, + "activeIncidents": { + "type": "integer", + "description": "Number of ongoing incidents" + }, + "resolvedIncidents": { + "type": "integer", + "description": "Number of resolved incidents" + }, + "averageResolutionTime": { + "type": "number", + "description": "Average time to resolve incidents in milliseconds" + }, + "incidentsByMonitor": { + "type": "array", + "items": { + "type": "object", + "properties": { + "monitorId": { + "type": "string" + }, + "monitorName": { + "type": "string" + }, + "count": { + "type": "integer" + } + } + } + } + } + }, + "QueueJob": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "Job ID" + }, + "name": { + "type": "string", + "description": "Job name" + }, + "data": { + "type": "object", + "description": "Job payload data" + }, + "status": { + "type": "string", + "enum": ["waiting", "active", "completed", "failed", "delayed"], + "description": "Current job status" + }, + "progress": { + "type": "number", + "description": "Job progress percentage" + }, + "attemptsMade": { + "type": "integer", + "description": "Number of attempts made" + }, + "timestamp": { + "type": "string", + "format": "date-time", + "description": "When the job was created" + }, + "processedOn": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the job started processing" + }, + "finishedOn": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the job finished" + } + } + }, + "QueueMetrics": { + "type": "object", + "properties": { + "waiting": { + "type": "integer", + "description": "Number of jobs waiting" + }, + "active": { + "type": "integer", + "description": "Number of jobs currently processing" + }, + "completed": { + "type": "integer", + "description": "Number of completed jobs" + }, + "failed": { + "type": "integer", + "description": "Number of failed jobs" + }, + "delayed": { + "type": "integer", + "description": "Number of delayed jobs" + } + } + }, + "Check": { + "type": "object", + "description": "A monitoring check result", + "properties": { + "_id": { + "type": "string", + "description": "Check ID" + }, + "metadata": { + "type": "object", + "properties": { + "monitorId": { + "type": "string", + "description": "Associated monitor ID" + }, + "teamId": { + "type": "string", + "description": "Associated team ID" + }, + "type": { + "type": "string", + "enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"], + "description": "Monitor type" + } + }, + "required": ["monitorId", "teamId", "type"] + }, + "status": { + "type": "boolean", + "description": "Check status (true = up, false = down)" + }, + "responseTime": { + "type": "number", + "description": "Response time in milliseconds" + }, + "statusCode": { + "type": "integer", + "description": "HTTP status code (for HTTP monitors)" + }, + "message": { + "type": "string", + "description": "Status message or error description" + }, + "timings": { + "$ref": "#/components/schemas/CheckTimings" + }, + "cpu": { + "$ref": "#/components/schemas/CheckCpuInfo" + }, + "memory": { + "$ref": "#/components/schemas/CheckMemoryInfo" + }, + "disk": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CheckDiskInfo" + } + }, + "host": { + "$ref": "#/components/schemas/CheckHostInfo" + }, + "errors": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CheckErrorInfo" + } + }, + "capture": { + "$ref": "#/components/schemas/CheckCaptureInfo" + }, + "net": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CheckNetworkInfo" + } + }, + "accessibility": { + "type": "number", + "description": "PageSpeed accessibility score (0-100)" + }, + "bestPractices": { + "type": "number", + "description": "PageSpeed best practices score (0-100)" + }, + "seo": { + "type": "number", + "description": "PageSpeed SEO score (0-100)" + }, + "performance": { + "type": "number", + "description": "PageSpeed performance score (0-100)" + }, + "audits": { + "$ref": "#/components/schemas/CheckAudits" + }, + "ack": { + "type": "boolean", + "description": "Whether the check has been acknowledged" + }, + "ackAt": { + "type": "string", + "format": "date-time", + "nullable": true, + "description": "When the check was acknowledged" + }, + "expiry": { + "type": "string", + "format": "date-time", + "description": "When the check expires" + }, + "createdAt": { + "type": "string", + "format": "date-time" + }, + "updatedAt": { + "type": "string", + "format": "date-time" + } + } + }, + "CheckTimings": { + "type": "object", + "description": "Detailed timing information for HTTP checks", + "properties": { + "start": { + "type": "number" + }, + "socket": { + "type": "number" + }, + "lookup": { + "type": "number" + }, + "connect": { + "type": "number" + }, + "secureConnect": { + "type": "number" + }, + "upload": { + "type": "number" + }, + "response": { + "type": "number" + }, + "end": { + "type": "number" + }, + "phases": { + "type": "object", + "properties": { + "wait": { + "type": "number" + }, + "dns": { + "type": "number" + }, + "tcp": { + "type": "number" + }, + "tls": { + "type": "number" + }, + "request": { + "type": "number" + }, + "firstByte": { + "type": "number" + }, + "download": { + "type": "number" + }, + "total": { + "type": "number" + } + } + } + } + }, + "CheckCpuInfo": { + "type": "object", + "description": "CPU information for infrastructure checks", + "properties": { + "physical_core": { + "type": "integer" + }, + "logical_core": { + "type": "integer" + }, + "frequency": { + "type": "number" + }, + "temperature": { + "type": "array", + "items": { + "type": "number" + } + }, + "free_percent": { + "type": "number" + }, + "usage_percent": { + "type": "number" + } + } + }, + "CheckMemoryInfo": { + "type": "object", + "description": "Memory information for infrastructure checks", + "properties": { + "total_bytes": { + "type": "number" + }, + "available_bytes": { + "type": "number" + }, + "used_bytes": { + "type": "number" + }, + "usage_percent": { + "type": "number" + } + } + }, + "CheckDiskInfo": { + "type": "object", + "description": "Disk information for infrastructure checks", + "properties": { + "device": { + "type": "string" + }, + "mountpoint": { + "type": "string" + }, + "read_speed_bytes": { + "type": "number" + }, + "write_speed_bytes": { + "type": "number" + }, + "total_bytes": { + "type": "number" + }, + "free_bytes": { + "type": "number" + }, + "usage_percent": { + "type": "number" + } + } + }, + "CheckHostInfo": { + "type": "object", + "description": "Host information for infrastructure checks", + "properties": { + "os": { + "type": "string" + }, + "platform": { + "type": "string" + }, + "kernel_version": { + "type": "string" + } + } + }, + "CheckErrorInfo": { + "type": "object", + "description": "Error information from checks", + "properties": { + "metric": { + "type": "array", + "items": { + "type": "string" + } + }, + "err": { + "type": "string" + } + } + }, + "CheckCaptureInfo": { + "type": "object", + "description": "Capture agent information", + "properties": { + "version": { + "type": "string" + }, + "mode": { + "type": "string" + } + } + }, + "CheckNetworkInfo": { + "type": "object", + "description": "Network interface information", + "properties": { + "name": { + "type": "string" + }, + "bytes_sent": { + "type": "number" + }, + "bytes_recv": { + "type": "number" + }, + "packets_sent": { + "type": "number" + }, + "packets_recv": { + "type": "number" + }, + "err_in": { + "type": "number" + }, + "err_out": { + "type": "number" + }, + "drop_in": { + "type": "number" + }, + "drop_out": { + "type": "number" + }, + "fifo_in": { + "type": "number" + }, + "fifo_out": { + "type": "number" + } + } + }, + "CheckAudits": { + "type": "object", + "description": "PageSpeed audit results", + "properties": { + "cls": { + "$ref": "#/components/schemas/LighthouseAudit" + }, + "si": { + "$ref": "#/components/schemas/LighthouseAudit" + }, + "fcp": { + "$ref": "#/components/schemas/LighthouseAudit" + }, + "lcp": { + "$ref": "#/components/schemas/LighthouseAudit" + }, + "tbt": { + "$ref": "#/components/schemas/LighthouseAudit" + } + } + }, + "LighthouseAudit": { + "type": "object", + "description": "Individual Lighthouse audit result", + "properties": { + "id": { + "type": "string" + }, + "title": { + "type": "string" + }, + "score": { + "type": "number" + }, + "displayValue": { + "type": "string" + }, + "numericValue": { + "type": "number" + }, + "numericUnit": { + "type": "string" + } + } } } } From f7f764fd68ea828e6ed74dc8b09c6aa8524ce536 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 18:58:17 +0000 Subject: [PATCH 272/400] providers --- .../notifications/INotificationsRepository.ts | 1 + .../MongoNotificationsRepository.ts | 6 +++ server/src/service/index.ts | 6 ++- .../SuperSimpleQueueHelper.ts | 2 +- .../INotificationProvider.ts | 3 +- .../notificationProviders/discord.ts | 10 +++++ .../notificationProviders/email.ts | 12 +++++ .../notificationProviders/slack.ts | 12 +++++ .../notificationProviders/utils.ts | 14 +++++- .../notificationProviders/webhook.ts | 44 ++++++++++++++++--- .../infrastructure/notificationsService.ts | 40 +++++++++++------ 11 files changed, 126 insertions(+), 24 deletions(-) create mode 100644 server/src/service/infrastructure/notificationProviders/discord.ts create mode 100644 server/src/service/infrastructure/notificationProviders/email.ts create mode 100644 server/src/service/infrastructure/notificationProviders/slack.ts diff --git a/server/src/repositories/notifications/INotificationsRepository.ts b/server/src/repositories/notifications/INotificationsRepository.ts index 0244972e4..8bb65769a 100644 --- a/server/src/repositories/notifications/INotificationsRepository.ts +++ b/server/src/repositories/notifications/INotificationsRepository.ts @@ -2,6 +2,7 @@ import type { Notification } from "@/types/index.js"; export interface INotificationsRepository { // create // fetch + findNotificationsByIds(ids: string[]): Promise; // update // delete } diff --git a/server/src/repositories/notifications/MongoNotificationsRepository.ts b/server/src/repositories/notifications/MongoNotificationsRepository.ts index 51839c125..01a514f1c 100644 --- a/server/src/repositories/notifications/MongoNotificationsRepository.ts +++ b/server/src/repositories/notifications/MongoNotificationsRepository.ts @@ -35,6 +35,12 @@ class MongoNotificationsRepository implements INotificationsRepository { updatedAt: toDateString(doc.updatedAt), }; }; + + findNotificationsByIds = async (ids: string[]) => { + const mongoIds = ids.map((id) => new mongoose.Types.ObjectId(id)); + const documents = await NotificationModel.find({ _id: { $in: mongoIds } }); + return this.mapDocuments(documents); + }; } export default MongoNotificationsRepository; diff --git a/server/src/service/index.ts b/server/src/service/index.ts index fbba7714d..f46e6d3d8 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -1,6 +1,10 @@ export * from "@/service/business/monitorService.js"; export * from "@/service/infrastructure/networkService.js"; -export * from "@/service/infrastructure/notificationProviders/webhook.js"; +export * from "@/service/infrastructure/notificationProviders/discord.js"; +export * from "@/service/infrastructure/notificationProviders/email.js"; export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; +export * from "@/service/infrastructure/notificationProviders/slack.js"; +export * from "@/service/infrastructure/notificationProviders/webhook.js"; export * from "@/service/infrastructure/notificationsService.js"; export * from "@/service/infrastructure/statusService.js"; +export * from "@/service/infrastructure/notificationProviders/webhook.js"; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 41885a146..449386102 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -83,7 +83,7 @@ class SuperSimpleQueueHelper { // Step 4. Update monitor status const statusChangeResult = await this.statusService.updateMonitorStatus(status, check); - // Step 5 handle notifications + // Step 5 handle notifications (best effort, continue even in event of failure, don't wait) this.notificationsService.handleNotifications(monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged); // this.notificationService diff --git a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts index 2743c40bb..2d363d6b3 100644 --- a/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts +++ b/server/src/service/infrastructure/notificationProviders/INotificationProvider.ts @@ -1,7 +1,6 @@ import type { Monitor, Notification, Alert, MonitorStatusResponse } from "@/types/index.js"; export interface INotificationProvider { - buildAlert: (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => Alert; - sendAlert: (alert: Alert, notification: Notification) => Promise; + sendAlert: (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => Promise; sendTestAlert(notification: Notification): Promise; } diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts new file mode 100644 index 000000000..18b1522be --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -0,0 +1,10 @@ +import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; +export class DiscordProvider implements INotificationProvider { + sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + return false; + }; + sendTestAlert = async (notification: Notification) => { + return false; + }; +} diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts new file mode 100644 index 000000000..dda7338a0 --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -0,0 +1,12 @@ +import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; + +export class EmailProvider implements INotificationProvider { + async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { + return false; + } + + async sendTestAlert(notification: Notification): Promise { + return false; + } +} diff --git a/server/src/service/infrastructure/notificationProviders/slack.ts b/server/src/service/infrastructure/notificationProviders/slack.ts new file mode 100644 index 000000000..bd2836d2b --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/slack.ts @@ -0,0 +1,12 @@ +import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; + +export class SlackProvider implements INotificationProvider { + async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { + return false; + } + + async sendTestAlert(notification: Notification): Promise { + return false; + } +} diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index 7cba00a5d..9a10e668b 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -93,13 +93,19 @@ export const buildHardwareAlerts = ( return { alertsToSend, discordPayload }; }; -const buildHardwareNotificationMessage = (clientHost: string, alerts: any, monitor: Monitor) => { +export const buildHardwareNotificationMessage = (clientHost: string, alerts: any, monitor: Monitor) => { const alertsHeader = [`Monitor: ${monitor.name}`, `URL: ${monitor.url}`]; const alertFooter = [`Go to incident: ${clientHost}/infrastructure/${monitor.id}`]; const alertText = alerts.length > 0 ? [...alertsHeader, ...alerts, ...alertFooter] : []; return alertText.map((alert) => alert).join("\n"); }; +export const buildHardwareWebhookBody = (alerts: string[], monitor: Monitor): { text: string; name: string; url: string } => { + const content = alerts.map((alert) => alert).join("\n"); + const body = { text: content, name: monitor.name, url: monitor.url }; + return body; +}; + export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: MonitorStatusResponse): boolean => { const thresholds = monitor.thresholds || {}; const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; @@ -125,3 +131,9 @@ export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: Monit return false; }; + +export const buildWebhookBody = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { status, code } = monitorStatusResponse; + const formattedTime = Date.now().toLocaleString(); + return `Monitor: ${monitor.name}\nTime: ${formattedTime}\nStatus: ${status ? "UP" : "DOWN"}\n Status Code: ${code}\n\u200B\n`; +}; diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts index 4266bbf3b..cf56db89b 100644 --- a/server/src/service/infrastructure/notificationProviders/webhook.ts +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -1,15 +1,47 @@ import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; +import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import got from "got"; + export class WebhookProvider implements INotificationProvider { - buildAlert = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - return null; + private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const body = buildHardwareWebhookBody(alertsToSend, monitor); + return body; }; - sendAlert = async (alert: Alert) => { - return false; + private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const body = buildWebhookBody(monitor, monitorStatusResponse); + return body; }; - sendTestAlert(notification: Notification): Promise { + sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + let body; + if (monitor.type === "hardware") { + body = this.getHardwareContent(monitor, monitorStatusResponse); + } else { + body = this.getContent(monitor, monitorStatusResponse); + } + + if (!notification.address) { + return false; + } + + try { + await got.post(notification.address, { + json: body, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error) { + return false; + } + }; + + sendTestAlert = async (notification: Notification) => { return false; - } + }; } diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 2070b0c4f..e170dd3c4 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,7 +1,7 @@ import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification } from "@/types/index.js"; -import { NotificationModel } from "@/db/models/index.js"; -import { buildHardwareAlerts, shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js"; +import { shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js"; import { INotificationsRepository } from "@/repositories/index.js"; +import { WebhookProvider } from "@/service/index.js"; export interface INotificationsService { handleNotifications: ( monitor: Monitor, @@ -13,21 +13,36 @@ export interface INotificationsService { export class NotificationsService implements INotificationsService { private notificationsRepository: INotificationsRepository; - constructor(notificationsRepository: INotificationsRepository) { + private webhookProvider: WebhookProvider; + + constructor(notificationsRepository: INotificationsRepository, webhookProvider: WebhookProvider) { this.notificationsRepository = notificationsRepository; + this.webhookProvider = webhookProvider; } - private send = (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - console.log(notification); + private send = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise => { + switch (notification.type) { + case " email": { + } + case "webhook": { + return await this.webhookProvider.sendAlert(notification, monitor, monitorStatusResponse); + } + } + return false; }; private sendNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - const notifications = await this.notificationsRepository.findByMonitorId(monitor.id); - console.log({ notifications }); - const tasks = notifications.map((notification) => { - this.send(notification, monitor, monitorStatusResponse); - }); - return true; + const notificationIds = monitor.notifications ?? []; + const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds); + const tasks = notifications.map((notification) => this.send(notification, monitor, monitorStatusResponse)); + const outcomes = await Promise.all(tasks); + const succeeded = outcomes.filter(Boolean).length; + const failed = outcomes.length - succeeded; + if (failed > 0) { + // logger.warn(`Notification send completed with ${succeeded} success, ${failed} failure(s)`); + } + // Return true if all notificaitons succeeded + return succeeded === notifications.length; }; handleNotifications = async ( @@ -51,12 +66,11 @@ export class NotificationsService implements INotificationsService { const metrics = payload?.data ?? null; if (metrics === null) return false; // No metrics, we're done - // We shoul dsend a notificaiton + // We should send a notificaiton const shouldSend = shouldSendHardwareAlert(monitor, monitorStatusResponse); if (shouldSend === false) return false; console.log(JSON.stringify(monitor, null, 2)); - console.log(JSON.stringify(monitorStatusResponse, null, 2)); // const { subject, html } = await this.notificationUtils.buildHardwareEmail(networkResponse, alerts); // const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts, monitor); // const webhookBody = await this.notificationUtils.buildHardwareWebhookBody(alerts, monitor); From ab2553175817ef59263ce93d5fa4e467b1404d2f Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Mon, 19 Jan 2026 14:13:18 -0500 Subject: [PATCH 273/400] docs: fix OpenAPI spec accuracy and warnings ## Changes ### Route Corrections - Remove non-existent `/auth/refresh` endpoint - Add missing `/auth/users/{userId}/password` PUT endpoint - Fix `/notifications` GET -> `/notifications/team` GET (correct path) ### OpenAPI 3.1.0 Compliance - Fix `nullable` warnings by using `type: ["string", "null"]` syntax - Updated Incident schema nullable fields (endTime, message, statusCode, etc.) - Updated QueueJob schema nullable fields (processedOn, finishedOn) - Updated Check schema nullable field (ackAt) ### Verified Against Codebase - Audited all 76 API endpoints across 12 route files - Confirmed DELETE /checks/team and DELETE /checks/{monitorId} exist - All routes now match actual codebase implementation ## Validation - Passes `swagger-cli validate` - Only remaining warnings are cosmetic `example` deprecations --- server/openapi.json | 196 +++++++++++++++++++++++--------------------- 1 file changed, 101 insertions(+), 95 deletions(-) diff --git a/server/openapi.json b/server/openapi.json index d104501d0..d56c619bf 100644 --- a/server/openapi.json +++ b/server/openapi.json @@ -162,51 +162,6 @@ } } }, - "/auth/refresh": { - "post": { - "tags": ["auth"], - "summary": "Refresh access token", - "description": "Generate new access token using refresh token", - "parameters": [ - { - "name": "x-refresh-token", - "in": "header", - "description": "Refresh token", - "required": true, - "schema": { - "type": "string" - } - }, - { - "name": "authorization", - "in": "header", - "description": "Bearer token (old access token)", - "required": true, - "schema": { - "type": "string" - } - } - ], - "responses": { - "200": { - "description": "Token refreshed successfully", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/AuthResponse" - } - } - } - }, - "401": { - "$ref": "#/components/responses/Unauthorized" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - } - } - }, "/auth/user/{userId}": { "put": { "tags": ["auth"], @@ -1362,6 +1317,39 @@ } }, "/notifications": { + "post": { + "tags": ["notifications"], + "summary": "Create notification", + "description": "Create a new notification integration", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/CreateNotificationRequest" + } + } + } + }, + "responses": { + "201": { + "description": "Notification created successfully" + }, + "422": { + "$ref": "#/components/responses/ValidationError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, + "/notifications/team": { "get": { "tags": ["notifications"], "summary": "Get team notifications", @@ -1401,37 +1389,6 @@ "bearerAuth": [] } ] - }, - "post": { - "tags": ["notifications"], - "summary": "Create notification", - "description": "Create a new notification integration", - "requestBody": { - "required": true, - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/CreateNotificationRequest" - } - } - } - }, - "responses": { - "201": { - "description": "Notification created successfully" - }, - "422": { - "$ref": "#/components/responses/ValidationError" - }, - "500": { - "$ref": "#/components/responses/InternalServerError" - } - }, - "security": [ - { - "bearerAuth": [] - } - ] } }, "/notifications/{id}": { @@ -1822,6 +1779,64 @@ ] } }, + "/auth/users/{userId}/password": { + "put": { + "tags": ["auth"], + "summary": "Update user password by ID", + "description": "Update a specific user's password by ID (superadmin only)", + "parameters": [ + { + "name": "userId", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "type": "object", + "required": ["password"], + "properties": { + "password": { + "type": "string", + "format": "password", + "minLength": 8, + "description": "New password for the user" + } + } + } + } + } + }, + "responses": { + "200": { + "$ref": "#/components/responses/Success" + }, + "404": { + "$ref": "#/components/responses/NotFound" + }, + "403": { + "$ref": "#/components/responses/Forbidden" + }, + "422": { + "$ref": "#/components/responses/ValidationError" + }, + "500": { + "$ref": "#/components/responses/InternalServerError" + } + }, + "security": [ + { + "bearerAuth": [] + } + ] + } + }, "/invite": { "post": { "tags": ["invite"], @@ -4096,9 +4111,8 @@ "description": "When the incident started" }, "endTime": { - "type": "string", + "type": ["string", "null"], "format": "date-time", - "nullable": true, "description": "When the incident was resolved (null if ongoing)" }, "status": { @@ -4106,29 +4120,24 @@ "description": "Incident status (true = ongoing, false = resolved)" }, "message": { - "type": "string", - "nullable": true, + "type": ["string", "null"], "description": "Error message or description" }, "statusCode": { - "type": "integer", - "nullable": true, + "type": ["integer", "null"], "description": "HTTP status code if applicable" }, "resolutionType": { - "type": "string", - "enum": ["automatic", "manual"], - "nullable": true, + "type": ["string", "null"], + "enum": ["automatic", "manual", null], "description": "How the incident was resolved" }, "resolvedBy": { - "type": "string", - "nullable": true, + "type": ["string", "null"], "description": "User ID who resolved the incident (for manual resolution)" }, "comment": { - "type": "string", - "nullable": true, + "type": ["string", "null"], "description": "Resolution comment" }, "checks": { @@ -4220,15 +4229,13 @@ "description": "When the job was created" }, "processedOn": { - "type": "string", + "type": ["string", "null"], "format": "date-time", - "nullable": true, "description": "When the job started processing" }, "finishedOn": { - "type": "string", + "type": ["string", "null"], "format": "date-time", - "nullable": true, "description": "When the job finished" } } @@ -4358,9 +4365,8 @@ "description": "Whether the check has been acknowledged" }, "ackAt": { - "type": "string", + "type": ["string", "null"], "format": "date-time", - "nullable": true, "description": "When the check was acknowledged" }, "expiry": { From fd33b903edee65b807452c0dc1eef43256d6ee74 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 19:30:46 +0000 Subject: [PATCH 274/400] email provider --- server/src/config/services.ts | 15 ++++++-- .../notificationProviders/email.ts | 28 ++++++++++++++- .../notificationProviders/utils.ts | 32 +++++++++++++++-- .../infrastructure/notificationsService.ts | 34 ++++++++++++++----- 4 files changed, 96 insertions(+), 13 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index e25ee1706..3a740549a 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -7,7 +7,7 @@ import EmailService from "../service/infrastructure/emailService.js"; import BufferService from "../service/infrastructure/bufferService.js"; import NotificationUtils from "../service/infrastructure/notificationUtils.js"; import NotificationService from "../service/infrastructure/notificationService.js"; -import { NotificationsService, StatusService } from "@/service/index.js"; +import { NotificationsService, StatusService, WebhookProvider, SlackProvider, EmailProvider, DiscordProvider } from "@/service/index.js"; import ErrorService from "../service/infrastructure/errorService.js"; import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; @@ -228,7 +228,17 @@ export const initializeServices = async ({ notificationUtils, }); - const notificationsService = new NotificationsService(notificationsRepository); + const webhookProvider = new WebhookProvider(); + const slackProvider = new SlackProvider(); + const emailProvider = new EmailProvider(emailService); + const discordProvider = new DiscordProvider(); + + const notificationsService = new NotificationsService(notificationsRepository, { + webhookProvider, + slackProvider, + emailProvider, + discordProvider, + }); const superSimpleQueueHelper = new SuperSimpleQueueHelper({ db, @@ -323,6 +333,7 @@ export const initializeServices = async ({ invitesRepository, recoveryTokensRepository, settingsRepository, + notificationsRepository, notificationsService, }; diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts index dda7338a0..e60775982 100644 --- a/server/src/service/infrastructure/notificationProviders/email.ts +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -1,9 +1,35 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; +import { buildHardwareAlerts, buildHardwareEmail, buildEmail } from "@/service/infrastructure/notificationProviders/utils.js"; export class EmailProvider implements INotificationProvider { + private emailService: any; + constructor(emailService: any) { + this.emailService = emailService; + } + + private buildHardwareEmail = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const html = buildHardwareEmail(this.emailService, monitor, alertsToSend); + return html; + }; + async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { - return false; + const subject = `Monitor ${monitor.name} infrastructure alerts`; + let html; + if (monitor.type === "hardware") { + html = this.buildHardwareEmail(monitor, monitorStatusResponse); + } else { + html = await buildEmail(this.emailService, monitor); + } + + if (!notification.address) { + return false; + } + + const messageId = await this.emailService.sendEmail(notification.address, subject, html); + if (!messageId) return false; + return true; } async sendTestAlert(notification: Notification): Promise { diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index 9a10e668b..0d25543ea 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -134,6 +134,34 @@ export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: Monit export const buildWebhookBody = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { status, code } = monitorStatusResponse; - const formattedTime = Date.now().toLocaleString(); - return `Monitor: ${monitor.name}\nTime: ${formattedTime}\nStatus: ${status ? "UP" : "DOWN"}\n Status Code: ${code}\n\u200B\n`; + const now = new Date(); + const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const localTime = new Intl.DateTimeFormat(undefined, { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + timeZone: localTimeZone, + timeZoneName: "short", + }).format(now); + const utcTime = now.toUTCString(); + return `Monitor: ${monitor.name}\nLocal Time (${localTimeZone}): ${localTime}\nUTC Time: ${utcTime}\nStatus: ${status ? "UP" : "DOWN"}\nStatus Code: ${code}\n\u200B\n`; +}; + +export const buildHardwareEmail = async (emailService: any, monitor: Monitor, alerts: string[]) => { + const template = "hardwareIncidentTemplate"; + const context = { monitor: monitor.name, url: monitor.url, alerts }; + const html = await emailService.buildEmail(template, context); + return html; +}; + +export const buildEmail = async (emailService: any, monitor: Monitor): Promise => { + const template = monitor.status === false ? "serverIsUpTemplate" : "serverIsDownTemplate"; + const context = { monitor: monitor.name, url: monitor.url }; + const html = await emailService.buildEmail(template, context); + console.log({ html: typeof html }); + return html; }; diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index e170dd3c4..8b44d4f9c 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,7 +1,7 @@ import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification } from "@/types/index.js"; import { shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js"; import { INotificationsRepository } from "@/repositories/index.js"; -import { WebhookProvider } from "@/service/index.js"; +import { DiscordProvider, EmailProvider, SlackProvider, WebhookProvider } from "@/service/index.js"; export interface INotificationsService { handleNotifications: ( monitor: Monitor, @@ -14,21 +14,39 @@ export interface INotificationsService { export class NotificationsService implements INotificationsService { private notificationsRepository: INotificationsRepository; private webhookProvider: WebhookProvider; + private emailProvider: EmailProvider; + private slackProvider: SlackProvider; + private discordProvider: DiscordProvider; - constructor(notificationsRepository: INotificationsRepository, webhookProvider: WebhookProvider) { + constructor( + notificationsRepository: INotificationsRepository, + providers: { + webhookProvider: WebhookProvider; + emailProvider: EmailProvider; + slackProvider: SlackProvider; + discordProvider: DiscordProvider; + } + ) { this.notificationsRepository = notificationsRepository; - this.webhookProvider = webhookProvider; + this.webhookProvider = providers.webhookProvider; + this.emailProvider = providers.emailProvider; + this.slackProvider = providers.slackProvider; + this.discordProvider = providers.discordProvider; } private send = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise => { switch (notification.type) { - case " email": { - } - case "webhook": { + case "email": + return await this.emailProvider.sendAlert(notification, monitor, monitorStatusResponse); + case "slack": + return await this.slackProvider.sendAlert(notification, monitor, monitorStatusResponse); + case "discord": + return await this.discordProvider.sendAlert(notification, monitor, monitorStatusResponse); + case "webhook": return await this.webhookProvider.sendAlert(notification, monitor, monitorStatusResponse); - } + default: + return false; } - return false; }; private sendNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { From be82a7ee2dd5c2708022ab857a572f06c33144eb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 19:58:15 +0000 Subject: [PATCH 275/400] discord --- .../notificationProviders/discord.ts | 34 +++++++++++- .../notificationProviders/utils.ts | 55 ++++++++++++++----- .../notificationProviders/webhook.ts | 3 +- 3 files changed, 76 insertions(+), 16 deletions(-) diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts index 18b1522be..fcb126455 100644 --- a/server/src/service/infrastructure/notificationProviders/discord.ts +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -1,8 +1,40 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; +import { buildHardwareAlerts, buildDiscordBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import got, { HTTPError } from "got"; + export class DiscordProvider implements INotificationProvider { + private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { discordPayload } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + return discordPayload; + }; + sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { - return false; + let body; + if (monitor.type === "hardware") { + body = this.getHardwareContent(monitor, monitorStatusResponse); + } else { + body = buildDiscordBody(monitor, monitorStatusResponse); + } + + if (!notification.address) { + return false; + } + + console.log({ body }); + + try { + await got.post(notification.address, { + json: body, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error: any) { + return false; + } }; sendTestAlert = async (notification: Notification) => { return false; diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index 0d25543ea..3c6737263 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -1,5 +1,23 @@ import { Monitor, HardwareStatusPayload, MonitorStatusResponse } from "@/types/index.js"; +const getTime = (): { localTimeZone: string; localTime: string; utcTime: string } => { + const now = new Date(); + const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; + const localTime = new Intl.DateTimeFormat(undefined, { + year: "numeric", + month: "2-digit", + day: "2-digit", + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + hour12: false, + timeZone: localTimeZone, + timeZoneName: "short", + }).format(now); + const utcTime = now.toUTCString(); + return { localTimeZone, localTime, utcTime }; +}; + export const buildHardwareAlerts = ( clientHost: string, monitor: Monitor, @@ -134,20 +152,7 @@ export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: Monit export const buildWebhookBody = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { status, code } = monitorStatusResponse; - const now = new Date(); - const localTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; - const localTime = new Intl.DateTimeFormat(undefined, { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: false, - timeZone: localTimeZone, - timeZoneName: "short", - }).format(now); - const utcTime = now.toUTCString(); + const { localTimeZone, localTime, utcTime } = getTime(); return `Monitor: ${monitor.name}\nLocal Time (${localTimeZone}): ${localTime}\nUTC Time: ${utcTime}\nStatus: ${status ? "UP" : "DOWN"}\nStatus Code: ${code}\n\u200B\n`; }; @@ -165,3 +170,25 @@ export const buildEmail = async (emailService: any, monitor: Monitor): Promise { + const { localTime, utcTime } = getTime(); + let body = { + embeds: [ + { + title: `Monitor ${monitor.name}`, + color: monitor.status ? 5763719 : 15548997, + + fields: [ + { name: "Monitor", value: monitor.name, inline: true }, + { name: "Status", value: monitor.status ? "Up" : "Down", inline: true }, + { name: "Status Code", value: String(monitorStatusResponse.code), inline: true }, + { name: "Time", value: `${localTime} (Local) / ${utcTime} (UTC)`, inline: true }, + { name: "URL", value: monitor.url, inline: false }, + ], + footer: { text: "Checkmate" }, + }, + ], + }; + return body; +}; diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts index cf56db89b..d5ae3bcd3 100644 --- a/server/src/service/infrastructure/notificationProviders/webhook.ts +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -15,7 +15,8 @@ export class WebhookProvider implements INotificationProvider { return body; }; - sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + console.log("WebhookProvider.sendAlert called"); let body; if (monitor.type === "hardware") { body = this.getHardwareContent(monitor, monitorStatusResponse); From 9006707e57ee97902e21857ee96876ff675877b8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 20:03:20 +0000 Subject: [PATCH 276/400] discord --- .../infrastructure/notificationProviders/discord.ts | 2 -- .../infrastructure/notificationProviders/utils.ts | 12 ++++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts index fcb126455..8ac042a92 100644 --- a/server/src/service/infrastructure/notificationProviders/discord.ts +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -21,8 +21,6 @@ export class DiscordProvider implements INotificationProvider { return false; } - console.log({ body }); - try { await got.post(notification.address, { json: body, diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index 3c6737263..fff682246 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -176,14 +176,14 @@ export const buildDiscordBody = (monitor: Monitor, monitorStatusResponse: Monito let body = { embeds: [ { - title: `Monitor ${monitor.name}`, - color: monitor.status ? 5763719 : 15548997, + title: `Monitor name: ${monitor.name}`, + color: monitorStatusResponse.status ? 5763719 : 15548997, fields: [ - { name: "Monitor", value: monitor.name, inline: true }, - { name: "Status", value: monitor.status ? "Up" : "Down", inline: true }, - { name: "Status Code", value: String(monitorStatusResponse.code), inline: true }, - { name: "Time", value: `${localTime} (Local) / ${utcTime} (UTC)`, inline: true }, + { name: "Monitor", value: monitor.name, inline: false }, + { name: "Status", value: monitorStatusResponse.status ? "Up" : "Down", inline: false }, + { name: "Status Code", value: String(monitorStatusResponse.code), inline: false }, + { name: "Time", value: `${localTime} (Local) / ${utcTime} (UTC)`, inline: false }, { name: "URL", value: monitor.url, inline: false }, ], footer: { text: "Checkmate" }, From 1d36f40caf983a8c7470982ed54a689791812f61 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 20:12:54 +0000 Subject: [PATCH 277/400] slack --- .../notificationProviders/discord.ts | 2 +- .../notificationProviders/slack.ts | 37 ++++++++++++++++++- .../notificationProviders/utils.ts | 6 +-- .../notificationProviders/webhook.ts | 1 - 4 files changed, 39 insertions(+), 7 deletions(-) diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts index 8ac042a92..5aa8dd1d4 100644 --- a/server/src/service/infrastructure/notificationProviders/discord.ts +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -1,7 +1,7 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; import { buildHardwareAlerts, buildDiscordBody } from "@/service/infrastructure/notificationProviders/utils.js"; -import got, { HTTPError } from "got"; +import got from "got"; export class DiscordProvider implements INotificationProvider { private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { diff --git a/server/src/service/infrastructure/notificationProviders/slack.ts b/server/src/service/infrastructure/notificationProviders/slack.ts index bd2836d2b..4f4c9df18 100644 --- a/server/src/service/infrastructure/notificationProviders/slack.ts +++ b/server/src/service/infrastructure/notificationProviders/slack.ts @@ -1,9 +1,44 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; +import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import got from "got"; export class SlackProvider implements INotificationProvider { + private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const body = buildHardwareWebhookBody(alertsToSend, monitor); + return body; + }; + + private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const body = buildWebhookBody(monitor, monitorStatusResponse); + return body; + }; + async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { - return false; + let body; + if (monitor.type === "hardware") { + body = this.getHardwareContent(monitor, monitorStatusResponse); + } else { + body = this.getContent(monitor, monitorStatusResponse); + } + + if (!notification.address) { + return false; + } + + try { + await got.post(notification.address, { + json: { text: body }, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error) { + return false; + } } async sendTestAlert(notification: Notification): Promise { diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index fff682246..7d7d25f49 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -118,10 +118,9 @@ export const buildHardwareNotificationMessage = (clientHost: string, alerts: any return alertText.map((alert) => alert).join("\n"); }; -export const buildHardwareWebhookBody = (alerts: string[], monitor: Monitor): { text: string; name: string; url: string } => { +export const buildHardwareWebhookBody = (alerts: string[], monitor: Monitor): string => { const content = alerts.map((alert) => alert).join("\n"); - const body = { text: content, name: monitor.name, url: monitor.url }; - return body; + return content; }; export const shouldSendHardwareAlert = (monitor: Monitor, networkResponse: MonitorStatusResponse): boolean => { @@ -167,7 +166,6 @@ export const buildEmail = async (emailService: any, monitor: Monitor): Promise { - console.log("WebhookProvider.sendAlert called"); let body; if (monitor.type === "hardware") { body = this.getHardwareContent(monitor, monitorStatusResponse); From 7c9410f9d73453ce48923b64f3c9643d3699288c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 21:39:02 +0000 Subject: [PATCH 278/400] test alerts --- server/src/config/controllers.ts | 2 +- server/src/config/services.ts | 40 +++++---- .../src/controllers/notificationController.ts | 15 ++-- server/src/service/index.ts | 3 + .../SuperSimpleQueueHelper.ts | 33 +++---- .../notificationProviders/email.ts | 8 +- .../notificationProviders/matrix.js | 34 ------- .../notificationProviders/matrix.ts | 56 ++++++++++++ .../notificationProviders/pagerduty.ts | 50 +++++++++++ .../notificationProviders/slack.ts | 24 ++++- .../notificationProviders/utils.ts | 10 +++ .../infrastructure/notificationsService.ts | 90 ++++++++++++++----- 12 files changed, 258 insertions(+), 107 deletions(-) delete mode 100644 server/src/service/infrastructure/notificationProviders/matrix.js create mode 100644 server/src/service/infrastructure/notificationProviders/matrix.ts create mode 100644 server/src/service/infrastructure/notificationProviders/pagerduty.ts diff --git a/server/src/config/controllers.ts b/server/src/config/controllers.ts index 44b051580..5ddbf2a06 100644 --- a/server/src/config/controllers.ts +++ b/server/src/config/controllers.ts @@ -26,7 +26,7 @@ export const initializeControllers = (services: InitializedSerivces) => { controllers.queueController = new QueueController(services.jobQueue); controllers.logController = new LogController(services.logger); controllers.statusPageController = new StatusPageController(services.db); - controllers.notificationController = new NotificationController(services.notificationService, services.db, services.monitorsRepository); + controllers.notificationController = new NotificationController(services.notificationsService, services.db, services.monitorsRepository); controllers.diagnosticController = new DiagnosticController(services.diagnosticService); controllers.incidentController = new IncidentController(services.incidentService); diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 3a740549a..035a0782f 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -6,8 +6,17 @@ import NetworkService from "../service/infrastructure/networkService.js"; import EmailService from "../service/infrastructure/emailService.js"; import BufferService from "../service/infrastructure/bufferService.js"; import NotificationUtils from "../service/infrastructure/notificationUtils.js"; -import NotificationService from "../service/infrastructure/notificationService.js"; -import { NotificationsService, StatusService, WebhookProvider, SlackProvider, EmailProvider, DiscordProvider } from "@/service/index.js"; +import { + NotificationsService, + StatusService, + WebhookProvider, + SlackProvider, + EmailProvider, + DiscordProvider, + PagerDutyProvider, + MatrixProvider, + INotificationsService, +} from "@/service/index.js"; import ErrorService from "../service/infrastructure/errorService.js"; import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; @@ -97,7 +106,6 @@ export type InitializedSerivces = { emailService: any; bufferService: any; statusService: any; - notificationService: any; jobQueue: any; userService: any; checkService: any; @@ -108,6 +116,7 @@ export type InitializedSerivces = { incidentService: any; errorService: any; logger: any; + notificationsService: INotificationsService; // Repositories monitorsRepository: IMonitorsRepository; @@ -219,33 +228,29 @@ export const initializeServices = async ({ settingsService, }); - const notificationService = new NotificationService({ - emailService, - db, - logger, - networkService, - stringService, - notificationUtils, - }); - const webhookProvider = new WebhookProvider(); const slackProvider = new SlackProvider(); const emailProvider = new EmailProvider(emailService); const discordProvider = new DiscordProvider(); + const pagerDutyProvider = new PagerDutyProvider(); + const matrixProvider = new MatrixProvider(); - const notificationsService = new NotificationsService(notificationsRepository, { + const notificationsService = new NotificationsService( + notificationsRepository, webhookProvider, - slackProvider, emailProvider, + slackProvider, discordProvider, - }); + pagerDutyProvider, + matrixProvider, + logger + ); const superSimpleQueueHelper = new SuperSimpleQueueHelper({ db, logger, networkService, statusService, - notificationService, notificationsService, checkService, buffer: bufferService, @@ -312,7 +317,6 @@ export const initializeServices = async ({ emailService, bufferService, statusService, - notificationService, jobQueue: superSimpleQueue, userService, checkService, @@ -323,6 +327,7 @@ export const initializeServices = async ({ incidentService, errorService, logger, + notificationsService, // Repositories monitorsRepository, @@ -334,7 +339,6 @@ export const initializeServices = async ({ recoveryTokensRepository, settingsRepository, notificationsRepository, - notificationsService, }; Object.values(services).forEach((service) => { diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index 6f4a7f164..1569f0986 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -1,18 +1,20 @@ import { Request, Response, NextFunction } from "express"; +import { Notification } from "@/types/index.js"; import { createNotificationBodyValidation } from "@/validation/joi.js"; import { AppError } from "@/utils/AppError.js"; import { IMonitorsRepository } from "@/repositories/index.js"; +import { INotificationsService } from "@/service/index.js"; const SERVICE_NAME = "NotificationController"; class NotificationController { static SERVICE_NAME = SERVICE_NAME; private db: any; - private notificationService: any; + private notificationsService: INotificationsService; private monitorsRepository: IMonitorsRepository; - constructor(notificationService: any, db: any, monitorsRepository: IMonitorsRepository) { - this.notificationService = notificationService; + constructor(notificationsService: INotificationsService, db: any, monitorsRepository: IMonitorsRepository) { + this.notificationsService = notificationsService; this.db = db; this.monitorsRepository = monitorsRepository; } @@ -23,9 +25,8 @@ class NotificationController { testNotification = async (req: Request, res: Response, next: NextFunction) => { try { - const notification = req.body; - - const success = await this.notificationService.sendTestNotification(notification); + const notification: Notification = req.body; + const success = await this.notificationsService.sendTestNotification(notification); if (!success) { throw new AppError({ message: "Sending notification failed", status: 500 }); @@ -177,7 +178,7 @@ class NotificationController { throw new AppError({ message: "No notifications", status: 400 }); } - const result = await this.notificationService.testAllNotifications(notifications); + const result = await this.notificationsService.testAllNotifications(notifications); if (!result) { throw new AppError({ message: "Failed to send all notifications", status: 500 }); diff --git a/server/src/service/index.ts b/server/src/service/index.ts index f46e6d3d8..7f7d0f14a 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -3,8 +3,11 @@ export * from "@/service/infrastructure/networkService.js"; export * from "@/service/infrastructure/notificationProviders/discord.js"; export * from "@/service/infrastructure/notificationProviders/email.js"; export * from "@/service/infrastructure/notificationProviders/INotificationProvider.js"; +export * from "@/service/infrastructure/notificationProviders/pagerduty.js"; +export * from "@/service/infrastructure/notificationProviders/matrix.js"; export * from "@/service/infrastructure/notificationProviders/slack.js"; export * from "@/service/infrastructure/notificationProviders/webhook.js"; export * from "@/service/infrastructure/notificationsService.js"; export * from "@/service/infrastructure/statusService.js"; export * from "@/service/infrastructure/notificationProviders/webhook.js"; +export * from "@/service/infrastructure/notificationProviders/matrix.js"; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 449386102..ebb0949b0 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -9,11 +9,9 @@ class SuperSimpleQueueHelper { private logger: any; private networkService: INetworkService; private statusService: IStatusService; - private notificationService: any; private notificationsService: INotificationsService; private checkService: any; private buffer: any; - private incidentService: any; constructor({ db, @@ -29,7 +27,6 @@ class SuperSimpleQueueHelper { logger: any; networkService: INetworkService; statusService: IStatusService; - notificationService: any; notificationsService: INotificationsService; checkService: any; buffer: any; @@ -38,7 +35,6 @@ class SuperSimpleQueueHelper { this.logger = logger; this.networkService = networkService; this.statusService = statusService; - this.notificationService = notificationService; this.checkService = checkService; this.buffer = buffer; this.notificationsService = notificationsService; @@ -84,24 +80,17 @@ class SuperSimpleQueueHelper { const statusChangeResult = await this.statusService.updateMonitorStatus(status, check); // Step 5 handle notifications (best effort, continue even in event of failure, don't wait) - this.notificationsService.handleNotifications(monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged); - - // this.notificationService - // .handleNotifications({ - // ...status, - // monitor: statusChangeResult.monitor, - // prevStatus: statusChangeResult.prevStatus, - // statusChanged: statusChangeResult.statusChanged, - // }) - // .catch((error: any) => { - // this.logger.error({ - // message: error.message, - // service: SERVICE_NAME, - // method: "getMonitorJob", - // details: `Error sending notifications for job ${monitor.id}: ${error.message}`, - // stack: error.stack, - // }); - // }); + this.notificationsService + .handleNotifications(monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged) + .catch((error: any) => { + this.logger.error({ + message: error.message, + service: SERVICE_NAME, + method: "getMonitorJob", + details: `Error sending notifications for job ${monitor.id}: ${error.message}`, + stack: error.stack, + }); + }); } catch (error: any) { this.logger.warn({ message: error.message, diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts index e60775982..0a405a078 100644 --- a/server/src/service/infrastructure/notificationProviders/email.ts +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -1,6 +1,6 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; -import { buildHardwareAlerts, buildHardwareEmail, buildEmail } from "@/service/infrastructure/notificationProviders/utils.js"; +import { buildHardwareAlerts, buildHardwareEmail, buildEmail, buildTestEmail } from "@/service/infrastructure/notificationProviders/utils.js"; export class EmailProvider implements INotificationProvider { private emailService: any; @@ -33,6 +33,10 @@ export class EmailProvider implements INotificationProvider { } async sendTestAlert(notification: Notification): Promise { - return false; + const subject = "Test notification"; + const html = await buildTestEmail(this.emailService); + const messageId = await this.emailService.sendEmail(notification.address, subject, html); + if (!messageId) return false; + return true; } } diff --git a/server/src/service/infrastructure/notificationProviders/matrix.js b/server/src/service/infrastructure/notificationProviders/matrix.js deleted file mode 100644 index a8b33d4b8..000000000 --- a/server/src/service/infrastructure/notificationProviders/matrix.js +++ /dev/null @@ -1,34 +0,0 @@ -const SERVICE_NAME = "Matrix"; - -class Matrix { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ networkService, logger }) { - this.networkService = networkService; - this.logger = logger; - } - - get serviceName() { - return Matrix.SERVICE_NAME; - } - - async send({ friendlyName, homeserverUrl, accessToken, roomId, message, monitorName }) { - const title = `Checkmate status for ${monitorName}`; - const formattedMessage = `## ${title}\n${message}`; - try { - await this.networkService.requestMatrix({ - homeserverUrl, - accessToken, - roomId, - message: formattedMessage, - }); - this.logger.info(`Successfully sent Matrix notification for ${friendlyName}`); - return true; - } catch (error) { - this.logger.error(`Failed to send Matrix notification for ${friendlyName}: ${error.message}`); - return false; - } - } -} - -export default Matrix; diff --git a/server/src/service/infrastructure/notificationProviders/matrix.ts b/server/src/service/infrastructure/notificationProviders/matrix.ts new file mode 100644 index 000000000..8985ac10b --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/matrix.ts @@ -0,0 +1,56 @@ +import got from "got"; +import type { INotificationProvider } from "@/service/index.js"; +import type { Notification, Monitor, MonitorStatusResponse } from "@/types/index.js"; +import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; + +export class MatrixProvider implements INotificationProvider { + private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const body = buildHardwareWebhookBody(alertsToSend, monitor); + return body; + }; + + private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const body = buildWebhookBody(monitor, monitorStatusResponse); + return body; + }; + + sendAlert = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { homeserverUrl, accessToken, roomId } = notification; + + let content; + if (monitor.type === "hardware") { + content = this.getHardwareContent(monitor, monitorStatusResponse); + } else { + content = this.getContent(monitor, monitorStatusResponse); + } + + const title = `Checkmate status for ${monitor.name}`; + + const formattedMessage = `## ${title}\n${content}`; + + const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`; + const body = { + msgtype: "m.text", + body: formattedMessage, + format: "org.matrix.custom.html", + formatted_body: formattedMessage, + }; + try { + await got.post(url, { + json: body, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error) { + return false; + } + }; + + sendTestAlert = async (notification: Notification) => { + return false; + }; +} diff --git a/server/src/service/infrastructure/notificationProviders/pagerduty.ts b/server/src/service/infrastructure/notificationProviders/pagerduty.ts new file mode 100644 index 000000000..d0309550b --- /dev/null +++ b/server/src/service/infrastructure/notificationProviders/pagerduty.ts @@ -0,0 +1,50 @@ +import got from "got"; +import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; +import { INotificationProvider } from "@/service/index.js"; +import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; + +export class PagerDutyProvider implements INotificationProvider { + private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); + const body = buildHardwareWebhookBody(alertsToSend, monitor); + return body; + }; + + private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + const body = buildWebhookBody(monitor, monitorStatusResponse); + return body; + }; + + async sendAlert(notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise { + let body; + if (monitor.type === "hardware") { + body = this.getHardwareContent(monitor, monitorStatusResponse); + } else { + body = this.getContent(monitor, monitorStatusResponse); + } + + try { + await got.post("https://events.pagerduty.com/v2/enqueue", { + json: { + routing_key: notification.address, + event_action: "trigger", + payload: { + summary: body, + severity: "critical", + source: monitor.url, + timestamp: new Date().toISOString(), + }, + }, + responseType: "json", + }); + + return true; + } catch (error) { + return false; + } + } + + async sendTestAlert(notification: Notification): Promise { + return false; + } +} diff --git a/server/src/service/infrastructure/notificationProviders/slack.ts b/server/src/service/infrastructure/notificationProviders/slack.ts index 4f4c9df18..c5745ce05 100644 --- a/server/src/service/infrastructure/notificationProviders/slack.ts +++ b/server/src/service/infrastructure/notificationProviders/slack.ts @@ -1,6 +1,11 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; -import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import { + buildHardwareAlerts, + buildHardwareWebhookBody, + buildWebhookBody, + getTestMessage, +} from "@/service/infrastructure/notificationProviders/utils.js"; import got from "got"; export class SlackProvider implements INotificationProvider { @@ -42,6 +47,21 @@ export class SlackProvider implements INotificationProvider { } async sendTestAlert(notification: Notification): Promise { - return false; + if (!notification.address) { + return false; + } + + try { + await got.post(notification.address, { + json: { text: getTestMessage() }, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error) { + return false; + } } } diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index 7d7d25f49..a7e954e55 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -190,3 +190,13 @@ export const buildDiscordBody = (monitor: Monitor, monitorStatusResponse: Monito }; return body; }; + +export const buildTestEmail = async (emailService: any) => { + const context = { testName: "Monitoring System" }; + const html = await emailService.buildEmail("testEmailTemplate", context); + return html; +}; + +export const getTestMessage = () => { + return "This is a test notification from Checkmate"; +}; diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 8b44d4f9c..7ca52945e 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,7 +1,7 @@ import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification } from "@/types/index.js"; import { shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js"; import { INotificationsRepository } from "@/repositories/index.js"; -import { DiscordProvider, EmailProvider, SlackProvider, WebhookProvider } from "@/service/index.js"; +import { INotificationProvider } from "./notificationProviders/INotificationProvider.js"; export interface INotificationsService { handleNotifications: ( monitor: Monitor, @@ -9,29 +9,42 @@ export interface INotificationsService { prevStatus: boolean | undefined, statusChanged: boolean ) => Promise; + sendTestNotification: (notification: Notification) => Promise; + testAllNotifications: (notificationIds: string[]) => Promise; } +const SERVICE_NAME = "NotificationsService"; + export class NotificationsService implements INotificationsService { + static SERVICE_NAME = SERVICE_NAME; + private notificationsRepository: INotificationsRepository; - private webhookProvider: WebhookProvider; - private emailProvider: EmailProvider; - private slackProvider: SlackProvider; - private discordProvider: DiscordProvider; + private webhookProvider: INotificationProvider; + private emailProvider: INotificationProvider; + private slackProvider: INotificationProvider; + private discordProvider: INotificationProvider; + private pagerDutyProvider: INotificationProvider; + private matrixProvider: INotificationProvider; + private logger: any; constructor( notificationsRepository: INotificationsRepository, - providers: { - webhookProvider: WebhookProvider; - emailProvider: EmailProvider; - slackProvider: SlackProvider; - discordProvider: DiscordProvider; - } + webhookProvider: INotificationProvider, + emailProvider: INotificationProvider, + slackProvider: INotificationProvider, + discordProvider: INotificationProvider, + pagerDutyProvider: INotificationProvider, + matrixProvider: INotificationProvider, + logger: any ) { this.notificationsRepository = notificationsRepository; - this.webhookProvider = providers.webhookProvider; - this.emailProvider = providers.emailProvider; - this.slackProvider = providers.slackProvider; - this.discordProvider = providers.discordProvider; + this.webhookProvider = webhookProvider; + this.emailProvider = emailProvider; + this.slackProvider = slackProvider; + this.discordProvider = discordProvider; + this.pagerDutyProvider = pagerDutyProvider; + this.matrixProvider = matrixProvider; + this.logger = logger; } private send = async (notification: Notification, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): Promise => { @@ -42,6 +55,10 @@ export class NotificationsService implements INotificationsService { return await this.slackProvider.sendAlert(notification, monitor, monitorStatusResponse); case "discord": return await this.discordProvider.sendAlert(notification, monitor, monitorStatusResponse); + case "pager_duty": + return await this.pagerDutyProvider.sendAlert(notification, monitor, monitorStatusResponse); + case "matrix": + return await this.matrixProvider.sendAlert(notification, monitor, monitorStatusResponse); case "webhook": return await this.webhookProvider.sendAlert(notification, monitor, monitorStatusResponse); default: @@ -57,7 +74,11 @@ export class NotificationsService implements INotificationsService { const succeeded = outcomes.filter(Boolean).length; const failed = outcomes.length - succeeded; if (failed > 0) { - // logger.warn(`Notification send completed with ${succeeded} success, ${failed} failure(s)`); + this.logger.warn({ + message: `Notification send completed with ${succeeded} success, ${failed} failure(s)`, + service: SERVICE_NAME, + method: "getMonitorJob", + }); } // Return true if all notificaitons succeeded return succeeded === notifications.length; @@ -88,15 +109,42 @@ export class NotificationsService implements INotificationsService { const shouldSend = shouldSendHardwareAlert(monitor, monitorStatusResponse); if (shouldSend === false) return false; - console.log(JSON.stringify(monitor, null, 2)); - // const { subject, html } = await this.notificationUtils.buildHardwareEmail(networkResponse, alerts); - // const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts, monitor); - // const webhookBody = await this.notificationUtils.buildHardwareWebhookBody(alerts, monitor); - // const success = await this.notifyAll({ notificationIDs, subject, html, content, discordContent, webhookBody }); + return await this.sendNotifications(monitor, monitorStatusResponse); } // We should send a notification for non-hardware monitor status change return await this.sendNotifications(monitor, monitorStatusResponse); }; + + sendTestNotification = async (notification: Notification) => { + switch (notification.type) { + case "email": + return await this.emailProvider.sendTestAlert(notification); + case "slack": + return await this.slackProvider.sendTestAlert(notification); + case "discord": + return await this.discordProvider.sendTestAlert(notification); + case "pager_duty": + return await this.pagerDutyProvider.sendTestAlert(notification); + case "matrix": + return await this.matrixProvider.sendTestAlert(notification); + case "webhook": + return await this.webhookProvider.sendTestAlert(notification); + default: + return false; + } + }; + + testAllNotifications = async (notificationIds: string[]) => { + const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds); + const tasks = notifications.map((notification) => this.sendTestNotification(notification)); + const outcomes = await Promise.all(tasks); + const succeeded = outcomes.filter(Boolean).length; + const failed = outcomes.length - succeeded; + if (failed > 0) { + return false; + } + return true; + }; } From bebb5d6942a0e3ad85d0d4cf1afb735eb4c57ffe Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 21:58:48 +0000 Subject: [PATCH 279/400] inject logger --- server/src/config/services.ts | 12 +++---- .../SuperSimpleQueueHelper.ts | 1 - .../notificationProviders/discord.ts | 23 +++++++++++-- .../notificationProviders/email.ts | 4 ++- .../notificationProviders/matrix.ts | 34 +++++++++++++++++-- .../notificationProviders/pagerduty.ts | 31 +++++++++++++++-- .../notificationProviders/slack.ts | 5 +++ .../notificationProviders/webhook.ts | 32 ++++++++++++++--- .../infrastructure/notificationsService.ts | 1 + 9 files changed, 125 insertions(+), 18 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 035a0782f..953fc5a1a 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -228,12 +228,12 @@ export const initializeServices = async ({ settingsService, }); - const webhookProvider = new WebhookProvider(); - const slackProvider = new SlackProvider(); - const emailProvider = new EmailProvider(emailService); - const discordProvider = new DiscordProvider(); - const pagerDutyProvider = new PagerDutyProvider(); - const matrixProvider = new MatrixProvider(); + const webhookProvider = new WebhookProvider(logger); + const slackProvider = new SlackProvider(logger); + const emailProvider = new EmailProvider(emailService, logger); + const discordProvider = new DiscordProvider(logger); + const pagerDutyProvider = new PagerDutyProvider(logger); + const matrixProvider = new MatrixProvider(logger); const notificationsService = new NotificationsService( notificationsRepository, diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index ebb0949b0..bbd3a11f0 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -18,7 +18,6 @@ class SuperSimpleQueueHelper { logger, networkService, statusService, - notificationService, notificationsService, checkService, buffer, diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts index 5aa8dd1d4..83d8e4ec2 100644 --- a/server/src/service/infrastructure/notificationProviders/discord.ts +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -1,9 +1,10 @@ import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; -import { buildHardwareAlerts, buildDiscordBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import { buildHardwareAlerts, buildDiscordBody, getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; import got from "got"; export class DiscordProvider implements INotificationProvider { + constructor(private logger: any) {} private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { discordPayload } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); return discordPayload; @@ -22,6 +23,7 @@ export class DiscordProvider implements INotificationProvider { } try { + this.logger?.debug?.("Sending Discord alert", { address: notification.address }); await got.post(notification.address, { json: body, headers: { @@ -31,10 +33,27 @@ export class DiscordProvider implements INotificationProvider { }); return true; } catch (error: any) { + this.logger?.error?.("Discord alert failed", { error }); return false; } }; sendTestAlert = async (notification: Notification) => { - return false; + if (!notification.address) { + return false; + } + try { + this.logger?.debug?.("Sending Discord test alert", { address: notification.address }); + await got.post(notification.address, { + json: { content: getTestMessage() }, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error) { + this.logger?.error?.("Discord test alert failed", { error }); + return false; + } }; } diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts index 0a405a078..344285e9c 100644 --- a/server/src/service/infrastructure/notificationProviders/email.ts +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -4,7 +4,7 @@ import { buildHardwareAlerts, buildHardwareEmail, buildEmail, buildTestEmail } f export class EmailProvider implements INotificationProvider { private emailService: any; - constructor(emailService: any) { + constructor(emailService: any, private logger: any) { this.emailService = emailService; } @@ -27,6 +27,7 @@ export class EmailProvider implements INotificationProvider { return false; } + this.logger?.debug?.("Sending Email alert", { address: notification.address }); const messageId = await this.emailService.sendEmail(notification.address, subject, html); if (!messageId) return false; return true; @@ -35,6 +36,7 @@ export class EmailProvider implements INotificationProvider { async sendTestAlert(notification: Notification): Promise { const subject = "Test notification"; const html = await buildTestEmail(this.emailService); + this.logger?.debug?.("Sending Email test alert", { address: notification.address }); const messageId = await this.emailService.sendEmail(notification.address, subject, html); if (!messageId) return false; return true; diff --git a/server/src/service/infrastructure/notificationProviders/matrix.ts b/server/src/service/infrastructure/notificationProviders/matrix.ts index 8985ac10b..4592500a5 100644 --- a/server/src/service/infrastructure/notificationProviders/matrix.ts +++ b/server/src/service/infrastructure/notificationProviders/matrix.ts @@ -1,9 +1,15 @@ import got from "got"; import type { INotificationProvider } from "@/service/index.js"; import type { Notification, Monitor, MonitorStatusResponse } from "@/types/index.js"; -import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import { + buildHardwareAlerts, + buildHardwareWebhookBody, + buildWebhookBody, + getTestMessage, +} from "@/service/infrastructure/notificationProviders/utils.js"; export class MatrixProvider implements INotificationProvider { + constructor(private logger: any) {} private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -37,6 +43,7 @@ export class MatrixProvider implements INotificationProvider { formatted_body: formattedMessage, }; try { + this.logger?.debug?.("Sending Matrix alert", { room: roomId }); await got.post(url, { json: body, headers: { @@ -46,11 +53,34 @@ export class MatrixProvider implements INotificationProvider { }); return true; } catch (error) { + this.logger?.error?.("Matrix alert failed", { error }); return false; } }; sendTestAlert = async (notification: Notification) => { - return false; + const { homeserverUrl, accessToken, roomId } = notification; + if (!homeserverUrl || !accessToken || !roomId) { + return false; + } + const url = `${homeserverUrl}/_matrix/client/v3/rooms/${roomId}/send/m.room.message?access_token=${accessToken}`; + const body = { + msgtype: "m.text", + body: getTestMessage(), + }; + try { + this.logger?.debug?.("Sending Matrix test alert", { room: roomId }); + await got.post(url, { + json: body, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error) { + this.logger?.error?.("Matrix test alert failed", { error }); + return false; + } }; } diff --git a/server/src/service/infrastructure/notificationProviders/pagerduty.ts b/server/src/service/infrastructure/notificationProviders/pagerduty.ts index d0309550b..2d85cd4d6 100644 --- a/server/src/service/infrastructure/notificationProviders/pagerduty.ts +++ b/server/src/service/infrastructure/notificationProviders/pagerduty.ts @@ -1,9 +1,15 @@ import got from "got"; import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; -import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import { + buildHardwareAlerts, + buildHardwareWebhookBody, + buildWebhookBody, + getTestMessage, +} from "@/service/infrastructure/notificationProviders/utils.js"; export class PagerDutyProvider implements INotificationProvider { + constructor(private logger: any) {} private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -24,6 +30,7 @@ export class PagerDutyProvider implements INotificationProvider { } try { + this.logger?.debug?.("Sending PagerDuty alert", { routingKey: notification.address }); await got.post("https://events.pagerduty.com/v2/enqueue", { json: { routing_key: notification.address, @@ -40,11 +47,31 @@ export class PagerDutyProvider implements INotificationProvider { return true; } catch (error) { + this.logger?.error?.("PagerDuty alert failed", { error }); return false; } } async sendTestAlert(notification: Notification): Promise { - return false; + try { + this.logger?.debug?.("Sending PagerDuty test alert", { routingKey: notification.address }); + await got.post("https://events.pagerduty.com/v2/enqueue", { + json: { + routing_key: notification.address, + event_action: "trigger", + payload: { + summary: getTestMessage(), + severity: "info", + source: "checkmate", + timestamp: new Date().toISOString(), + }, + }, + responseType: "json", + }); + return true; + } catch (error) { + this.logger?.error?.("PagerDuty test alert failed", { error }); + return false; + } } } diff --git a/server/src/service/infrastructure/notificationProviders/slack.ts b/server/src/service/infrastructure/notificationProviders/slack.ts index c5745ce05..947048e4c 100644 --- a/server/src/service/infrastructure/notificationProviders/slack.ts +++ b/server/src/service/infrastructure/notificationProviders/slack.ts @@ -9,6 +9,7 @@ import { import got from "got"; export class SlackProvider implements INotificationProvider { + constructor(private logger: any) {} private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -33,6 +34,7 @@ export class SlackProvider implements INotificationProvider { } try { + this.logger?.debug?.("Sending Slack alert", { address: notification.address }); await got.post(notification.address, { json: { text: body }, headers: { @@ -42,6 +44,7 @@ export class SlackProvider implements INotificationProvider { }); return true; } catch (error) { + this.logger?.error?.("Slack alert failed", { error }); return false; } } @@ -52,6 +55,7 @@ export class SlackProvider implements INotificationProvider { } try { + this.logger?.debug?.("Sending Slack test alert", { address: notification.address }); await got.post(notification.address, { json: { text: getTestMessage() }, headers: { @@ -61,6 +65,7 @@ export class SlackProvider implements INotificationProvider { }); return true; } catch (error) { + this.logger?.error?.("Slack test alert failed", { error }); return false; } } diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts index 4d102c2b7..3976fb8e5 100644 --- a/server/src/service/infrastructure/notificationProviders/webhook.ts +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -1,16 +1,22 @@ import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; -import { buildHardwareAlerts, buildHardwareWebhookBody, buildWebhookBody } from "@/service/infrastructure/notificationProviders/utils.js"; +import { + buildHardwareAlerts, + buildHardwareWebhookBody, + buildWebhookBody, + getTestMessage, +} from "@/service/infrastructure/notificationProviders/utils.js"; import got from "got"; export class WebhookProvider implements INotificationProvider { + constructor(private logger: any) {} private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); return body; }; - private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { + private getContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): string => { const body = buildWebhookBody(monitor, monitorStatusResponse); return body; }; @@ -28,8 +34,9 @@ export class WebhookProvider implements INotificationProvider { } try { + this.logger?.debug?.("Sending webhook alert", { address: notification.address }); await got.post(notification.address, { - json: body, + json: { text: body }, headers: { "Content-Type": "application/json", }, @@ -37,11 +44,28 @@ export class WebhookProvider implements INotificationProvider { }); return true; } catch (error) { + this.logger?.error?.("Webhook alert failed", { error }); return false; } }; sendTestAlert = async (notification: Notification) => { - return false; + if (!notification.address) { + return false; + } + try { + this.logger?.debug?.("Sending webhook test alert", { address: notification.address }); + await got.post(notification.address, { + json: { text: getTestMessage() }, + headers: { + "Content-Type": "application/json", + }, + responseType: "json", + }); + return true; + } catch (error) { + this.logger?.error?.("Webhook test alert failed", { error }); + return false; + } }; } diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 7ca52945e..ad27fe99a 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -140,6 +140,7 @@ export class NotificationsService implements INotificationsService { const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds); const tasks = notifications.map((notification) => this.sendTestNotification(notification)); const outcomes = await Promise.all(tasks); + console.log(outcomes); const succeeded = outcomes.filter(Boolean).length; const failed = outcomes.length - succeeded; if (failed > 0) { From 5f17520ba0e7cbda174f8bfa3ed595effec64c24 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 22:16:33 +0000 Subject: [PATCH 280/400] logging --- .../notificationProviders/discord.ts | 29 ++++++++++++++----- .../notificationProviders/email.ts | 27 +++++++++++++---- .../notificationProviders/matrix.ts | 27 ++++++++++++----- .../notificationProviders/pagerduty.ts | 26 +++++++++++++---- .../notificationProviders/slack.ts | 29 ++++++++++++++----- .../notificationProviders/webhook.ts | 27 ++++++++++++----- .../infrastructure/notificationsService.ts | 1 - 7 files changed, 124 insertions(+), 42 deletions(-) diff --git a/server/src/service/infrastructure/notificationProviders/discord.ts b/server/src/service/infrastructure/notificationProviders/discord.ts index 83d8e4ec2..ad1f2f28a 100644 --- a/server/src/service/infrastructure/notificationProviders/discord.ts +++ b/server/src/service/infrastructure/notificationProviders/discord.ts @@ -1,10 +1,15 @@ +const SERVICE_NAME = "DiscordProvider"; import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; import { buildHardwareAlerts, buildDiscordBody, getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; import got from "got"; export class DiscordProvider implements INotificationProvider { - constructor(private logger: any) {} + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { discordPayload } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); return discordPayload; @@ -23,17 +28,21 @@ export class DiscordProvider implements INotificationProvider { } try { - this.logger?.debug?.("Sending Discord alert", { address: notification.address }); await got.post(notification.address, { json: body, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; - } catch (error: any) { - this.logger?.error?.("Discord alert failed", { error }); + } catch (error) { + const err = error as Error; + this.logger.warn({ + message: "Discord alert failed", + service: SERVICE_NAME, + method: "sendAlert", + stack: err?.stack, + }); return false; } }; @@ -42,17 +51,21 @@ export class DiscordProvider implements INotificationProvider { return false; } try { - this.logger?.debug?.("Sending Discord test alert", { address: notification.address }); await got.post(notification.address, { json: { content: getTestMessage() }, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Discord test alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "Discord test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); return false; } }; diff --git a/server/src/service/infrastructure/notificationProviders/email.ts b/server/src/service/infrastructure/notificationProviders/email.ts index 344285e9c..16e48f5c1 100644 --- a/server/src/service/infrastructure/notificationProviders/email.ts +++ b/server/src/service/infrastructure/notificationProviders/email.ts @@ -1,11 +1,15 @@ +const SERVICE_NAME = "EmailProvider"; import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; import { buildHardwareAlerts, buildHardwareEmail, buildEmail, buildTestEmail } from "@/service/infrastructure/notificationProviders/utils.js"; export class EmailProvider implements INotificationProvider { private emailService: any; - constructor(emailService: any, private logger: any) { + private logger: any; + + constructor(emailService: any, logger: any) { this.emailService = emailService; + this.logger = logger; } private buildHardwareEmail = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { @@ -27,18 +31,31 @@ export class EmailProvider implements INotificationProvider { return false; } - this.logger?.debug?.("Sending Email alert", { address: notification.address }); const messageId = await this.emailService.sendEmail(notification.address, subject, html); - if (!messageId) return false; + if (!messageId) { + this.logger.warn({ + message: "Email alert failed", + service: SERVICE_NAME, + method: "sendAlert", + }); + return false; + } return true; } async sendTestAlert(notification: Notification): Promise { const subject = "Test notification"; const html = await buildTestEmail(this.emailService); - this.logger?.debug?.("Sending Email test alert", { address: notification.address }); + const messageId = await this.emailService.sendEmail(notification.address, subject, html); - if (!messageId) return false; + if (!messageId) { + this.logger.warn({ + message: "Email test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + }); + return false; + } return true; } } diff --git a/server/src/service/infrastructure/notificationProviders/matrix.ts b/server/src/service/infrastructure/notificationProviders/matrix.ts index 4592500a5..8914dbf80 100644 --- a/server/src/service/infrastructure/notificationProviders/matrix.ts +++ b/server/src/service/infrastructure/notificationProviders/matrix.ts @@ -1,3 +1,4 @@ +const SERVICE_NAME = "MatrixProvider"; import got from "got"; import type { INotificationProvider } from "@/service/index.js"; import type { Notification, Monitor, MonitorStatusResponse } from "@/types/index.js"; @@ -9,7 +10,11 @@ import { } from "@/service/infrastructure/notificationProviders/utils.js"; export class MatrixProvider implements INotificationProvider { - constructor(private logger: any) {} + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -43,17 +48,21 @@ export class MatrixProvider implements INotificationProvider { formatted_body: formattedMessage, }; try { - this.logger?.debug?.("Sending Matrix alert", { room: roomId }); await got.post(url, { json: body, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Matrix alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "Matrix alert failed", + service: SERVICE_NAME, + method: "sendAlert", + stack: err?.stack, + }); return false; } }; @@ -69,17 +78,21 @@ export class MatrixProvider implements INotificationProvider { body: getTestMessage(), }; try { - this.logger?.debug?.("Sending Matrix test alert", { room: roomId }); await got.post(url, { json: body, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Matrix test alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "Matrix test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); return false; } }; diff --git a/server/src/service/infrastructure/notificationProviders/pagerduty.ts b/server/src/service/infrastructure/notificationProviders/pagerduty.ts index 2d85cd4d6..f65e50c68 100644 --- a/server/src/service/infrastructure/notificationProviders/pagerduty.ts +++ b/server/src/service/infrastructure/notificationProviders/pagerduty.ts @@ -1,3 +1,4 @@ +const SERVICE_NAME = "PagerDutyProvider"; import got from "got"; import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; @@ -9,7 +10,11 @@ import { } from "@/service/infrastructure/notificationProviders/utils.js"; export class PagerDutyProvider implements INotificationProvider { - constructor(private logger: any) {} + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -30,7 +35,6 @@ export class PagerDutyProvider implements INotificationProvider { } try { - this.logger?.debug?.("Sending PagerDuty alert", { routingKey: notification.address }); await got.post("https://events.pagerduty.com/v2/enqueue", { json: { routing_key: notification.address, @@ -42,19 +46,23 @@ export class PagerDutyProvider implements INotificationProvider { timestamp: new Date().toISOString(), }, }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("PagerDuty alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "PagerDuty alert failed", + service: SERVICE_NAME, + method: "sendAlert", + stack: err?.stack, + }); return false; } } async sendTestAlert(notification: Notification): Promise { try { - this.logger?.debug?.("Sending PagerDuty test alert", { routingKey: notification.address }); await got.post("https://events.pagerduty.com/v2/enqueue", { json: { routing_key: notification.address, @@ -70,7 +78,13 @@ export class PagerDutyProvider implements INotificationProvider { }); return true; } catch (error) { - this.logger?.error?.("PagerDuty test alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "PagerDuty test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); return false; } } diff --git a/server/src/service/infrastructure/notificationProviders/slack.ts b/server/src/service/infrastructure/notificationProviders/slack.ts index 947048e4c..2aeb442eb 100644 --- a/server/src/service/infrastructure/notificationProviders/slack.ts +++ b/server/src/service/infrastructure/notificationProviders/slack.ts @@ -1,3 +1,4 @@ +const SERVICE_NAME = "SlackProvider"; import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; import { @@ -6,10 +7,14 @@ import { buildWebhookBody, getTestMessage, } from "@/service/infrastructure/notificationProviders/utils.js"; -import got from "got"; +import got, { HTTPError } from "got"; export class SlackProvider implements INotificationProvider { - constructor(private logger: any) {} + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -34,17 +39,21 @@ export class SlackProvider implements INotificationProvider { } try { - this.logger?.debug?.("Sending Slack alert", { address: notification.address }); await got.post(notification.address, { json: { text: body }, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Slack alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "Slack alert failed", + service: SERVICE_NAME, + method: "sendAlert", + stack: err?.stack, + }); return false; } } @@ -55,17 +64,21 @@ export class SlackProvider implements INotificationProvider { } try { - this.logger?.debug?.("Sending Slack test alert", { address: notification.address }); await got.post(notification.address, { json: { text: getTestMessage() }, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Slack test alert failed", { error }); + const err = error as HTTPError; + this.logger.warn({ + message: "Slack test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); return false; } } diff --git a/server/src/service/infrastructure/notificationProviders/webhook.ts b/server/src/service/infrastructure/notificationProviders/webhook.ts index 3976fb8e5..9e743ae53 100644 --- a/server/src/service/infrastructure/notificationProviders/webhook.ts +++ b/server/src/service/infrastructure/notificationProviders/webhook.ts @@ -1,3 +1,4 @@ +const SERVICE_NAME = "WebhookProvider"; import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/types/index.js"; import { INotificationProvider } from "@/service/index.js"; import { @@ -9,7 +10,11 @@ import { import got from "got"; export class WebhookProvider implements INotificationProvider { - constructor(private logger: any) {} + private logger: any; + + constructor(logger: any) { + this.logger = logger; + } private getHardwareContent = (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse) => { const { alertsToSend } = buildHardwareAlerts("HOST_PLACEHOLDER", monitor, monitorStatusResponse); const body = buildHardwareWebhookBody(alertsToSend, monitor); @@ -34,17 +39,21 @@ export class WebhookProvider implements INotificationProvider { } try { - this.logger?.debug?.("Sending webhook alert", { address: notification.address }); await got.post(notification.address, { json: { text: body }, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Webhook alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "Webhook alert failed", + service: SERVICE_NAME, + method: "sendAlert", + stack: err?.stack, + }); return false; } }; @@ -54,17 +63,21 @@ export class WebhookProvider implements INotificationProvider { return false; } try { - this.logger?.debug?.("Sending webhook test alert", { address: notification.address }); await got.post(notification.address, { json: { text: getTestMessage() }, headers: { "Content-Type": "application/json", }, - responseType: "json", }); return true; } catch (error) { - this.logger?.error?.("Webhook test alert failed", { error }); + const err = error as Error; + this.logger.warn({ + message: "Webhook test alert failed", + service: SERVICE_NAME, + method: "sendTestAlert", + stack: err?.stack, + }); return false; } }; diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index ad27fe99a..7ca52945e 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -140,7 +140,6 @@ export class NotificationsService implements INotificationsService { const notifications = await this.notificationsRepository.findNotificationsByIds(notificationIds); const tasks = notifications.map((notification) => this.sendTestNotification(notification)); const outcomes = await Promise.all(tasks); - console.log(outcomes); const succeeded = outcomes.filter(Boolean).length; const failed = outcomes.length - succeeded; if (failed > 0) { From dde54a4e4540a9b057780779c617322cba3fee10 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 22:18:17 +0000 Subject: [PATCH 281/400] remove old service --- server/src/config/services.ts | 7 - .../infrastructure/notificationService.js | 128 ----------- .../infrastructure/notificationUtils.js | 205 ------------------ 3 files changed, 340 deletions(-) delete mode 100644 server/src/service/infrastructure/notificationService.js delete mode 100644 server/src/service/infrastructure/notificationUtils.js diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 953fc5a1a..9ae5ecb12 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -5,7 +5,6 @@ import MongoDB from "../db/MongoDB.js"; import NetworkService from "../service/infrastructure/networkService.js"; import EmailService from "../service/infrastructure/emailService.js"; import BufferService from "../service/infrastructure/bufferService.js"; -import NotificationUtils from "../service/infrastructure/notificationUtils.js"; import { NotificationsService, StatusService, @@ -222,12 +221,6 @@ export const initializeServices = async ({ const statusService = new StatusService({ db, logger, buffer: bufferService, monitorsRepository }); - const notificationUtils = new NotificationUtils({ - stringService, - emailService, - settingsService, - }); - const webhookProvider = new WebhookProvider(logger); const slackProvider = new SlackProvider(logger); const emailProvider = new EmailProvider(emailService, logger); diff --git a/server/src/service/infrastructure/notificationService.js b/server/src/service/infrastructure/notificationService.js deleted file mode 100644 index 8c6fb063f..000000000 --- a/server/src/service/infrastructure/notificationService.js +++ /dev/null @@ -1,128 +0,0 @@ -const SERVICE_NAME = "NotificationService"; -import Matrix from "./notificationProviders/matrix.js"; - -class NotificationService { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ emailService, db, logger, networkService, stringService, notificationUtils }) { - this.emailService = emailService; - this.db = db; - this.logger = logger; - this.networkService = networkService; - this.stringService = stringService; - this.notificationUtils = notificationUtils; - } - - get serviceName() { - return NotificationService.SERVICE_NAME; - } - - sendNotification = async ({ notification, subject, content, html, discordContent = null, webhookBody = null }) => { - const { type, address } = notification; - - if (type === "email") { - const messageId = await this.emailService.sendEmail(address, subject, html); - if (!messageId) return false; - return true; - } - - // Create a body for webhooks - let body = { text: content }; - if (type === "discord") { - body = !discordContent ? { content } : discordContent; - } - if (type === "webhook") { - body = !webhookBody ? { content } : webhookBody; - } - if (type === "slack" || type === "discord" || type === "webhook") { - const response = await this.networkService.requestWebhook(type, address, body); - return response.status; - } - if (type === "pager_duty") { - const response = await this.networkService.requestPagerDuty({ - message: content, - monitorUrl: subject, - routingKey: address, - }); - - return response; - } - if (type === "matrix") { - const { friendlyName, homeserverUrl, accessToken, roomId } = notification; - const monitorName = subject; - const message = content; - const matrix = new Matrix({ networkService: this.networkService, logger: this.logger }); - const success = await matrix.send({ friendlyName, homeserverUrl, accessToken, roomId, message, monitorName }); - return success; - } - }; - - async handleNotifications(networkResponse) { - const { monitor, statusChanged, prevStatus } = networkResponse; - const { type } = monitor; - if (type !== "hardware" && statusChanged === false) return false; - // if prevStatus is undefined, monitor is resuming, we're done - if (type !== "hardware" && prevStatus === undefined) return false; - - const notificationIDs = networkResponse.monitor?.notifications ?? []; - if (notificationIDs.length === 0) return false; - if (networkResponse.monitor.type === "hardware") { - const thresholds = networkResponse?.monitor?.thresholds; - - if (thresholds === undefined) return false; // No thresholds set, we're done - const metrics = networkResponse?.payload?.data ?? null; - if (metrics === null) return false; // No metrics, we're done - - const [alerts, discordContent] = await this.notificationUtils.buildHardwareAlerts(networkResponse); - if (alerts.length === 0) return false; - - const { subject, html } = await this.notificationUtils.buildHardwareEmail(networkResponse, alerts); - const content = await this.notificationUtils.buildHardwareNotificationMessage(alerts, monitor); - const webhookBody = await this.notificationUtils.buildHardwareWebhookBody(alerts, monitor); - const success = await this.notifyAll({ notificationIDs, subject, html, content, discordContent, webhookBody }); - return success; - } - - // Status monitors - const { subject, html } = await this.notificationUtils.buildStatusEmail(networkResponse); - const [content, discordContent] = await this.notificationUtils.buildWebhookMessage(networkResponse); - const success = this.notifyAll({ notificationIDs, subject, html, content, discordContent }); - return success; - } - - async notifyAll({ notificationIDs, subject, html, content, discordContent = null, webhookBody = null }) { - const notifications = await this.db.notificationModule.getNotificationsByIds(notificationIDs); - // Map each notification to a test promise - const promises = notifications.map(async (notification) => { - try { - await this.sendNotification({ notification, subject, content, html, discordContent, webhookBody }); - return true; - } catch (err) { - return false; - } - }); - - const results = await Promise.all(promises); - return results.every((r) => r === true); - } - - async getTestNotification() { - const html = await this.notificationUtils.buildTestEmail(); - const content = "This is a test notification"; - const subject = "Test Notification"; - return { subject, html, content }; - } - - async testAllNotifications(notificationIDs) { - const { subject, html, content } = await this.getTestNotification(); - return this.notifyAll({ notificationIDs, subject, html, content }); - } - - async sendTestNotification(notification) { - const { subject, html, content } = await this.getTestNotification(); - const success = await this.sendNotification({ notification, subject, content, html }); - return success; - } -} - -export default NotificationService; diff --git a/server/src/service/infrastructure/notificationUtils.js b/server/src/service/infrastructure/notificationUtils.js deleted file mode 100644 index 106c8f349..000000000 --- a/server/src/service/infrastructure/notificationUtils.js +++ /dev/null @@ -1,205 +0,0 @@ -const SERVICE_NAME = "NotificationUtils"; - -class NotificationUtils { - static SERVICE_NAME = SERVICE_NAME; - - constructor({ stringService, emailService, settingsService }) { - this.stringService = stringService; - this.emailService = emailService; - this.settingsService = settingsService; - } - - get serviceName() { - return NotificationUtils.SERVICE_NAME; - } - - buildTestEmail = async () => { - const context = { testName: "Monitoring System" }; - const html = await this.emailService.buildEmail("testEmailTemplate", context); - return html; - }; - - buildStatusEmail = async (networkResponse) => { - const { monitor, status, prevStatus } = networkResponse; - const template = prevStatus === false ? "serverIsUpTemplate" : "serverIsDownTemplate"; - const context = { monitor: monitor.name, url: monitor.url }; - const subject = `Monitor ${monitor.name} is ${status === true ? "up" : "down"}`; - const html = await this.emailService.buildEmail(template, context); - return { subject, html }; - }; - - buildWebhookMessage = (networkResponse) => { - const { monitor, status, code, timestamp } = networkResponse; - // Format timestamp using the local system timezone - const formatTime = (timestamp) => { - const date = new Date(timestamp); - - // Get timezone abbreviation and format the date - const timeZoneAbbr = date.toLocaleTimeString("en-US", { timeZoneName: "short" }).split(" ").pop(); - - // Format the date with readable format - return ( - date - .toLocaleString("en-US", { - year: "numeric", - month: "2-digit", - day: "2-digit", - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - hour12: false, - }) - .replace(/(\d+)\/(\d+)\/(\d+),\s/, "$3-$1-$2 ") + - " " + - timeZoneAbbr - ); - }; - - // Get formatted time - const formattedTime = timestamp ? formatTime(timestamp) : formatTime(new Date().getTime()); - - // Create different messages based on status with extra spacing - let messageText; - if (status === true) { - messageText = this.stringService.monitorUpAlert - .replace("{monitorName}", monitor.name) - .replace("{time}", formattedTime) - .replace("{code}", code || "Unknown"); - } else { - messageText = this.stringService.monitorDownAlert - .replace("{monitorName}", monitor.name) - .replace("{time}", formattedTime) - .replace("{code}", code || "Unknown"); - } - const dn = this.stringService.discordNotification; - let discordMessageText = { - embeds: [ - { - title: status - ? dn.uptimeAlert.replace("{monitorName}", monitor?.name ?? dn.unknown) - : dn.downtimeAlert.replace("{monitorName}", monitor?.name ?? dn.unknown), - color: status ? 5763719 : 15548997, - - fields: [ - { name: dn.monitor, value: monitor?.name ?? dn.unknown, inline: true }, - { name: dn.status, value: status ? dn.up : dn.down, inline: true }, - { name: dn.statusCode, value: String(code ?? dn.unknown), inline: true }, - { name: dn.time, value: formattedTime, inline: true }, - { name: dn.url, value: monitor?.url ?? dn.unknown, inline: false }, - ], - footer: { text: dn.checkmate }, - }, - ], - }; - return [messageText, discordMessageText]; - }; - - buildHardwareAlerts = async (networkResponse) => { - const monitor = networkResponse?.monitor; - const thresholds = networkResponse?.monitor?.thresholds; - const { usage_cpu: cpuThreshold = -1, usage_memory: memoryThreshold = -1, usage_disk: diskThreshold = -1 } = thresholds; - - const metrics = networkResponse?.payload?.data; - const { cpu: { usage_percent: cpuUsage = -1 } = {}, memory: { usage_percent: memoryUsage = -1 } = {}, disk = [] } = metrics; - - const { clientHost } = this.settingsService.getSettings(); - - const alerts = { - cpu: cpuThreshold !== -1 && cpuUsage > cpuThreshold ? true : false, - memory: memoryThreshold !== -1 && memoryUsage > memoryThreshold ? true : false, - disk: disk?.some((d) => diskThreshold !== -1 && typeof d?.usage_percent === "number" && d?.usage_percent > diskThreshold) ?? false, - }; - - const alertsToSend = []; - const discordEmbeds = []; - const monitorInfoFields = [ - { name: "Monitor", value: monitor.name, inline: true }, - { name: "URL", value: monitor.url, inline: false }, - ]; - const goToIncidentField = { name: `Go to incident`, value: `${clientHost}/infrastructure/${monitor._id}` }; - const formatDiscordAlert = { - cpu: () => ({ - title: "CPU alert", - description: `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, - color: 15548997, - fields: [...monitorInfoFields, goToIncidentField], - footer: { text: "Checkmate" }, - }), - - memory: () => ({ - title: "Memory alert", - description: `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, - color: 15548997, - fields: [...monitorInfoFields, goToIncidentField], - footer: { text: "Checkmate" }, - }), - - disk: () => ({ - title: "Disk alert", - description: `Your current disk usage is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, - color: 15548997, - footer: { text: "Checkmate" }, - fields: [ - ...monitorInfoFields, - ...(Array.isArray(disk) ? disk : []).map((d, idx) => ({ - name: `Disk ${idx}`, - value: `${(d?.usage_percent * 100).toFixed(0)}%`, - inline: true, - })), - goToIncidentField, - ], - }), - }; - const alertTypes = ["cpu", "memory", "disk"]; - for (const type of alertTypes) { - // Iterate over each alert type to see if any need to be decremented - if (alerts[type] === true) { - monitor[`${type}AlertThreshold`]--; // Decrement threshold if an alert is triggered - - if (monitor[`${type}AlertThreshold`] <= 0) { - // If threshold drops below 0, reset and send notification - monitor[`${type}AlertThreshold`] = monitor.alertThreshold; - - const formatAlert = { - cpu: () => `Your current CPU usage (${(cpuUsage * 100).toFixed(0)}%) is above your threshold (${(cpuThreshold * 100).toFixed(0)}%)`, - memory: () => - `Your current memory usage (${(memoryUsage * 100).toFixed(0)}%) is above your threshold (${(memoryThreshold * 100).toFixed(0)}%)`, - disk: () => - `Your current disk usage: ${disk - .map((d, idx) => `(Disk${idx}: ${(d.usage_percent * 100).toFixed(0)}%)`) - .join(", ")} is above your threshold (${(diskThreshold * 100).toFixed(0)}%)`, - }; - alertsToSend.push(formatAlert[type]()); - discordEmbeds.push(formatDiscordAlert[type]()); - } - } - } - await monitor.save(); - const discordPayload = discordEmbeds.length ? { embeds: discordEmbeds } : null; - return [alertsToSend, discordPayload]; - }; - - buildHardwareEmail = async (networkResponse, alerts) => { - const { monitor } = networkResponse; - const template = "hardwareIncidentTemplate"; - const context = { monitor: monitor.name, url: monitor.url, alerts }; - const subject = `Monitor ${monitor.name} infrastructure alerts`; - const html = await this.emailService.buildEmail(template, context); - return { subject, html }; - }; - - buildHardwareNotificationMessage = (alerts, monitor) => { - const { clientHost } = this.settingsService.getSettings(); - const alertsHeader = [`Monitor: ${monitor.name}`, `URL: ${monitor.url}`]; - const alertFooter = [`Go to incident: ${clientHost}/infrastructure/${monitor._id}`]; - const alertText = alerts.length > 0 ? [...alertsHeader, ...alerts, ...alertFooter] : []; - return alertText.map((alert) => alert).join("\n"); - }; - buildHardwareWebhookBody = (alerts, monitor) => { - const content = alerts.map((alert) => alert).join("\n"); - const body = { text: content, name: monitor.name, url: monitor.url }; - return body; - }; -} - -export default NotificationUtils; From e91a54b75c33327b1cf20e0a2dd89178c2942087 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 19 Jan 2026 23:41:24 +0000 Subject: [PATCH 282/400] initial commit --- server/src/config/services.ts | 7 + server/src/controllers/incidentController.ts | 12 +- .../db/models/{Incident.js => Incident.ts} | 42 +-- server/src/db/models/index.ts | 3 + .../incidents/IIncidentsRepository.ts | 22 ++ .../incidents/MongoIncidentRepository.ts | 100 +++++++ server/src/repositories/index.ts | 3 + ...{incidentService.js => incidentService.ts} | 252 +++++++----------- .../SuperSimpleQueueHelper.ts | 11 + .../service/infrastructure/statusService.ts | 64 ----- server/src/types/incident.ts | 20 ++ server/src/types/index.ts | 1 + 12 files changed, 295 insertions(+), 242 deletions(-) rename server/src/db/models/{Incident.js => Incident.ts} (55%) create mode 100644 server/src/repositories/incidents/IIncidentsRepository.ts create mode 100644 server/src/repositories/incidents/MongoIncidentRepository.ts rename server/src/service/business/{incidentService.js => incidentService.ts} (55%) create mode 100644 server/src/types/incident.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 9ae5ecb12..cbd58f2e9 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -84,6 +84,7 @@ import { MongoRecoveryTokensRepository, MongoSettingsRepository, MongoNotificationsRepository, + MongoIncidentRepository, IMonitorsRepository, IChecksRepository, IMonitorStatsRepository, @@ -93,6 +94,7 @@ import { IRecoveryTokensRepository, ISettingsRepository, INotificationsRepository, + IIncidentsRepository, } from "@/repositories/index.js"; export type InitializedSerivces = { @@ -127,6 +129,7 @@ export type InitializedSerivces = { recoveryTokensRepository: IRecoveryTokensRepository; settingsRepository: ISettingsRepository; notificationsRepository: INotificationsRepository; + incidentsRepository: IIncidentsRepository; }; export const initializeServices = async ({ @@ -183,6 +186,7 @@ export const initializeServices = async ({ const recoveryTokensRepository = new MongoRecoveryTokensRepository(); const settingsRepository = new MongoSettingsRepository(); const notificationsRepository = new MongoNotificationsRepository(); + const incidentsRepository = new MongoIncidentRepository(); const networkService = new NetworkService({ axios, got, @@ -205,6 +209,7 @@ export const initializeServices = async ({ logger, errorService, stringService, + incidentsRepository, }); const checkService = new CheckService({ @@ -247,6 +252,7 @@ export const initializeServices = async ({ notificationsService, checkService, buffer: bufferService, + incidentService, }); const superSimpleQueue = await SuperSimpleQueue.create({ @@ -332,6 +338,7 @@ export const initializeServices = async ({ recoveryTokensRepository, settingsRepository, notificationsRepository, + incidentsRepository, }; Object.values(services).forEach((service) => { diff --git a/server/src/controllers/incidentController.ts b/server/src/controllers/incidentController.ts index b17788d80..645149acd 100644 --- a/server/src/controllers/incidentController.ts +++ b/server/src/controllers/incidentController.ts @@ -67,12 +67,12 @@ class IncidentController { resolveIncidentManually = async (req: Request, res: Response, next: NextFunction) => { try { - const resolvedIncident = await this.incidentService.resolveIncidentManually({ - incidentId: req?.params?.incidentId, - userId: req?.user?.id, - teamId: req?.user?.teamId, - comment: req?.body?.comment, - }); + const resolvedIncident = await this.incidentService.resolveIncident( + req?.params?.incidentId, + req?.user?.id, + req?.user?.teamId, + req?.body?.comment + ); return res.status(200).json({ success: true, diff --git a/server/src/db/models/Incident.js b/server/src/db/models/Incident.ts similarity index 55% rename from server/src/db/models/Incident.js rename to server/src/db/models/Incident.ts index a9477301b..8de1c7c2c 100644 --- a/server/src/db/models/Incident.js +++ b/server/src/db/models/Incident.ts @@ -1,16 +1,31 @@ -import mongoose from "mongoose"; +import { Schema, model, type Types } from "mongoose"; +import { IncidentResolutionTypes, type Incident } from "@/types/incident.js"; -const IncidentSchema = mongoose.Schema( +type IncidentDocumentBase = Omit & { + monitorId: Types.ObjectId; + teamId: Types.ObjectId; + resolvedBy?: Types.ObjectId | null; + startTime: Date; + endTime: Date | null; + createdAt: Date; + updatedAt: Date; +}; + +export interface IncidentDocument extends IncidentDocumentBase { + _id: Types.ObjectId; +} + +const IncidentSchema = new Schema( { monitorId: { - type: mongoose.Schema.Types.ObjectId, + type: Schema.Types.ObjectId, ref: "Monitor", required: true, immutable: true, index: true, }, teamId: { - type: mongoose.Schema.Types.ObjectId, + type: Schema.Types.ObjectId, ref: "Team", required: true, immutable: true, @@ -18,8 +33,8 @@ const IncidentSchema = mongoose.Schema( }, startTime: { type: Date, - required: true, immutable: true, + required: true, }, endTime: { type: Date, @@ -36,16 +51,16 @@ const IncidentSchema = mongoose.Schema( }, statusCode: { type: Number, - index: true, default: null, + index: true, }, resolutionType: { type: String, - enum: ["automatic", "manual"], + enum: IncidentResolutionTypes, default: null, }, resolvedBy: { - type: mongoose.Schema.Types.ObjectId, + type: Schema.Types.ObjectId, ref: "User", default: null, }, @@ -53,12 +68,6 @@ const IncidentSchema = mongoose.Schema( type: String, default: null, }, - checks: [ - { - type: mongoose.Schema.Types.ObjectId, - ref: "Check", - }, - ], }, { timestamps: true } ); @@ -71,6 +80,7 @@ IncidentSchema.index({ resolutionType: 1, status: 1 }); IncidentSchema.index({ resolvedBy: 1, status: 1 }); IncidentSchema.index({ createdAt: -1 }); -const Incident = mongoose.model("Incident", IncidentSchema); +const IncidentModel = model("Incident", IncidentSchema); -export default Incident; +export { IncidentModel }; +export default IncidentModel; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index 18a3a3b19..4b71937bb 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -24,3 +24,6 @@ export { default as RecoveryTokenModel } from "@/db/models/RecoveryToken.js"; export * from "@/db/models/Notification.js"; export { default as NotificationModel } from "@/db/models/Notification.js"; + +export * from "@/db/models/Incident.js"; +export { default as IncidentModel } from "@/db/models/Incident.js"; diff --git a/server/src/repositories/incidents/IIncidentsRepository.ts b/server/src/repositories/incidents/IIncidentsRepository.ts new file mode 100644 index 000000000..00235de0d --- /dev/null +++ b/server/src/repositories/incidents/IIncidentsRepository.ts @@ -0,0 +1,22 @@ +import type { Incident } from "@/types/index.js"; +export interface IIncidentsRepository { + // create + create(incident: Partial): Promise; + // fetch + findActiveByIncidentId(incidentId: string, teamId: string): Promise; + findActiveByMonitorId(monitorId: string, teamId: string): Promise; + findByTeamId( + teamId: string, + dateRange: string, + page: number, + rowsPerPage: number, + sortOrder?: string, + status?: string, + monitorId?: string, + resolutionType?: string + ): Promise; + + // update + updateById(incidentId: string, teamId: string, updateData: Partial): Promise; + // delete +} diff --git a/server/src/repositories/incidents/MongoIncidentRepository.ts b/server/src/repositories/incidents/MongoIncidentRepository.ts new file mode 100644 index 000000000..db0048fc7 --- /dev/null +++ b/server/src/repositories/incidents/MongoIncidentRepository.ts @@ -0,0 +1,100 @@ +import { IncidentModel } from "@/db/models/index.js"; +import type { IncidentDocument } from "@/db/models/Incident.js"; +import type { Incident } from "@/types/index.js"; +import type { IIncidentsRepository } from "@/repositories/index.js"; +import mongoose from "mongoose"; +import { AppError } from "@/utils/AppError.js"; + +class MongoIncidentRepository implements IIncidentsRepository { + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + protected toEntity = (doc: IncidentDocument): Incident => { + return { + id: this.toStringId(doc._id), + monitorId: this.toStringId(doc.monitorId), + teamId: this.toStringId(doc.teamId), + startTime: this.toDateString(doc.startTime), + endTime: doc.endTime ? this.toDateString(doc.endTime) : null, + status: doc.status, + message: doc.message ?? null, + statusCode: doc.statusCode ?? null, + resolutionType: doc.resolutionType ?? null, + resolvedBy: doc.resolvedBy ? this.toStringId(doc.resolvedBy) : null, + comment: doc.comment ?? null, + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + protected mapDocuments = (documents: IncidentDocument[] | IncidentDocument | null): Incident[] => { + if (!documents) { + return []; + } + if (Array.isArray(documents)) { + return documents.map((doc) => this.toEntity(doc)); + } + return [this.toEntity(documents)]; + }; + + async create(incident: Partial): Promise { + const newIncident = await IncidentModel.create(incident); + return this.toEntity(newIncident); + } + + findActiveByIncidentId = async (incidentId: string, teamId: string): Promise => { + const incident = await IncidentModel.findOne({ + _id: new mongoose.Types.ObjectId(incidentId), + teamId: new mongoose.Types.ObjectId(teamId), + status: true, + }); + if (!incident) { + return null; + } + return this.toEntity(incident); + }; + + findActiveByMonitorId = async (monitorId: string, teamId: string): Promise => { + const incident = await IncidentModel.findOne({ + monitorId: new mongoose.Types.ObjectId(monitorId), + teamId: new mongoose.Types.ObjectId(teamId), + status: true, + }); + if (!incident) { + return null; + } + return this.toEntity(incident); + }; + + findByTeamId = async (teamId: string): Promise => { + throw new Error("Method not implemented."); + }; + + updateById = async (incidentId: string, teamId: string, patch: Partial) => { + const updatedIncident = await IncidentModel.findOneAndUpdate( + { _id: new mongoose.Types.ObjectId(incidentId), teamId: new mongoose.Types.ObjectId(teamId) }, + { + $set: { + ...patch, + }, + }, + { new: true, runValidators: true } + ); + if (!updatedIncident) { + throw new AppError({ message: `Failed to update incident with id ${incidentId}`, status: 500 }); + } + return this.toEntity(updatedIncident); + }; +} +export default MongoIncidentRepository; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 1a3199081..72dfd2f40 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -24,3 +24,6 @@ export { default as MongoSettingsRepository } from "@/repositories/settings/Mong export * from "@/repositories/notifications/INotificationsRepository.js"; export { default as MongoNotificationsRepository } from "@/repositories/notifications/MongoNotificationsRepository.js"; + +export * from "@/repositories/incidents/IIncidentsRepository.js"; +export { default as MongoIncidentRepository } from "@/repositories/incidents/MongoIncidentRepository.js"; diff --git a/server/src/service/business/incidentService.js b/server/src/service/business/incidentService.ts similarity index 55% rename from server/src/service/business/incidentService.js rename to server/src/service/business/incidentService.ts index db89caa68..a96c7923d 100644 --- a/server/src/service/business/incidentService.js +++ b/server/src/service/business/incidentService.ts @@ -1,183 +1,123 @@ const SERVICE_NAME = "incidentService"; +import type { Monitor } from "@/types/monitor.js"; +import { AppError } from "@/utils/AppError.js"; +import type { IIncidentsRepository } from "@/repositories/index.js"; +import type { Incident } from "@/types/index.js"; class IncidentService { static SERVICE_NAME = SERVICE_NAME; - constructor({ db, logger, errorService, stringService }) { + private db: any; + private logger: any; + private errorService: any; + private stringService: any; + private incidentsRepository: IIncidentsRepository; + + constructor({ + db, + logger, + errorService, + stringService, + incidentsRepository, + }: { + db: any; + logger: any; + errorService: any; + stringService: any; + incidentsRepository: IIncidentsRepository; + }) { this.db = db; this.logger = logger; this.errorService = errorService; this.stringService = stringService; + this.incidentsRepository = incidentsRepository; } get serviceName() { return IncidentService.SERVICE_NAME; } - createIncident = async (monitor, check) => { - try { - if (!monitor || !monitor._id) { - throw this.errorService.createBadRequestError("Monitor is required"); - } - - if (!check || !check._id) { - throw this.errorService.createBadRequestError("Check is required"); - } - - const activeIncident = await this.db.incidentModule.getActiveIncidentByMonitor(monitor._id); - + handleIncident = async (monitor: Monitor, code: number): Promise => { + const activeIncident = await this.incidentsRepository.findActiveByMonitorId(monitor.id, monitor.teamId); + // Monitor is down, create an incident + if (monitor.status === false) { if (activeIncident) { - await this.db.incidentModule.addCheckToIncident(activeIncident._id, check._id); - - this.logger.info({ - service: this.SERVICE_NAME, - method: "createIncident", - message: `Check added to existing active incident for monitor ${monitor.name}`, - incidentId: activeIncident._id, - monitorId: monitor._id, - }); - return activeIncident; + } else { + const incident = { + monitorId: monitor.id, + teamId: monitor.teamId, + startTime: Date.now().toString(), + status: true, + statusCode: code, + }; + return await this.incidentsRepository.create(incident); } - - const incidentData = { - monitorId: monitor._id, - teamId: monitor.teamId, - type: monitor.type, - startTime: new Date(), - status: true, - message: check.message || null, - statusCode: check.statusCode || null, - checks: [check._id], - }; - - const incident = await this.db.incidentModule.createIncident(incidentData); - - this.logger.info({ - service: this.SERVICE_NAME, - method: "createIncident", - message: `New incident created for monitor ${monitor.name}`, - incidentId: incident._id, - monitorId: monitor._id, - }); - - return incident; - } catch (error) { - this.logger.error({ - service: this.SERVICE_NAME, - method: "createIncident", - message: error.message, - monitorId: monitor?._id, - error: error.stack, - }); - throw error; } + + // Monitor is up, resolve active incidents + if (!activeIncident) { + return null; + } + activeIncident.status = false; + activeIncident.endTime = Date.now().toString(); + activeIncident.resolutionType = "automatic"; + return await this.incidentsRepository.updateById(activeIncident.id, activeIncident.teamId, activeIncident); }; - resolveIncident = async (monitor, check) => { - try { - if (!monitor || !monitor._id) { - throw this.errorService.createBadRequestError("Monitor is required"); - } - - const activeIncident = await this.db.incidentModule.getActiveIncidentByMonitor(monitor._id); - - if (!activeIncident) { - this.logger.info({ - service: this.SERVICE_NAME, - method: "resolveIncident", - message: `No active incident found for monitor ${monitor.name}`, - monitorId: monitor._id, - }); - return null; - } - - await this.db.incidentModule.addCheckToIncident(activeIncident._id, check._id); - - const resolvedIncident = await this.db.incidentModule.resolveIncident(activeIncident._id, { - resolutionType: "automatic", - resolvedBy: null, - endTime: new Date(), - }); - - this.logger.info({ - service: this.SERVICE_NAME, - method: "resolveIncident", - message: `Incident automatically resolved for monitor ${monitor.name}`, - incidentId: resolvedIncident._id, - monitorId: monitor._id, - }); - - return resolvedIncident; - } catch (error) { - this.logger.error({ - service: this.SERVICE_NAME, - method: "resolveIncident", - message: error.message, - monitorId: monitor?._id, - error: error.stack, - }); - throw error; - } - }; - - resolveIncidentManually = async ({ incidentId, userId, teamId, comment }) => { + resolveIncident = async (incidentId: string, userId: string, teamId: string, comment?: string) => { try { if (!incidentId) { - throw this.errorService.createBadRequestError("No incident ID in request"); + throw new AppError({ message: "No incident ID in request", service: SERVICE_NAME, method: "resolveIncident" }); } if (!userId) { - throw this.errorService.createBadRequestError("No user ID in request"); + throw new AppError({ message: "No user ID in request", service: SERVICE_NAME, method: "resolveIncident" }); } if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); + throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "resolveIncident" }); } - const incident = await this.db.incidentModule.getIncidentById(incidentId); + const incident = await this.incidentsRepository.findActiveByIncidentId(incidentId, teamId); if (!incident) { - throw this.errorService.createNotFoundError("Incident not found"); - } - - if (!incident.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); + throw new AppError({ message: "Incident not found", service: SERVICE_NAME, method: "resolveIncident" }); } if (incident.status === false) { - throw this.errorService.createBadRequestError("Incident is already resolved"); + throw new AppError({ message: "Incident is already resolved", service: SERVICE_NAME, method: "resolveIncident" }); } - const resolvedIncident = await this.db.incidentModule.resolveIncident(incidentId, { - resolutionType: "manual", - resolvedBy: userId, - comment: comment || null, - endTime: new Date(), - }); + incident.resolutionType = "manual"; + incident.status = false; + incident.resolvedBy = userId; + incident.comment = comment || null; + incident.endTime = Date.now().toString(); - this.logger.info({ - service: this.SERVICE_NAME, + const resolvedIncident = await this.incidentsRepository.updateById(incident.id, teamId, incident); + + this.logger.debug({ + service: SERVICE_NAME, method: "resolveIncidentManually", message: `Incident manually resolved by user`, - incidentId: resolvedIncident._id, - userId, + details: resolvedIncident.id, }); return resolvedIncident; - } catch (error) { + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, - method: "resolveIncidentManually", + service: SERVICE_NAME, + method: "resolveIncident", message: error.message, - incidentId, - error: error.stack, + details: incidentId, + stack: error.stack, }); throw error; } }; - getIncidentsByTeam = async ({ teamId, query }) => { + getIncidentsByTeam = async ({ teamId, query }: { teamId: string; query?: any }) => { try { if (!teamId) { throw this.errorService.createBadRequestError("No team ID in request"); @@ -197,19 +137,19 @@ class IncidentService { }); return result; - } catch (error) { + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "getIncidentsByTeam", message: error.message, - teamId, - error: error.stack, + details: teamId, + stack: error.stack, }); throw error; } }; - getIncidentSummary = async ({ teamId, query }) => { + getIncidentSummary = async ({ teamId, query }: { teamId: string; query?: any }) => { try { if (!teamId) { throw this.errorService.createBadRequestError("No team ID in request"); @@ -223,19 +163,19 @@ class IncidentService { }); return summary; - } catch (error) { + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "getIncidentSummary", message: error.message, - teamId, - error: error.stack, + details: teamId, + stack: error.stack, }); throw error; } }; - getIncidentById = async ({ incidentId, teamId }) => { + getIncidentById = async ({ incidentId, teamId }: { incidentId: string; teamId: string }) => { try { if (!incidentId) { throw this.errorService.createBadRequestError("No incident ID in request"); @@ -256,19 +196,19 @@ class IncidentService { } return incident; - } catch (error) { + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "getIncidentById", message: error.message, - incidentId, - error: error.stack, + details: incidentId, + stack: error.stack, }); throw error; } }; - processIncidentsFromBuffer = async (incidentBufferItems) => { + processIncidentsFromBuffer = async (incidentBufferItems: any[]) => { try { if (!incidentBufferItems || incidentBufferItems.length === 0) { return; @@ -285,16 +225,16 @@ class IncidentService { } } - for (const item of resolveItems) { + for (const item of resolveItems as any[]) { try { - await this.resolveIncident(item.monitor, item.check); - } catch (error) { + await this.resolveIncident(item.monitor); + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "processIncidentsFromBuffer", message: `Failed to resolve incident from buffer: ${error.message}`, monitorId: item.monitor?._id, - error: error.stack, + stack: error.stack, }); } } @@ -303,11 +243,11 @@ class IncidentService { return; } - const groupedByMonitor = {}; + const groupedByMonitor: Record = {}; for (const item of createItems) { if (!item.monitor || !item.monitor.id || !item.check || !item.check.id) { this.logger.warn({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "processIncidentsFromBuffer", message: "Skipping item with missing monitor or check data", item, @@ -329,7 +269,7 @@ class IncidentService { const activeIncidents = await this.db.incidentModule.getActiveIncidentsByMonitors(monitorIds); - const incidentsCreatedInFlush = {}; + const incidentsCreatedInFlush: Record = {}; const checksToAddToIncidents = []; const newIncidentsToCreate = []; @@ -392,19 +332,19 @@ class IncidentService { } this.logger.info({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "processIncidentsFromBuffer", message: `Processed ${incidentBufferItems.length} incident buffer items`, created: newIncidentsToCreate.length, checksAdded: checksToAddToIncidents.length, resolved: resolveItems.length, }); - } catch (error) { + } catch (error: any) { this.logger.error({ - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "processIncidentsFromBuffer", message: error.message, - error: error.stack, + stack: error.stack, }); throw error; } diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index bbd3a11f0..26d45db22 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -2,6 +2,8 @@ const SERVICE_NAME = "JobQueueHelper"; import type { Monitor } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js"; +import IncidentService from "@/service/business/incidentService.js"; + class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; @@ -12,6 +14,7 @@ class SuperSimpleQueueHelper { private notificationsService: INotificationsService; private checkService: any; private buffer: any; + private incidentService: IncidentService; constructor({ db, @@ -21,6 +24,7 @@ class SuperSimpleQueueHelper { notificationsService, checkService, buffer, + incidentService, }: { db: any; logger: any; @@ -29,6 +33,7 @@ class SuperSimpleQueueHelper { notificationsService: INotificationsService; checkService: any; buffer: any; + incidentService: IncidentService; }) { this.db = db; this.logger = logger; @@ -37,6 +42,7 @@ class SuperSimpleQueueHelper { this.checkService = checkService; this.buffer = buffer; this.notificationsService = notificationsService; + this.incidentService = incidentService; } get serviceName() { @@ -90,6 +96,11 @@ class SuperSimpleQueueHelper { stack: error.stack, }); }); + + // Step 6. Handle incidents + if (statusChangeResult.statusChanged) { + await this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code); + } } catch (error: any) { this.logger.warn({ message: error.message, diff --git a/server/src/service/infrastructure/statusService.ts b/server/src/service/infrastructure/statusService.ts index 3d9591c02..3a83dbd28 100755 --- a/server/src/service/infrastructure/statusService.ts +++ b/server/src/service/infrastructure/statusService.ts @@ -218,70 +218,6 @@ export class StatusService implements IStatusService { statusChanged = true; } - if (statusChanged) { - this.logger.info({ - service: SERVICE_NAME, - message: `${monitor.name} went from ${this.getStatusString(prevStatus)} to ${this.getStatusString(newStatus)}`, - prevStatus, - newStatus, - }); - - if (newStatus === false) { - await this.handleIncidentForCheck(check, monitor, "create", "status change to down"); - } else if (prevStatus === false) { - await this.handleIncidentForCheck(check, monitor, "resolve", "status change to up"); - } - } - - if (monitor.status === false && !statusChanged) { - try { - const lastManuallyResolvedIncident = await this.db.incidentModule.getLastManuallyResolvedIncident(monitor.id); - - let calculatedFailureRate = failureRate; - - if (lastManuallyResolvedIncident && lastManuallyResolvedIncident.endTime) { - try { - const checksAfterResolution = await CheckModel.find({ - monitorId: monitor.id, - createdAt: { $gt: lastManuallyResolvedIncident.endTime }, - }) - .sort({ createdAt: 1 }) - .limit(monitor.statusWindowSize) - .select("status") - .lean(); - - if (checksAfterResolution.length > 0) { - const checksStatuses = checksAfterResolution.map((c) => c.status); - const failuresAfterResolution = checksStatuses.filter((s) => s === false).length; - calculatedFailureRate = (failuresAfterResolution / monitor.statusWindow.length) * 100; - } else { - calculatedFailureRate = 0; - } - } catch (checkQueryError: any) { - this.logger.error({ - service: SERVICE_NAME, - method: "updateStatus", - message: `Failed to query checks after manual resolution: ${checkQueryError.message}`, - monitorId: monitor.id, - stack: checkQueryError.stack, - }); - } - } - - if (calculatedFailureRate >= monitor.statusWindowThreshold) { - await this.handleIncidentForCheck(check, monitor, "create", "threshold check without status change"); - } - } catch (error: any) { - this.logger.error({ - service: SERVICE_NAME, - method: "updateStatus", - message: `Error handling threshold check without status change: ${error.message}`, - monitorId: monitor.id, - stack: error.stack, - }); - } - } - monitor.status = newStatus; const updated = await this.monitorsRepository.updateById(monitor.id, monitor.teamId, monitor); diff --git a/server/src/types/incident.ts b/server/src/types/incident.ts new file mode 100644 index 000000000..65186f08c --- /dev/null +++ b/server/src/types/incident.ts @@ -0,0 +1,20 @@ +// export type IncidentResolutionType = "automatic" | "manual" | null; + +export const IncidentResolutionTypes = ["automatic", "manual", null] as const; +export type IncidentResolutionType = (typeof IncidentResolutionTypes)[number]; + +export interface Incident { + id: string; + monitorId: string; + teamId: string; + startTime: string; + endTime: string | null; + status: boolean; + message?: string | null; + statusCode?: number | null; + resolutionType: IncidentResolutionType; + resolvedBy?: string | null; + comment?: string | null; + createdAt: string; + updatedAt: string; +} diff --git a/server/src/types/index.ts b/server/src/types/index.ts index fda48f0c6..e28d75e91 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -9,3 +9,4 @@ export * from "@/types/recoveryToken.js"; export * from "@/types/settings.js"; export * from "@/types/notification.js"; export * from "@/types/alert.js"; +export * from "@/types/incident.js"; From adb2a383cfd6705473bc40b12de9eeeeb87f6190 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 00:02:27 +0000 Subject: [PATCH 283/400] refactor --- .../incidents/IIncidentsRepository.ts | 3 +- .../incidents/MongoIncidentRepository.ts | 25 ++++++++- .../src/service/business/incidentService.ts | 51 +++++++++++++++---- 3 files changed, 65 insertions(+), 14 deletions(-) diff --git a/server/src/repositories/incidents/IIncidentsRepository.ts b/server/src/repositories/incidents/IIncidentsRepository.ts index 00235de0d..dbdf2b05f 100644 --- a/server/src/repositories/incidents/IIncidentsRepository.ts +++ b/server/src/repositories/incidents/IIncidentsRepository.ts @@ -7,7 +7,8 @@ export interface IIncidentsRepository { findActiveByMonitorId(monitorId: string, teamId: string): Promise; findByTeamId( teamId: string, - dateRange: string, + startDate: Date, + endDate: Date, page: number, rowsPerPage: number, sortOrder?: string, diff --git a/server/src/repositories/incidents/MongoIncidentRepository.ts b/server/src/repositories/incidents/MongoIncidentRepository.ts index db0048fc7..ead783e11 100644 --- a/server/src/repositories/incidents/MongoIncidentRepository.ts +++ b/server/src/repositories/incidents/MongoIncidentRepository.ts @@ -77,8 +77,29 @@ class MongoIncidentRepository implements IIncidentsRepository { return this.toEntity(incident); }; - findByTeamId = async (teamId: string): Promise => { - throw new Error("Method not implemented."); + findByTeamId = async ( + teamId: string, + startDate: Date, + endDate: Date, + page: number, + rowsPerPage: number, + sortOrder?: string, + status?: string, + monitorId?: string, + resolutionType?: string + ): Promise => { + const matchStage: Record = { + teamId: new mongoose.Types.ObjectId(teamId), + ...(status !== undefined && { status: status }), + ...(monitorId && { monitorId: new mongoose.Types.ObjectId(monitorId) }), + ...(resolutionType && { resolutionType }), + createdAt: { $gte: startDate, $lte: endDate }, + }; + const incidents = await IncidentModel.find(matchStage) + .sort({ createdAt: sortOrder === "asc" ? 1 : -1 }) + .skip(page * rowsPerPage) + .limit(rowsPerPage); + return this.mapDocuments(incidents); }; updateById = async (incidentId: string, teamId: string, patch: Partial) => { diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index a96c7923d..b2ad444d3 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -4,6 +4,15 @@ import { AppError } from "@/utils/AppError.js"; import type { IIncidentsRepository } from "@/repositories/index.js"; import type { Incident } from "@/types/index.js"; +const dateRangeLookup: Record = { + recent: new Date(new Date().setDate(new Date().getDate() - 2)), + hour: new Date(new Date().setHours(new Date().getHours() - 1)), + day: new Date(new Date().setDate(new Date().getDate() - 1)), + week: new Date(new Date().setDate(new Date().getDate() - 7)), + month: new Date(new Date().setMonth(new Date().getMonth() - 1)), + all: undefined, +}; + class IncidentService { static SERVICE_NAME = SERVICE_NAME; @@ -125,18 +134,38 @@ class IncidentService { const { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = query || {}; - const result = await this.db.incidentModule.getIncidentsByTeam({ - teamId, - sortOrder, - dateRange, - page, - rowsPerPage, - status, - monitorId, - resolutionType, - }); + const startDate = dateRangeLookup[dateRange]; - return result; + const endDate = new Date(); + + const parsedPage = Number.isFinite(parseInt(page)) ? parseInt(page) : 0; + const parsedRowsPerPage = Number.isFinite(parseInt(rowsPerPage)) ? parseInt(rowsPerPage) : 20; + const parsedStatus = status === "true" ? "true" : status === "false" ? "false" : undefined; + + const res = await this.incidentsRepository.findByTeamId( + teamId, + startDate, + endDate, + parsedPage, + parsedRowsPerPage, + sortOrder, + parsedStatus, + monitorId, + resolutionType + ); + + // const result = await this.db.incidentModule.getIncidentsByTeam({ + // teamId, + // sortOrder, + // dateRange, + // page, + // rowsPerPage, + // status, + // monitorId, + // resolutionType, + // }); + + return res; } catch (error: any) { this.logger.error({ service: SERVICE_NAME, From 3cf7243c93fd4fc26f1db6f141c640b41a3aaa6d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 18:56:06 +0000 Subject: [PATCH 284/400] summary --- .../Components/IncidentTable/index.jsx | 9 +- .../Incidents2/hooks/useFetchIncidents.js | 3 +- client/src/Pages/Incidents2/index.jsx | 3 +- server/src/controllers/incidentController.ts | 16 +- .../incidents/IIncidentsRepository.ts | 9 +- .../incidents/MongoIncidentRepository.ts | 166 ++++++++++++++++-- .../src/service/business/incidentService.ts | 49 +++--- server/src/types/incident.ts | 29 +++ server/src/utils/{utils.js => utils.ts} | 6 +- 9 files changed, 237 insertions(+), 53 deletions(-) rename server/src/utils/{utils.js => utils.ts} (74%) diff --git a/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx index d613e15c7..670da4a81 100644 --- a/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx @@ -15,6 +15,7 @@ import { useTranslation } from "react-i18next"; import { Button, Typography, useTheme } from "@mui/material"; const IncidentTable = ({ + monitors = [], incidents = [], incidentsCount = 0, isLoading = false, @@ -44,7 +45,11 @@ const IncidentTable = ({ { id: "monitorName", content: t("incidentsTableMonitorName"), - render: (row) => row.monitorName ?? "N/A", + render: (row) => { + console.log(monitors, row); + const monitor = monitors.find((monitor) => monitor.id === row.monitorId); + return monitor ? monitor.name : "N/A"; + }, }, { id: "status", @@ -130,7 +135,7 @@ const IncidentTable = ({ lineHeight: 1.2, }} onClick={() => { - handleResolveIncident(row._id); + handleResolveIncident(row.id); }} > {t("incidentsPage.incidentsTableActionResolveManually")} diff --git a/client/src/Pages/Incidents2/hooks/useFetchIncidents.js b/client/src/Pages/Incidents2/hooks/useFetchIncidents.js index ab22c42e0..54874bae7 100644 --- a/client/src/Pages/Incidents2/hooks/useFetchIncidents.js +++ b/client/src/Pages/Incidents2/hooks/useFetchIncidents.js @@ -45,9 +45,8 @@ const useFetchIncidents = () => { setNetworkError(false); const res = await networkService.getIncidentsByTeam(config); - setIncidents(res.data?.data?.incidents || []); - setIncidentsCount(res.data?.data?.incidentsCount || 0); + setIncidentsCount(res.data?.data?.count || 0); } catch (error) { setNetworkError(true); console.error(t("incidentsPage.errorFetchingIncidents"), error); diff --git a/client/src/Pages/Incidents2/index.jsx b/client/src/Pages/Incidents2/index.jsx index 2f31864bf..4e68279c5 100644 --- a/client/src/Pages/Incidents2/index.jsx +++ b/client/src/Pages/Incidents2/index.jsx @@ -88,7 +88,7 @@ const Incidents2 = () => { useEffect(() => { const lookup = monitors?.reduce((acc, monitor) => { acc[monitor.id] = { - _id: monitor.id, + id: monitor.id, name: monitor.name, type: monitor.type, }; @@ -132,6 +132,7 @@ const Incidents2 = () => { /> { try { - const summary = await this.incidentService.getIncidentSummary({ - teamId: req?.user?.teamId, - query: req?.query, - }); + const teamId = req.user?.teamId; + + if (!teamId) { + throw new AppError({ message: "Team ID is required", service: SERVICE_NAME, status: 400 }); + } + + const limit = req.query.limit ? parseInt(req.query.limit as string, 10) : 10; + + const summary = await this.incidentService.getIncidentSummary(req?.user?.teamId, limit); return res.status(200).json({ success: true, diff --git a/server/src/repositories/incidents/IIncidentsRepository.ts b/server/src/repositories/incidents/IIncidentsRepository.ts index dbdf2b05f..7c2b0179e 100644 --- a/server/src/repositories/incidents/IIncidentsRepository.ts +++ b/server/src/repositories/incidents/IIncidentsRepository.ts @@ -1,4 +1,5 @@ import type { Incident } from "@/types/index.js"; +import type { IncidentSummary } from "@/types/index.js"; export interface IIncidentsRepository { // create create(incident: Partial): Promise; @@ -7,17 +8,19 @@ export interface IIncidentsRepository { findActiveByMonitorId(monitorId: string, teamId: string): Promise; findByTeamId( teamId: string, - startDate: Date, - endDate: Date, + startDate: Date | undefined, page: number, rowsPerPage: number, sortOrder?: string, - status?: string, + status?: boolean, monitorId?: string, resolutionType?: string ): Promise; + findSummaryByTeamId(teamId: string, limit?: number): Promise; + countByTeamId(teamId: string, startDate: Date | undefined, status?: boolean, monitorId?: string, resolutionType?: string): Promise; // update updateById(incidentId: string, teamId: string, updateData: Partial): Promise; // delete + // other } diff --git a/server/src/repositories/incidents/MongoIncidentRepository.ts b/server/src/repositories/incidents/MongoIncidentRepository.ts index ead783e11..10cb47213 100644 --- a/server/src/repositories/incidents/MongoIncidentRepository.ts +++ b/server/src/repositories/incidents/MongoIncidentRepository.ts @@ -1,6 +1,6 @@ import { IncidentModel } from "@/db/models/index.js"; import type { IncidentDocument } from "@/db/models/Incident.js"; -import type { Incident } from "@/types/index.js"; +import type { Incident, IncidentSummary } from "@/types/index.js"; import type { IIncidentsRepository } from "@/repositories/index.js"; import mongoose from "mongoose"; import { AppError } from "@/utils/AppError.js"; @@ -20,6 +20,32 @@ class MongoIncidentRepository implements IIncidentsRepository { return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); }; + private buildMatchStage({ + teamId, + startDate, + status, + monitorId, + resolutionType, + }: { + teamId: string; + startDate: Date | undefined; + status?: boolean; + monitorId?: string; + resolutionType?: string; + }): Record { + const matchStage: Record = { + teamId: new mongoose.Types.ObjectId(teamId), + ...(status !== undefined && { status }), + ...(monitorId && { monitorId: new mongoose.Types.ObjectId(monitorId) }), + ...(resolutionType && { resolutionType }), + }; + + if (startDate) { + matchStage.createdAt = { $gte: startDate }; + } + return matchStage; + } + protected toEntity = (doc: IncidentDocument): Incident => { return { id: this.toStringId(doc._id), @@ -79,22 +105,15 @@ class MongoIncidentRepository implements IIncidentsRepository { findByTeamId = async ( teamId: string, - startDate: Date, - endDate: Date, + startDate: Date | undefined, page: number, rowsPerPage: number, sortOrder?: string, - status?: string, + status?: boolean, monitorId?: string, resolutionType?: string ): Promise => { - const matchStage: Record = { - teamId: new mongoose.Types.ObjectId(teamId), - ...(status !== undefined && { status: status }), - ...(monitorId && { monitorId: new mongoose.Types.ObjectId(monitorId) }), - ...(resolutionType && { resolutionType }), - createdAt: { $gte: startDate, $lte: endDate }, - }; + const matchStage = this.buildMatchStage({ teamId, startDate, status, monitorId, resolutionType }); const incidents = await IncidentModel.find(matchStage) .sort({ createdAt: sortOrder === "asc" ? 1 : -1 }) .skip(page * rowsPerPage) @@ -117,5 +136,130 @@ class MongoIncidentRepository implements IIncidentsRepository { } return this.toEntity(updatedIncident); }; + + countByTeamId = async ( + teamId: string, + startDate: Date | undefined, + status?: boolean, + monitorId?: string, + resolutionType?: string + ): Promise => { + const matchStage = this.buildMatchStage({ teamId, startDate, status, monitorId, resolutionType }); + return IncidentModel.countDocuments(matchStage); + }; + + findSummaryByTeamId = async (teamId: string, limit: number): Promise => { + const matchStage = { teamId: new mongoose.Types.ObjectId(teamId) }; + + const counts = await IncidentModel.aggregate([ + { $match: matchStage }, + { + $group: { + _id: "$status", + count: { $sum: 1 }, + manualResolutions: { + $sum: { $cond: [{ $eq: ["$resolutionType", "manual"] }, 1, 0] }, + }, + automaticResolutions: { + $sum: { $cond: [{ $eq: ["$resolutionType", "automatic"] }, 1, 0] }, + }, + }, + }, + ]); + + let total = 0; + let active = 0; + let manual = 0; + let automatic = 0; + + counts.forEach((item) => { + total += item.count; + if (item._id === true) { + active = item.count; + } + manual += item.manualResolutions; + automatic += item.automaticResolutions; + }); + + const resolutionTimeResult = await IncidentModel.aggregate([ + { $match: { ...matchStage, status: false, endTime: { $exists: true, $ne: null } } }, + { $project: { resolutionTime: { $subtract: ["$endTime", "$startTime"] } } }, + { $group: { _id: null, avgResolutionTime: { $avg: "$resolutionTime" } } }, + ]); + const avgResolutionTimeMs = resolutionTimeResult[0]?.avgResolutionTime || 0; + const avgResolutionTimeHours = Math.round((avgResolutionTimeMs / (1000 * 60 * 60) || 0) * 100) / 100; + + const monitorResult = await IncidentModel.aggregate([ + { $match: matchStage }, + { $group: { _id: "$monitorId", count: { $sum: 1 } } }, + { $sort: { count: -1 } }, + { $limit: 1 }, + { + $lookup: { + from: "monitors", + localField: "_id", + foreignField: "_id", + as: "monitor", + }, + }, + { $project: { monitorId: "$_id", count: 1, monitorName: { $arrayElemAt: ["$monitor.name", 0] } } }, + ]); + + const latestLimit = Math.max(1, Number.isFinite(Number(limit)) ? Number(limit) : 10); + const latestIncidents = await IncidentModel.aggregate([ + { $match: matchStage }, + { $sort: { createdAt: -1 } }, + { $limit: latestLimit }, + { + $lookup: { + from: "monitors", + localField: "monitorId", + foreignField: "_id", + as: "monitor", + }, + }, + { + $project: { + _id: 1, + monitorId: 1, + monitorName: { $arrayElemAt: ["$monitor.name", 0] }, + status: 1, + startTime: 1, + endTime: 1, + resolutionType: 1, + message: 1, + statusCode: 1, + createdAt: 1, + }, + }, + ]); + + return { + total, + totalActive: active, + totalManualResolutions: manual, + totalAutomaticResolutions: automatic, + avgResolutionTimeHours, + topMonitor: monitorResult[0] + ? { + monitorId: this.toStringId(monitorResult[0].monitorId), + monitorName: monitorResult[0].monitorName ?? null, + incidentCount: monitorResult[0].count, + } + : null, + latestIncidents: latestIncidents.map((incident) => ({ + id: this.toStringId(incident._id), + monitorId: this.toStringId(incident.monitorId), + monitorName: incident.monitorName ?? null, + status: incident.status, + startTime: this.toDateString(incident.startTime), + endTime: incident.endTime ? this.toDateString(incident.endTime) : null, + resolutionType: incident.resolutionType ?? null, + message: incident.message ?? null, + statusCode: incident.statusCode ?? null, + createdAt: this.toDateString(incident.createdAt), + })), + }; + }; } export default MongoIncidentRepository; diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index b2ad444d3..bc69eeed0 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -1,6 +1,7 @@ const SERVICE_NAME = "incidentService"; import type { Monitor } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; +import { ParseBoolean } from "@/utils/utils.js"; import type { IIncidentsRepository } from "@/repositories/index.js"; import type { Incident } from "@/types/index.js"; @@ -136,16 +137,13 @@ class IncidentService { const startDate = dateRangeLookup[dateRange]; - const endDate = new Date(); - const parsedPage = Number.isFinite(parseInt(page)) ? parseInt(page) : 0; const parsedRowsPerPage = Number.isFinite(parseInt(rowsPerPage)) ? parseInt(rowsPerPage) : 20; - const parsedStatus = status === "true" ? "true" : status === "false" ? "false" : undefined; + const parsedStatus = typeof status === "undefined" ? undefined : ParseBoolean(status); - const res = await this.incidentsRepository.findByTeamId( + const incidents = await this.incidentsRepository.findByTeamId( teamId, startDate, - endDate, parsedPage, parsedRowsPerPage, sortOrder, @@ -154,18 +152,9 @@ class IncidentService { resolutionType ); - // const result = await this.db.incidentModule.getIncidentsByTeam({ - // teamId, - // sortOrder, - // dateRange, - // page, - // rowsPerPage, - // status, - // monitorId, - // resolutionType, - // }); + const count = await this.incidentsRepository.countByTeamId(teamId, startDate, parsedStatus, monitorId, resolutionType); - return res; + return { incidents, count }; } catch (error: any) { this.logger.error({ service: SERVICE_NAME, @@ -178,18 +167,14 @@ class IncidentService { } }; - getIncidentSummary = async ({ teamId, query }: { teamId: string; query?: any }) => { + getIncidentSummary = async (teamId: string, limit?: string) => { try { if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); + throw this.errorService.createBadRequestError(" team ID in request"); } - const { limit } = query || {}; - - const summary = await this.db.incidentModule.getIncidentSummary({ - teamId, - limit, - }); + const parsedLimit = limit && Number.isFinite(parseInt(limit, 10)) ? parseInt(limit, 10) : 10; + const summary = await this.incidentsRepository.findSummaryByTeamId(teamId, parsedLimit); return summary; } catch (error: any) { @@ -256,7 +241,7 @@ class IncidentService { for (const item of resolveItems as any[]) { try { - await this.resolveIncident(item.monitor); + await this.resolveIncidentForMonitor(item.monitor); } catch (error: any) { this.logger.error({ service: SERVICE_NAME, @@ -378,6 +363,20 @@ class IncidentService { throw error; } }; + + private resolveIncidentForMonitor = async (monitor: Monitor) => { + if (!monitor?.id) { + return; + } + const incident = await this.incidentsRepository.findActiveByMonitorId(monitor.id, monitor.teamId); + if (!incident) { + return; + } + incident.status = false; + incident.endTime = new Date().toISOString(); + incident.resolutionType = "automatic"; + await this.incidentsRepository.updateById(incident.id, incident.teamId, incident); + }; } export default IncidentService; diff --git a/server/src/types/incident.ts b/server/src/types/incident.ts index 65186f08c..39dfb3d0d 100644 --- a/server/src/types/incident.ts +++ b/server/src/types/incident.ts @@ -18,3 +18,32 @@ export interface Incident { createdAt: string; updatedAt: string; } + +export interface IncidentSummaryTopMonitor { + monitorId: string; + monitorName: string | null; + incidentCount: number; +} + +export interface IncidentSummaryItem { + id: string; + monitorId: string; + monitorName: string | null; + status: boolean; + startTime: string; + endTime: string | null; + resolutionType: IncidentResolutionType; + message: string | null; + statusCode: number | null; + createdAt: string; +} + +export interface IncidentSummary { + total: number; + totalActive: number; + totalManualResolutions: number; + totalAutomaticResolutions: number; + avgResolutionTimeHours: number; + topMonitor: IncidentSummaryTopMonitor | null; + latestIncidents: IncidentSummaryItem[]; +} diff --git a/server/src/utils/utils.js b/server/src/utils/utils.ts similarity index 74% rename from server/src/utils/utils.js rename to server/src/utils/utils.ts index ee4ee1c6d..a82d8734f 100755 --- a/server/src/utils/utils.js +++ b/server/src/utils/utils.ts @@ -1,4 +1,4 @@ -const ParseBoolean = (value) => { +export const ParseBoolean = (value: boolean | string | null | undefined) => { if (value === true || value === "true") { return true; } else if (value === false || value === "false" || value === null || value === undefined) { @@ -6,7 +6,7 @@ const ParseBoolean = (value) => { } }; -const getTokenFromHeaders = (headers) => { +export const getTokenFromHeaders = (headers: Record) => { const authorizationHeader = headers.authorization; if (!authorizationHeader) throw new Error("No auth headers"); @@ -15,5 +15,3 @@ const getTokenFromHeaders = (headers) => { return parts[1]; }; - -export { ParseBoolean, getTokenFromHeaders }; From 7afdcb8ebcf058ba8232c7ba03251b4fe295b640 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 19:01:50 +0000 Subject: [PATCH 285/400] fix query param --- server/src/controllers/incidentController.ts | 2 +- server/src/service/business/incidentService.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/server/src/controllers/incidentController.ts b/server/src/controllers/incidentController.ts index a94474c5a..93783b20c 100644 --- a/server/src/controllers/incidentController.ts +++ b/server/src/controllers/incidentController.ts @@ -19,7 +19,7 @@ class IncidentController { try { const result = await this.incidentService.getIncidentsByTeam({ teamId: req?.user?.teamId, - query: req?.query?.limit, + query: req?.query, }); return res.status(200).json({ diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index bc69eeed0..513f16b1e 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -134,7 +134,6 @@ class IncidentService { } const { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = query || {}; - const startDate = dateRangeLookup[dateRange]; const parsedPage = Number.isFinite(parseInt(page)) ? parseInt(page) : 0; From 69d050ff6115a007c074e8fec36953ef2260fef3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 19:04:30 +0000 Subject: [PATCH 286/400] remove incidents from buffer --- .../src/service/business/incidentService.ts | 156 ------------------ .../service/infrastructure/bufferService.ts | 61 +------ 2 files changed, 1 insertion(+), 216 deletions(-) diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index 513f16b1e..82211bc4e 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -220,162 +220,6 @@ class IncidentService { throw error; } }; - - processIncidentsFromBuffer = async (incidentBufferItems: any[]) => { - try { - if (!incidentBufferItems || incidentBufferItems.length === 0) { - return; - } - - const createItems = []; - const resolveItems = []; - - for (const item of incidentBufferItems) { - if (item.action === "resolve") { - resolveItems.push(item); - } else { - createItems.push(item); - } - } - - for (const item of resolveItems as any[]) { - try { - await this.resolveIncidentForMonitor(item.monitor); - } catch (error: any) { - this.logger.error({ - service: SERVICE_NAME, - method: "processIncidentsFromBuffer", - message: `Failed to resolve incident from buffer: ${error.message}`, - monitorId: item.monitor?._id, - stack: error.stack, - }); - } - } - - if (createItems.length === 0) { - return; - } - - const groupedByMonitor: Record = {}; - for (const item of createItems) { - if (!item.monitor || !item.monitor.id || !item.check || !item.check.id) { - this.logger.warn({ - service: SERVICE_NAME, - method: "processIncidentsFromBuffer", - message: "Skipping item with missing monitor or check data", - item, - }); - continue; - } - - const monitorId = item.monitor.id.toString(); - if (!groupedByMonitor[monitorId]) { - groupedByMonitor[monitorId] = []; - } - groupedByMonitor[monitorId].push(item); - } - - const monitorIds = Object.keys(groupedByMonitor); - if (monitorIds.length === 0) { - return; - } - - const activeIncidents = await this.db.incidentModule.getActiveIncidentsByMonitors(monitorIds); - - const incidentsCreatedInFlush: Record = {}; - const checksToAddToIncidents = []; - const newIncidentsToCreate = []; - - for (const [monitorId, items] of Object.entries(groupedByMonitor)) { - const existingIncident = activeIncidents.get(monitorId) || incidentsCreatedInFlush[monitorId]; - - if (existingIncident) { - const incidentId = existingIncident._id ? existingIncident._id.toString() : existingIncident; - for (const item of items) { - checksToAddToIncidents.push({ - incidentId, - checkId: item.check.id.toString(), - }); - } - } else { - const firstItem = items[0]; - const incidentData = { - monitorId: firstItem.monitor.id, - teamId: firstItem.monitor.teamId, - type: firstItem.monitor.type, - startTime: new Date(), - status: true, - message: firstItem.check.message || null, - statusCode: firstItem.check.statusCode || null, - checks: [firstItem.check.id], - }; - - newIncidentsToCreate.push({ - incidentData, - monitorId, - remainingChecks: items.slice(1), // Checks restantes para agregar después - }); - } - } - - if (newIncidentsToCreate.length > 0) { - const incidentDataArray = newIncidentsToCreate.map((item) => item.incidentData); - await this.db.incidentModule.createIncidents(incidentDataArray); - - const createdIncidentsMap = await this.db.incidentModule.getActiveIncidentsByMonitors(newIncidentsToCreate.map((item) => item.monitorId)); - - for (const item of newIncidentsToCreate) { - const createdIncident = createdIncidentsMap.get(item.monitorId); - if (createdIncident && createdIncident._id) { - const incidentId = createdIncident._id.toString(); - incidentsCreatedInFlush[item.monitorId] = incidentId; - - for (const remainingItem of item.remainingChecks) { - checksToAddToIncidents.push({ - incidentId, - checkId: remainingItem.check.id, - }); - } - } - } - } - - if (checksToAddToIncidents.length > 0) { - await this.db.incidentModule.addChecksToIncidentsBatch(checksToAddToIncidents); - } - - this.logger.info({ - service: SERVICE_NAME, - method: "processIncidentsFromBuffer", - message: `Processed ${incidentBufferItems.length} incident buffer items`, - created: newIncidentsToCreate.length, - checksAdded: checksToAddToIncidents.length, - resolved: resolveItems.length, - }); - } catch (error: any) { - this.logger.error({ - service: SERVICE_NAME, - method: "processIncidentsFromBuffer", - message: error.message, - stack: error.stack, - }); - throw error; - } - }; - - private resolveIncidentForMonitor = async (monitor: Monitor) => { - if (!monitor?.id) { - return; - } - const incident = await this.incidentsRepository.findActiveByMonitorId(monitor.id, monitor.teamId); - if (!incident) { - return; - } - incident.status = false; - incident.endTime = new Date().toISOString(); - incident.resolutionType = "automatic"; - await this.incidentsRepository.updateById(incident.id, incident.teamId, incident); - }; } export default IncidentService; diff --git a/server/src/service/infrastructure/bufferService.ts b/server/src/service/infrastructure/bufferService.ts index 0363aa362..89cd6a55f 100755 --- a/server/src/service/infrastructure/bufferService.ts +++ b/server/src/service/infrastructure/bufferService.ts @@ -7,21 +7,17 @@ class BufferService { static SERVICE_NAME = SERVICE_NAME; private BUFFER_TIMEOUT: number; private logger: any; - private incidentService: any; private SERVICE_NAME: string; private buffer: any[]; - private incidentBuffer: any[]; private bufferTimer: NodeJS.Timeout | null = null; private checksService: any; - constructor({ logger, incidentService, checkService }: { logger: any; incidentService: any; checkService: any }) { + constructor({ logger, checkService }: { logger: any; checkService: any }) { this.BUFFER_TIMEOUT = config.NODE_ENV === "development" ? 10 : 1000 * 60 * 1; // 1 minute this.logger = logger; - this.incidentService = incidentService; this.checksService = checkService; this.SERVICE_NAME = SERVICE_NAME; this.buffer = []; - this.incidentBuffer = []; this.scheduleNextFlush(); this.logger.info({ message: `Buffer service initialized, flushing every ${this.BUFFER_TIMEOUT / 1000}s`, @@ -47,28 +43,6 @@ class BufferService { } } - addIncidentToBuffer({ monitor, check, action = "create" }: { monitor: any; check: Check; action?: string }) { - try { - if (!monitor || !check) { - this.logger.warn({ - message: "Skipping incident buffer item: missing monitor or check", - service: this.SERVICE_NAME, - method: "addIncidentToBuffer", - }); - return; - } - - this.incidentBuffer.push({ monitor, check, action }); - } catch (error: any) { - this.logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "addIncidentToBuffer", - stack: error.stack, - }); - } - } - removeCheckFromBuffer(checkToRemove: Check) { try { if (!checkToRemove) { @@ -141,40 +115,7 @@ class BufferService { }); } - try { - if (this.incidentBuffer.length > 0 && this.incidentService) { - await this.flushIncidentBuffer(); - } - } catch (error: any) { - this.logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "flushBuffer", - stack: error.stack, - }); - } - this.buffer = []; - this.incidentBuffer = []; - } - - async flushIncidentBuffer() { - if (!this.incidentService || this.incidentBuffer.length === 0) { - return; - } - - try { - const itemsToProcess = [...this.incidentBuffer]; - await this.incidentService.processIncidentsFromBuffer(itemsToProcess); - } catch (error: any) { - this.logger.error({ - message: `Error flushing incident buffer: ${error.message}`, - service: this.SERVICE_NAME, - method: "flushIncidentBuffer", - stack: error.stack, - }); - throw error; - } } } From 427e5a18eab629713ba93ea4b1187bc5a0976845 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 19:12:05 +0000 Subject: [PATCH 287/400] incidents -> checks --- .../Checks/Components/IncidentTable/index.jsx | 264 ++++++++++++++++ .../Components/OptionsHeader/index.jsx | 15 +- .../Components/OptionsHeader/skeleton.jsx | 0 .../Components/StatusBoxes/StatusBox.jsx | 0 .../Components/StatusBoxes/index.jsx | 0 .../Components/StatusBoxes/skeleton.jsx | 0 client/src/Pages/Checks/index.jsx | 120 +++++++ .../Components/ActiveIncidentsPanel/index.jsx | 0 .../Components/IncidentTable/index.jsx | 293 +++++++----------- .../IncidentsSummaryPanel/index.jsx | 0 .../IncidentsSummaryPanel/skeleton.jsx | 0 .../LatestIncidentsPanel/IncidentItem.jsx | 0 .../Components/LatestIncidentsPanel/index.jsx | 0 .../Components/OptionsHeader/index.jsx | 15 +- .../Components/StatisticsPanel/index.jsx | 0 .../Components/SummaryCard/index.jsx | 0 .../hooks/useFetchIncidents.js | 0 client/src/Pages/Incidents/index.jsx | 153 +++++---- .../Components/IncidentTable/index.jsx | 209 ------------- client/src/Pages/Incidents2/index.jsx | 151 --------- client/src/Routes/index.jsx | 8 +- 21 files changed, 615 insertions(+), 613 deletions(-) create mode 100644 client/src/Pages/Checks/Components/IncidentTable/index.jsx rename client/src/Pages/{Incidents2 => Checks}/Components/OptionsHeader/index.jsx (87%) rename client/src/Pages/{Incidents => Checks}/Components/OptionsHeader/skeleton.jsx (100%) rename client/src/Pages/{Incidents => Checks}/Components/StatusBoxes/StatusBox.jsx (100%) rename client/src/Pages/{Incidents => Checks}/Components/StatusBoxes/index.jsx (100%) rename client/src/Pages/{Incidents => Checks}/Components/StatusBoxes/skeleton.jsx (100%) create mode 100644 client/src/Pages/Checks/index.jsx rename client/src/Pages/{Incidents2 => Incidents}/Components/ActiveIncidentsPanel/index.jsx (100%) rename client/src/Pages/{Incidents2 => Incidents}/Components/IncidentsSummaryPanel/index.jsx (100%) rename client/src/Pages/{Incidents2 => Incidents}/Components/IncidentsSummaryPanel/skeleton.jsx (100%) rename client/src/Pages/{Incidents2 => Incidents}/Components/LatestIncidentsPanel/IncidentItem.jsx (100%) rename client/src/Pages/{Incidents2 => Incidents}/Components/LatestIncidentsPanel/index.jsx (100%) rename client/src/Pages/{Incidents2 => Incidents}/Components/StatisticsPanel/index.jsx (100%) rename client/src/Pages/{Incidents2 => Incidents}/Components/SummaryCard/index.jsx (100%) rename client/src/Pages/{Incidents2 => Incidents}/hooks/useFetchIncidents.js (100%) delete mode 100644 client/src/Pages/Incidents2/Components/IncidentTable/index.jsx delete mode 100644 client/src/Pages/Incidents2/index.jsx diff --git a/client/src/Pages/Checks/Components/IncidentTable/index.jsx b/client/src/Pages/Checks/Components/IncidentTable/index.jsx new file mode 100644 index 000000000..e066049e6 --- /dev/null +++ b/client/src/Pages/Checks/Components/IncidentTable/index.jsx @@ -0,0 +1,264 @@ +//Components +import Stack from "@mui/material/Stack"; +import DataTable from "@/Components/v1/Table/index.jsx"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableRow from "@mui/material/TableRow"; +import TableCell from "@mui/material/TableCell"; +import TableSkeleton from "@/Components/v1/Table/skeleton.jsx"; +import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; +import { StatusLabel } from "@/Components/v1/Label/index.jsx"; +import { HttpStatusLabel } from "@/Components/v1/HttpStatusLabel/index.jsx"; +import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; +import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; + +//Utils +import { formatDateWithTz } from "@/Utils/timeUtils.js"; +import { useSelector } from "react-redux"; +import { useState } from "react"; +import PropTypes from "prop-types"; +import { useTranslation } from "react-i18next"; +import { + useFetchChecksTeam, + useFetchChecksByMonitor, + useResolveIncident, +} from "@/Hooks/checkHooks.js"; +import { Button, Typography, useTheme } from "@mui/material"; +import { lighten } from "@mui/material/styles"; + +const GetTooltip = (row) => { + const theme = useTheme(); + const phases = row?.timings?.phases; + + const phaseKeyFormattingMap = { + firstByte: "first byte", + }; + return ( + + {`Status code: ${row?.statusCode}`} + {`Response time: ${row?.responseTime} ms`} + {phases && ( + <> + {`Request timing: `} + + + {Object.keys(phases)?.map((phaseKey) => ( + + + + {`${phaseKeyFormattingMap[phaseKey] || phaseKey}:`} + + + + {`${phases[phaseKey]} ms`} + + + ))} + +
+ + )} +
+ ); +}; + +const IncidentTable = ({ + isLoading, + monitors, + selectedMonitor, + filter, + dateRange, + updateTrigger, + setUpdateTrigger, +}) => { + //Redux state + const uiTimezone = useSelector((state) => state.ui.timezone); + + //Local state + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + const selectedMonitorDetails = monitors?.[selectedMonitor]; + const selectedMonitorType = selectedMonitorDetails?.type; + + //Hooks + const [resolveIncident, resolveLoading] = useResolveIncident(); + + const [checksMonitor, checksCountMonitor, isLoadingMonitor, networkErrorMonitor] = + useFetchChecksByMonitor({ + monitorId: selectedMonitor === "0" ? undefined : selectedMonitor, + type: selectedMonitorType, + status: false, + sortOrder: "desc", + limit: null, + dateRange, + filter: filter === "resolved" ? "all" : filter, + ack: filter === "resolved" ? true : false, + page: page, + rowsPerPage: rowsPerPage, + enabled: selectedMonitor !== "0", + updateTrigger, + }); + + const [checksTeam, checksCountTeam, isLoadingTeam, networkErrorTeam] = + useFetchChecksTeam({ + status: false, + sortOrder: "desc", + limit: null, + dateRange, + filter: filter === "resolved" ? "all" : filter, + ack: filter === "resolved" ? true : false, + page: page, + rowsPerPage: rowsPerPage, + enabled: selectedMonitor === "0", + updateTrigger, + }); + + const checks = selectedMonitor === "0" ? checksTeam : checksMonitor; + const checksCount = selectedMonitor === "0" ? checksCountTeam : checksCountMonitor; + isLoading = isLoadingTeam || isLoadingMonitor; + const networkError = selectedMonitor === "0" ? networkErrorTeam : networkErrorMonitor; + + const { t } = useTranslation(); + + //Handlers + const handleChangePage = (_, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(event.target.value); + }; + + const handleResolveIncident = (checkId) => { + resolveIncident(checkId, setUpdateTrigger); + }; + + const headers = [ + { + id: "monitorName", + content: t("incidentsTableMonitorName"), + render: (row) => monitors[row.monitorId]?.name ?? "N/A", + }, + { + id: "status", + content: t("incidentsTableStatus"), + render: (row) => { + const status = row.status === true ? "up" : "down"; + return ( + + ); + }, + }, + { + id: "dateTime", + content: t("incidentsTableDateTime"), + render: (row) => { + const formattedDate = formatDateWithTz( + row.createdAt, + "YYYY-MM-DD HH:mm:ss A", + uiTimezone + ); + return formattedDate; + }, + }, + { + id: "statusCode", + content: t("incidentsTableStatusCode"), + render: (row) => , + }, + { id: "message", content: t("incidentsTableMessage"), render: (row) => row.message }, + { + id: "action", + content: t("actions"), + render: (row) => { + return row.ack === false ? ( + + ) : ( + + {t("incidentsTableResolvedAt")}{" "} + {formatDateWithTz(row.ackAt, "YYYY-MM-DD HH:mm:ss A", uiTimezone)} + + ); + }, + }, + ]; + + if (isLoading || resolveLoading) return ; + + if (networkError) { + return ( + + + + ); + } + + if (!isLoading && typeof checksCount === "undefined") { + return {t("incidentsTableNoIncidents")}; + } + + return ( + <> + + + + ); +}; + +IncidentTable.propTypes = { + isLoading: PropTypes.bool, + monitors: PropTypes.object, + selectedMonitor: PropTypes.string, + filter: PropTypes.string, + dateRange: PropTypes.string, + updateTrigger: PropTypes.bool, + setUpdateTrigger: PropTypes.func, +}; +export default IncidentTable; diff --git a/client/src/Pages/Incidents2/Components/OptionsHeader/index.jsx b/client/src/Pages/Checks/Components/OptionsHeader/index.jsx similarity index 87% rename from client/src/Pages/Incidents2/Components/OptionsHeader/index.jsx rename to client/src/Pages/Checks/Components/OptionsHeader/index.jsx index 3a0ca1985..c16f54f18 100644 --- a/client/src/Pages/Incidents2/Components/OptionsHeader/index.jsx +++ b/client/src/Pages/Checks/Components/OptionsHeader/index.jsx @@ -5,6 +5,7 @@ import PropTypes from "prop-types"; //Utils import { useTheme } from "@emotion/react"; +import SkeletonLayout from "./skeleton.jsx"; import { useTranslation } from "react-i18next"; const OptionsHeader = ({ @@ -14,27 +15,27 @@ const OptionsHeader = ({ monitors, filter = "all", setFilter, - dateRange = "all", + dateRange = "hour", setDateRange, }) => { const theme = useTheme(); const { t } = useTranslation(); const monitorNames = typeof monitors !== "undefined" ? Object.values(monitors) : []; - const filterOptions = [ - { _id: "all", name: t("incidentsPage.incidentsOptionsHeaderFilterAll") }, - { _id: "active", name: t("incidentsPage.incidentsOptionsHeaderFilterActive") }, - { _id: "resolved", name: t("incidentsPage.incidentsOptionsHeaderFilterResolved") }, - { _id: "manual", name: t("incidentsPage.incidentsOptionsHeaderFilterManual") }, + { _id: "all", name: t("incidentsOptionsHeaderFilterAll") }, + { _id: "down", name: t("incidentsOptionsHeaderFilterDown") }, + { _id: "resolve", name: t("incidentsOptionsHeaderFilterCannotResolve") }, + { _id: "resolved", name: t("incidentsOptionsHeaderFilterResolved") }, ]; + // The stacks below which are three in number have the same style so const stackStyles = { direction: "row", alignItems: "center", gap: theme.spacing(6), }; - if (!shouldRender) return; + if (!shouldRender) return ; return ( { + // Redux state + const { t } = useTranslation(); + + const BREADCRUMBS = [{ name: t("checksPageTitle"), path: "/checks" }]; + + // Local state + const [selectedMonitor, setSelectedMonitor] = useState("0"); + const [filter, setFilter] = useState(undefined); + const [dateRange, setDateRange] = useState(undefined); + const [monitorLookup, setMonitorLookup] = useState(undefined); + const [updateTrigger, setUpdateTrigger] = useState(false); + + //Hooks + const { acknowledge, isLoadingAcknowledge } = useAcknowledgeChecks(); + + //Utils + const theme = useTheme(); + const [monitors, , isLoading, networkError] = useFetchMonitorsByTeamId({}); + const [summary, isLoadingSummary, networkErrorSummary] = useFetchChecksSummaryByTeamId({ + updateTrigger, + }); + const { monitorId } = useParams(); + + useEffect(() => { + if (monitorId) { + setSelectedMonitor(monitorId); + } + }, [monitorId]); + + useEffect(() => { + const monitorLookup = monitors?.reduce((acc, monitor) => { + acc[monitor._id] = { + _id: monitor._id, + name: monitor.name, + type: monitor.type, + }; + return acc; + }, {}); + setMonitorLookup(monitorLookup); + }, [monitors]); + + const handleAcknowledge = () => { + const monitorId = selectedMonitor === "0" ? null : selectedMonitor; + acknowledge(setUpdateTrigger, monitorId); + }; + + if (networkError || networkErrorSummary) { + return ( + + + + ); + } + + return ( + + + + + + + + + + ); +}; + +export default Checks; diff --git a/client/src/Pages/Incidents2/Components/ActiveIncidentsPanel/index.jsx b/client/src/Pages/Incidents/Components/ActiveIncidentsPanel/index.jsx similarity index 100% rename from client/src/Pages/Incidents2/Components/ActiveIncidentsPanel/index.jsx rename to client/src/Pages/Incidents/Components/ActiveIncidentsPanel/index.jsx diff --git a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx index e066049e6..670da4a81 100644 --- a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx @@ -1,10 +1,5 @@ //Components -import Stack from "@mui/material/Stack"; import DataTable from "@/Components/v1/Table/index.jsx"; -import Table from "@mui/material/Table"; -import TableBody from "@mui/material/TableBody"; -import TableRow from "@mui/material/TableRow"; -import TableCell from "@mui/material/TableCell"; import TableSkeleton from "@/Components/v1/Table/skeleton.jsx"; import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; import { StatusLabel } from "@/Components/v1/Label/index.jsx"; @@ -13,213 +8,154 @@ import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; //Utils -import { formatDateWithTz } from "@/Utils/timeUtils.js"; +import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; import { useSelector } from "react-redux"; -import { useState } from "react"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; -import { - useFetchChecksTeam, - useFetchChecksByMonitor, - useResolveIncident, -} from "@/Hooks/checkHooks.js"; import { Button, Typography, useTheme } from "@mui/material"; -import { lighten } from "@mui/material/styles"; - -const GetTooltip = (row) => { - const theme = useTheme(); - const phases = row?.timings?.phases; - - const phaseKeyFormattingMap = { - firstByte: "first byte", - }; - return ( - - {`Status code: ${row?.statusCode}`} - {`Response time: ${row?.responseTime} ms`} - {phases && ( - <> - {`Request timing: `} - - - {Object.keys(phases)?.map((phaseKey) => ( - - - - {`${phaseKeyFormattingMap[phaseKey] || phaseKey}:`} - - - - {`${phases[phaseKey]} ms`} - - - ))} - -
- - )} -
- ); -}; const IncidentTable = ({ - isLoading, - monitors, - selectedMonitor, - filter, - dateRange, - updateTrigger, - setUpdateTrigger, + monitors = [], + incidents = [], + incidentsCount = 0, + isLoading = false, + networkError = false, + page = 0, + rowsPerPage = 10, + handleChangePage, + handleChangeRowsPerPage, + resolveIncident, + handleUpdateTrigger, }) => { - //Redux state const uiTimezone = useSelector((state) => state.ui.timezone); - //Local state - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(10); - const selectedMonitorDetails = monitors?.[selectedMonitor]; - const selectedMonitorType = selectedMonitorDetails?.type; - - //Hooks - const [resolveIncident, resolveLoading] = useResolveIncident(); - - const [checksMonitor, checksCountMonitor, isLoadingMonitor, networkErrorMonitor] = - useFetchChecksByMonitor({ - monitorId: selectedMonitor === "0" ? undefined : selectedMonitor, - type: selectedMonitorType, - status: false, - sortOrder: "desc", - limit: null, - dateRange, - filter: filter === "resolved" ? "all" : filter, - ack: filter === "resolved" ? true : false, - page: page, - rowsPerPage: rowsPerPage, - enabled: selectedMonitor !== "0", - updateTrigger, - }); - - const [checksTeam, checksCountTeam, isLoadingTeam, networkErrorTeam] = - useFetchChecksTeam({ - status: false, - sortOrder: "desc", - limit: null, - dateRange, - filter: filter === "resolved" ? "all" : filter, - ack: filter === "resolved" ? true : false, - page: page, - rowsPerPage: rowsPerPage, - enabled: selectedMonitor === "0", - updateTrigger, - }); - - const checks = selectedMonitor === "0" ? checksTeam : checksMonitor; - const checksCount = selectedMonitor === "0" ? checksCountTeam : checksCountMonitor; - isLoading = isLoadingTeam || isLoadingMonitor; - const networkError = selectedMonitor === "0" ? networkErrorTeam : networkErrorMonitor; - const { t } = useTranslation(); + const theme = useTheme(); - //Handlers - const handleChangePage = (_, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - setRowsPerPage(event.target.value); - }; - - const handleResolveIncident = (checkId) => { - resolveIncident(checkId, setUpdateTrigger); + const handleResolveIncident = async (incidentId) => { + try { + await resolveIncident(incidentId); + handleUpdateTrigger(); + } catch (error) { + console.error(t("incidentsPage.errorResolvingIncident"), error); + } }; const headers = [ { id: "monitorName", content: t("incidentsTableMonitorName"), - render: (row) => monitors[row.monitorId]?.name ?? "N/A", + render: (row) => { + console.log(monitors, row); + const monitor = monitors.find((monitor) => monitor.id === row.monitorId); + return monitor ? monitor.name : "N/A"; + }, }, { id: "status", content: t("incidentsTableStatus"), render: (row) => { - const status = row.status === true ? "up" : "down"; + const status = row.status === true ? "down" : "up"; + const statusText = + row.status === true ? t("incidentsPage.active") : t("incidentsPage.resolved"); return ( ); }, }, { - id: "dateTime", - content: t("incidentsTableDateTime"), + id: "startTime", + content: t("incidentsPage.startTime"), render: (row) => { const formattedDate = formatDateWithTz( - row.createdAt, + row.startTime || row.createdAt, "YYYY-MM-DD HH:mm:ss A", uiTimezone ); return formattedDate; }, }, + { + id: "endTime", + content: t("incidentsPage.endTime"), + render: (row) => { + if (row.endTime) { + return formatDateWithTz(row.endTime, "YYYY-MM-DD HH:mm:ss A", uiTimezone); + } + return "-"; + }, + }, + { + id: "resolutionType", + content: t("incidentsPage.resolutionType"), + render: (row) => { + if (row.resolutionType) { + return ( + + {row.resolutionType} + + ); + } + return "-"; + }, + }, { id: "statusCode", content: t("incidentsTableStatusCode"), render: (row) => , }, - { id: "message", content: t("incidentsTableMessage"), render: (row) => row.message }, + { + id: "message", + content: t("incidentsTableMessage"), + render: (row) => row.message || "-", + }, { id: "action", content: t("actions"), render: (row) => { - return row.ack === false ? ( - - ) : ( - - {t("incidentsTableResolvedAt")}{" "} - {formatDateWithTz(row.ackAt, "YYYY-MM-DD HH:mm:ss A", uiTimezone)} - - ); + if (row.status === true) { + return ( + + ); + } else { + return ( + + {t("incidentsPage.incidentsTableResolved")} + + ); + } }, }, ]; - if (isLoading || resolveLoading) return ; + if (isLoading) return ; if (networkError) { return ( @@ -229,20 +165,25 @@ const IncidentTable = ({ ); } - if (!isLoading && typeof checksCount === "undefined") { - return {t("incidentsTableNoIncidents")}; + if (!isLoading && !networkError && incidents?.length === 0) { + return ( + + {t("incidentsTableNoIncidents", "No incidents found")} + + ); } + const incidentsData = Array.isArray(incidents) ? incidents : []; + return ( <> { const theme = useTheme(); const { t } = useTranslation(); const monitorNames = typeof monitors !== "undefined" ? Object.values(monitors) : []; + const filterOptions = [ - { _id: "all", name: t("incidentsOptionsHeaderFilterAll") }, - { _id: "down", name: t("incidentsOptionsHeaderFilterDown") }, - { _id: "resolve", name: t("incidentsOptionsHeaderFilterCannotResolve") }, - { _id: "resolved", name: t("incidentsOptionsHeaderFilterResolved") }, + { _id: "all", name: t("incidentsPage.incidentsOptionsHeaderFilterAll") }, + { _id: "active", name: t("incidentsPage.incidentsOptionsHeaderFilterActive") }, + { _id: "resolved", name: t("incidentsPage.incidentsOptionsHeaderFilterResolved") }, + { _id: "manual", name: t("incidentsPage.incidentsOptionsHeaderFilterManual") }, ]; - // The stacks below which are three in number have the same style so const stackStyles = { direction: "row", alignItems: "center", gap: theme.spacing(6), }; - if (!shouldRender) return ; + if (!shouldRender) return; return ( { - // Redux state +// Hooks +import useFetchIncidents from "./hooks/useFetchIncidents.js"; +import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks.js"; + +const Incidents2 = () => { const { t } = useTranslation(); - const BREADCRUMBS = [{ name: t("checksPageTitle"), path: "/checks" }]; + const BREADCRUMBS = [ + { name: t("incidentsPageTitle", "Incidents"), path: "/incidents" }, + ]; - // Local state const [selectedMonitor, setSelectedMonitor] = useState("0"); - const [filter, setFilter] = useState(undefined); - const [dateRange, setDateRange] = useState(undefined); + const [filter, setFilter] = useState("all"); + const [dateRange, setDateRange] = useState("all"); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); const [monitorLookup, setMonitorLookup] = useState(undefined); const [updateTrigger, setUpdateTrigger] = useState(false); + const handleUpdateTrigger = () => { + setUpdateTrigger((prev) => !prev); + }; - //Hooks - const { acknowledge, isLoadingAcknowledge } = useAcknowledgeChecks(); + const [monitors, isLoadingMonitors, monitorsNetworkError] = useFetchMonitorsByTeamId( + {} + ); + + const { + incidents, + incidentsCount, + isLoading: isLoadingIncidents, + networkError: incidentsNetworkError, + fetchIncidents, + fetchActiveIncidents, + fetchResolvedIncidents, + resolveIncident, + } = useFetchIncidents(); + + const networkError = monitorsNetworkError || incidentsNetworkError; - //Utils const theme = useTheme(); - const [monitors, , isLoading, networkError] = useFetchMonitorsByTeamId({}); - const [summary, isLoadingSummary, networkErrorSummary] = useFetchChecksSummaryByTeamId({ - updateTrigger, - }); - const { monitorId } = useParams(); useEffect(() => { - if (monitorId) { - setSelectedMonitor(monitorId); + setPage(0); + }, [selectedMonitor, filter, dateRange]); + + useEffect(() => { + const config = { + monitorId: selectedMonitor !== "0" ? selectedMonitor : undefined, + sortOrder: "desc", + dateRange, + page, + rowsPerPage, + }; + + if (filter === "active") { + fetchActiveIncidents(config); + } else if (filter === "resolved") { + fetchResolvedIncidents(config); + } else { + fetchIncidents(config); } - }, [monitorId]); + }, [ + selectedMonitor, + filter, + dateRange, + page, + rowsPerPage, + updateTrigger, + fetchActiveIncidents, + fetchResolvedIncidents, + fetchIncidents, + ]); useEffect(() => { - const monitorLookup = monitors?.reduce((acc, monitor) => { - acc[monitor._id] = { - _id: monitor._id, + const lookup = monitors?.reduce((acc, monitor) => { + acc[monitor.id] = { + id: monitor.id, name: monitor.name, type: monitor.type, }; return acc; }, {}); - setMonitorLookup(monitorLookup); + setMonitorLookup(lookup); }, [monitors]); - const handleAcknowledge = () => { - const monitorId = selectedMonitor === "0" ? null : selectedMonitor; - acknowledge(setUpdateTrigger, monitorId); - }; - - if (networkError || networkErrorSummary) { + if (networkError) { return ( @@ -75,27 +105,23 @@ const Checks = () => { ); } + const handleChangePage = (_, newPage) => { + setPage(newPage); + }; + + const handleChangeRowsPerPage = (event) => { + setRowsPerPage(parseInt(event.target.value, 10)); + setPage(0); + }; + return ( - - - - + + + { dateRange={dateRange} setDateRange={setDateRange} /> + ); }; -export default Checks; +export default Incidents2; diff --git a/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx deleted file mode 100644 index 670da4a81..000000000 --- a/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx +++ /dev/null @@ -1,209 +0,0 @@ -//Components -import DataTable from "@/Components/v1/Table/index.jsx"; -import TableSkeleton from "@/Components/v1/Table/skeleton.jsx"; -import Pagination from "@/Components/v1/Table/TablePagination/index.jsx"; -import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import { HttpStatusLabel } from "@/Components/v1/HttpStatusLabel/index.jsx"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; - -//Utils -import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; -import { useSelector } from "react-redux"; -import PropTypes from "prop-types"; -import { useTranslation } from "react-i18next"; -import { Button, Typography, useTheme } from "@mui/material"; - -const IncidentTable = ({ - monitors = [], - incidents = [], - incidentsCount = 0, - isLoading = false, - networkError = false, - page = 0, - rowsPerPage = 10, - handleChangePage, - handleChangeRowsPerPage, - resolveIncident, - handleUpdateTrigger, -}) => { - const uiTimezone = useSelector((state) => state.ui.timezone); - - const { t } = useTranslation(); - const theme = useTheme(); - - const handleResolveIncident = async (incidentId) => { - try { - await resolveIncident(incidentId); - handleUpdateTrigger(); - } catch (error) { - console.error(t("incidentsPage.errorResolvingIncident"), error); - } - }; - - const headers = [ - { - id: "monitorName", - content: t("incidentsTableMonitorName"), - render: (row) => { - console.log(monitors, row); - const monitor = monitors.find((monitor) => monitor.id === row.monitorId); - return monitor ? monitor.name : "N/A"; - }, - }, - { - id: "status", - content: t("incidentsTableStatus"), - render: (row) => { - const status = row.status === true ? "down" : "up"; - const statusText = - row.status === true ? t("incidentsPage.active") : t("incidentsPage.resolved"); - return ( - - ); - }, - }, - { - id: "startTime", - content: t("incidentsPage.startTime"), - render: (row) => { - const formattedDate = formatDateWithTz( - row.startTime || row.createdAt, - "YYYY-MM-DD HH:mm:ss A", - uiTimezone - ); - return formattedDate; - }, - }, - { - id: "endTime", - content: t("incidentsPage.endTime"), - render: (row) => { - if (row.endTime) { - return formatDateWithTz(row.endTime, "YYYY-MM-DD HH:mm:ss A", uiTimezone); - } - return "-"; - }, - }, - { - id: "resolutionType", - content: t("incidentsPage.resolutionType"), - render: (row) => { - if (row.resolutionType) { - return ( - - {row.resolutionType} - - ); - } - return "-"; - }, - }, - { - id: "statusCode", - content: t("incidentsTableStatusCode"), - render: (row) => , - }, - { - id: "message", - content: t("incidentsTableMessage"), - render: (row) => row.message || "-", - }, - { - id: "action", - content: t("actions"), - render: (row) => { - if (row.status === true) { - return ( - - ); - } else { - return ( - - {t("incidentsPage.incidentsTableResolved")} - - ); - } - }, - }, - ]; - - if (isLoading) return ; - - if (networkError) { - return ( - - - - ); - } - - if (!isLoading && !networkError && incidents?.length === 0) { - return ( - - {t("incidentsTableNoIncidents", "No incidents found")} - - ); - } - - const incidentsData = Array.isArray(incidents) ? incidents : []; - - return ( - <> - - - - ); -}; - -IncidentTable.propTypes = { - incidents: PropTypes.array.isRequired, // Array of incident objects - incidentsCount: PropTypes.number.isRequired, // Total count for pagination - isLoading: PropTypes.bool.isRequired, // Loading state - networkError: PropTypes.bool, // Network error object - page: PropTypes.number.isRequired, // Current page number - rowsPerPage: PropTypes.number.isRequired, // Number of rows per page - handleChangePage: PropTypes.func.isRequired, // Handler for page change - handleChangeRowsPerPage: PropTypes.func.isRequired, // Handler for rows per page change - resolveIncident: PropTypes.func.isRequired, - handleUpdateTrigger: PropTypes.func.isRequired, -}; - -export default IncidentTable; diff --git a/client/src/Pages/Incidents2/index.jsx b/client/src/Pages/Incidents2/index.jsx deleted file mode 100644 index 4e68279c5..000000000 --- a/client/src/Pages/Incidents2/index.jsx +++ /dev/null @@ -1,151 +0,0 @@ -// Components -import { Stack } from "@mui/material"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import IncidentTable from "./Components/IncidentTable/index.jsx"; -import OptionsHeader from "./Components/OptionsHeader/index.jsx"; -import IncidentsSummaryPanel from "./Components/IncidentsSummaryPanel/index.jsx"; - -//Utils -import { useTheme } from "@emotion/react"; -import { useState, useEffect } from "react"; -import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; -import { useTranslation } from "react-i18next"; - -// Hooks -import useFetchIncidents from "./hooks/useFetchIncidents.js"; -import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks.js"; - -const Incidents2 = () => { - const { t } = useTranslation(); - - const BREADCRUMBS = [ - { name: t("incidentsPageTitle", "Incidents"), path: "/incidents" }, - ]; - - const [selectedMonitor, setSelectedMonitor] = useState("0"); - const [filter, setFilter] = useState("all"); - const [dateRange, setDateRange] = useState("all"); - const [page, setPage] = useState(0); - const [rowsPerPage, setRowsPerPage] = useState(10); - const [monitorLookup, setMonitorLookup] = useState(undefined); - const [updateTrigger, setUpdateTrigger] = useState(false); - const handleUpdateTrigger = () => { - setUpdateTrigger((prev) => !prev); - }; - - const [monitors, isLoadingMonitors, monitorsNetworkError] = useFetchMonitorsByTeamId( - {} - ); - - const { - incidents, - incidentsCount, - isLoading: isLoadingIncidents, - networkError: incidentsNetworkError, - fetchIncidents, - fetchActiveIncidents, - fetchResolvedIncidents, - resolveIncident, - } = useFetchIncidents(); - - const networkError = monitorsNetworkError || incidentsNetworkError; - - const theme = useTheme(); - - useEffect(() => { - setPage(0); - }, [selectedMonitor, filter, dateRange]); - - useEffect(() => { - const config = { - monitorId: selectedMonitor !== "0" ? selectedMonitor : undefined, - sortOrder: "desc", - dateRange, - page, - rowsPerPage, - }; - - if (filter === "active") { - fetchActiveIncidents(config); - } else if (filter === "resolved") { - fetchResolvedIncidents(config); - } else { - fetchIncidents(config); - } - }, [ - selectedMonitor, - filter, - dateRange, - page, - rowsPerPage, - updateTrigger, - fetchActiveIncidents, - fetchResolvedIncidents, - fetchIncidents, - ]); - - useEffect(() => { - const lookup = monitors?.reduce((acc, monitor) => { - acc[monitor.id] = { - id: monitor.id, - name: monitor.name, - type: monitor.type, - }; - return acc; - }, {}); - setMonitorLookup(lookup); - }, [monitors]); - - if (networkError) { - return ( - - - - ); - } - - const handleChangePage = (_, newPage) => { - setPage(newPage); - }; - - const handleChangeRowsPerPage = (event) => { - setRowsPerPage(parseInt(event.target.value, 10)); - setPage(0); - }; - - return ( - - - - - - - - - - ); -}; - -export default Incidents2; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index d9f6d58cb..878b17b09 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -28,9 +28,11 @@ import InfrastructureDetails from "../Pages/Infrastructure/Details/index.jsx"; // Server Status import ServerUnreachable from "../Pages/ServerUnreachable.jsx"; +// Checks +import Checks from "../Pages/Checks/index.jsx"; + // Incidents -import Checks from "../Pages/Incidents"; -import Incidents2 from "../Pages/Incidents2/index.jsx"; +import Incidents from "../Pages/Incidents/index.jsx"; // Status pages import CreateStatus from "../Pages/StatusPage/Create/index.jsx"; @@ -136,7 +138,7 @@ const Routes = () => { /> } + element={} /> Date: Tue, 20 Jan 2026 19:21:03 +0000 Subject: [PATCH 288/400] remove ack --- client/src/Hooks/checkHooks.js | 27 -------------- .../Checks/Components/IncidentTable/index.jsx | 37 +------------------ client/src/Pages/Checks/index.jsx | 24 +----------- 3 files changed, 4 insertions(+), 84 deletions(-) diff --git a/client/src/Hooks/checkHooks.js b/client/src/Hooks/checkHooks.js index b8af32073..49a9b6ba3 100644 --- a/client/src/Hooks/checkHooks.js +++ b/client/src/Hooks/checkHooks.js @@ -185,36 +185,9 @@ const useResolveIncident = () => { return [resolveIncident, isLoading]; }; -const useAcknowledgeChecks = () => { - const [isLoading, setIsLoading] = useState(false); - const { t } = useTranslation(); - - const acknowledge = async (setUpdateTrigger, monitorId = null) => { - setIsLoading(true); - try { - if (monitorId) { - await networkService.updateMonitorChecksStatus({ monitorId, ack: true }); - } else { - await networkService.updateAllChecksStatus({ ack: true }); - } - setUpdateTrigger((prev) => !prev); - } catch (error) { - const toastMessage = monitorId - ? t("checkHooks.failureResolveMonitor") - : t("checkHooks.failureResolveAll"); - createToast({ body: toastMessage }); - } finally { - setIsLoading(false); - } - }; - - return { acknowledge, isLoading }; -}; - export { useFetchChecksByMonitor, useFetchChecksTeam, useFetchChecksSummaryByTeamId, useResolveIncident, - useAcknowledgeChecks, }; diff --git a/client/src/Pages/Checks/Components/IncidentTable/index.jsx b/client/src/Pages/Checks/Components/IncidentTable/index.jsx index e066049e6..cce9b10b7 100644 --- a/client/src/Pages/Checks/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Checks/Components/IncidentTable/index.jsx @@ -18,11 +18,7 @@ import { useSelector } from "react-redux"; import { useState } from "react"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; -import { - useFetchChecksTeam, - useFetchChecksByMonitor, - useResolveIncident, -} from "@/Hooks/checkHooks.js"; +import { useFetchChecksTeam, useFetchChecksByMonitor } from "@/Hooks/checkHooks.js"; import { Button, Typography, useTheme } from "@mui/material"; import { lighten } from "@mui/material/styles"; @@ -93,7 +89,6 @@ const IncidentTable = ({ filter, dateRange, updateTrigger, - setUpdateTrigger, }) => { //Redux state const uiTimezone = useSelector((state) => state.ui.timezone); @@ -105,8 +100,6 @@ const IncidentTable = ({ const selectedMonitorType = selectedMonitorDetails?.type; //Hooks - const [resolveIncident, resolveLoading] = useResolveIncident(); - const [checksMonitor, checksCountMonitor, isLoadingMonitor, networkErrorMonitor] = useFetchChecksByMonitor({ monitorId: selectedMonitor === "0" ? undefined : selectedMonitor, @@ -153,10 +146,6 @@ const IncidentTable = ({ setRowsPerPage(event.target.value); }; - const handleResolveIncident = (checkId) => { - resolveIncident(checkId, setUpdateTrigger); - }; - const headers = [ { id: "monitorName", @@ -195,31 +184,9 @@ const IncidentTable = ({ render: (row) => , }, { id: "message", content: t("incidentsTableMessage"), render: (row) => row.message }, - { - id: "action", - content: t("actions"), - render: (row) => { - return row.ack === false ? ( - - ) : ( - - {t("incidentsTableResolvedAt")}{" "} - {formatDateWithTz(row.ackAt, "YYYY-MM-DD HH:mm:ss A", uiTimezone)} - - ); - }, - }, ]; - if (isLoading || resolveLoading) return ; + if (isLoading) return ; if (networkError) { return ( diff --git a/client/src/Pages/Checks/index.jsx b/client/src/Pages/Checks/index.jsx index 698fc4a2f..d0c2ea80d 100644 --- a/client/src/Pages/Checks/index.jsx +++ b/client/src/Pages/Checks/index.jsx @@ -10,10 +10,7 @@ import { Box, Button } from "@mui/material"; //Utils import { useTheme } from "@emotion/react"; import { useFetchMonitorsByTeamId } from "@/Hooks/monitorHooks.js"; -import { - useFetchChecksSummaryByTeamId, - useAcknowledgeChecks, -} from "@/Hooks/checkHooks.js"; +import { useFetchChecksSummaryByTeamId } from "@/Hooks/checkHooks.js"; import { useState, useEffect } from "react"; import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; import { useTranslation } from "react-i18next"; @@ -34,7 +31,6 @@ const Checks = () => { const [updateTrigger, setUpdateTrigger] = useState(false); //Hooks - const { acknowledge, isLoadingAcknowledge } = useAcknowledgeChecks(); //Utils const theme = useTheme(); @@ -62,11 +58,6 @@ const Checks = () => { setMonitorLookup(monitorLookup); }, [monitors]); - const handleAcknowledge = () => { - const monitorId = selectedMonitor === "0" ? null : selectedMonitor; - acknowledge(setUpdateTrigger, monitorId); - }; - if (networkError || networkErrorSummary) { return ( @@ -78,18 +69,7 @@ const Checks = () => { return ( - - - + Date: Tue, 20 Jan 2026 19:22:04 +0000 Subject: [PATCH 289/400] remove dependency --- server/src/config/services.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index cbd58f2e9..74a261d05 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -222,7 +222,7 @@ export const initializeServices = async ({ checksRepository, }); - const bufferService = new BufferService({ logger, incidentService, checkService }); + const bufferService = new BufferService({ logger, checkService }); const statusService = new StatusService({ db, logger, buffer: bufferService, monitorsRepository }); From 476ff39a17cd423bb7ebec59f6ffa80ac94661dd Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 19:25:00 +0000 Subject: [PATCH 290/400] remove check ack routes and controller functions --- server/src/controllers/checkController.ts | 47 ----------------------- server/src/routes/v1/checkRoute.js | 5 --- 2 files changed, 52 deletions(-) diff --git a/server/src/controllers/checkController.ts b/server/src/controllers/checkController.ts index 8ae6fd785..8277df3e7 100755 --- a/server/src/controllers/checkController.ts +++ b/server/src/controllers/checkController.ts @@ -77,53 +77,6 @@ class CheckController { } }; - ackCheck = async (req: Request, res: Response, next: NextFunction) => { - try { - await ackCheckBodyValidation.validateAsync(req.body); - - const updatedCheck = await this.checkService.ackCheck({ - checkId: req?.params?.checkId, - teamId: req?.user?.teamId, - ack: req?.body?.ack, - }); - - return res.status(200).json({ - success: true, - msg: "Check acknowledged successfully", - data: updatedCheck, - }); - } catch (error) { - next(error); - } - }; - - ackAllChecks = async (req: Request, res: Response, next: NextFunction) => { - try { - await ackAllChecksParamValidation.validateAsync(req.params); - await ackAllChecksBodyValidation.validateAsync(req.body); - - const teamId = req?.user?.teamId; - if (!teamId) { - throw new Error("No team ID in request"); - } - - const updatedChecks = await this.checkService.ackAllChecks({ - monitorId: req?.params?.monitorId, - path: req?.params?.path, - teamId: req?.user?.teamId, - ack: req?.body?.ack, - }); - - return res.status(200).json({ - success: true, - msg: "All checks acknowledged successfully", - data: updatedChecks, - }); - } catch (error) { - next(error); - } - }; - deleteChecks = async (req: Request, res: Response, next: NextFunction) => { try { await deleteChecksParamValidation.validateAsync(req.params); diff --git a/server/src/routes/v1/checkRoute.js b/server/src/routes/v1/checkRoute.js index 78f6b8a42..6565425d4 100755 --- a/server/src/routes/v1/checkRoute.js +++ b/server/src/routes/v1/checkRoute.js @@ -14,13 +14,8 @@ class CheckRoutes { this.router.get("/team", this.checkController.getChecksByTeam); this.router.put("/team/ttl", isAllowed(["admin", "superadmin"]), this.checkController.updateChecksTTL); this.router.delete("/team", isAllowed(["admin", "superadmin"]), this.checkController.deleteChecksByTeamId); - - this.router.put("/check/:checkId", this.checkController.ackCheck); - this.router.get("/:monitorId", this.checkController.getChecksByMonitor); this.router.delete("/:monitorId", this.checkController.deleteChecks); - - this.router.put("/:path/:monitorId?", this.checkController.ackAllChecks); } getRouter() { From c0e4893f964aaebbc5aae1c4b8be91ca286ebf69 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 19:26:46 +0000 Subject: [PATCH 291/400] remove ack service and db methods --- server/src/db/modules/checkModule.js | 35 --------------------- server/src/service/business/checkService.ts | 27 ---------------- 2 files changed, 62 deletions(-) diff --git a/server/src/db/modules/checkModule.js b/server/src/db/modules/checkModule.js index e420d5ce7..e6304e20c 100755 --- a/server/src/db/modules/checkModule.js +++ b/server/src/db/modules/checkModule.js @@ -178,41 +178,6 @@ class CheckModule { } }; - ackCheck = async (checkId, teamId, ack) => { - try { - const updatedCheck = await CheckModel.findOneAndUpdate( - { _id: checkId, "metadata.teamId": teamId }, - { $set: { ack, ackAt: new Date() } }, - { new: true } - ); - - if (!updatedCheck) { - throw new Error("Check not found"); - } - - return updatedCheck; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "ackCheck"; - throw error; - } - }; - - ackAllChecks = async (monitorId, teamId, ack, path) => { - try { - const filter = - path === "monitor" - ? { "metadata.monitorId": new mongoose.Types.ObjectId(monitorId) } - : { "metadata.teamId": new mongoose.Types.ObjectId(teamId) }; - const updatedChecks = await CheckModel.updateMany(filter, { $set: { ack, ackAt: new Date() } }); - return updatedChecks.modifiedCount; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "ackAllChecks"; - throw error; - } - }; - getChecksSummaryByTeamId = async ({ teamId }) => { try { const matchStage = { diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index 3e6c727b5..8dee9d215 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -174,33 +174,6 @@ class CheckService { return summary; }; - ackCheck = async ({ checkId, teamId, ack }: { checkId: string; teamId: string; ack: any }) => { - if (!checkId) { - throw this.errorService.createBadRequestError("No check ID in request"); - } - - if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); - } - - const updatedCheck = await this.db.checkModule.ackCheck(checkId, teamId, ack); - return updatedCheck; - }; - - ackAllChecks = async ({ monitorId, path, teamId, ack }: { monitorId: string; path: string; teamId: string; ack: any }) => { - if (path === "monitor") { - if (!monitorId) { - throw this.errorService.createBadRequestError("No monitor ID in request"); - } - - // For verificaiton, throws an error if monitor doesn't belong to team - await this.monitorsRepository.findById(monitorId, teamId); - } - - const updatedChecks = await this.db.checkModule.ackAllChecks(monitorId, teamId, ack, path); - return updatedChecks; - }; - deleteChecks = async ({ monitorId, teamId }: { monitorId: string; teamId: string }) => { if (!monitorId) { throw this.errorService.createBadRequestError("No monitor ID in request"); From c1b2bda62392665afffa9c44334156348b3845d1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 19:41:57 +0000 Subject: [PATCH 292/400] _id -> id --- .../src/Pages/Checks/Components/IncidentTable/index.jsx | 4 +++- .../src/Pages/Checks/Components/OptionsHeader/index.jsx | 9 +++++---- client/src/Pages/Checks/index.jsx | 4 ++-- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/client/src/Pages/Checks/Components/IncidentTable/index.jsx b/client/src/Pages/Checks/Components/IncidentTable/index.jsx index cce9b10b7..31475f8b1 100644 --- a/client/src/Pages/Checks/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Checks/Components/IncidentTable/index.jsx @@ -150,7 +150,9 @@ const IncidentTable = ({ { id: "monitorName", content: t("incidentsTableMonitorName"), - render: (row) => monitors[row.monitorId]?.name ?? "N/A", + render: (row) => { + return monitors?.[row.metadata?.monitorId]?.name || "N/A"; + }, }, { id: "status", diff --git a/client/src/Pages/Checks/Components/OptionsHeader/index.jsx b/client/src/Pages/Checks/Components/OptionsHeader/index.jsx index c16f54f18..74bea4711 100644 --- a/client/src/Pages/Checks/Components/OptionsHeader/index.jsx +++ b/client/src/Pages/Checks/Components/OptionsHeader/index.jsx @@ -20,12 +20,13 @@ const OptionsHeader = ({ }) => { const theme = useTheme(); const { t } = useTranslation(); + console.log(monitors); const monitorNames = typeof monitors !== "undefined" ? Object.values(monitors) : []; const filterOptions = [ - { _id: "all", name: t("incidentsOptionsHeaderFilterAll") }, - { _id: "down", name: t("incidentsOptionsHeaderFilterDown") }, - { _id: "resolve", name: t("incidentsOptionsHeaderFilterCannotResolve") }, - { _id: "resolved", name: t("incidentsOptionsHeaderFilterResolved") }, + { id: "all", name: t("incidentsOptionsHeaderFilterAll") }, + { id: "down", name: t("incidentsOptionsHeaderFilterDown") }, + { id: "resolve", name: t("incidentsOptionsHeaderFilterCannotResolve") }, + { id: "resolved", name: t("incidentsOptionsHeaderFilterResolved") }, ]; // The stacks below which are three in number have the same style so diff --git a/client/src/Pages/Checks/index.jsx b/client/src/Pages/Checks/index.jsx index d0c2ea80d..aa8717c1a 100644 --- a/client/src/Pages/Checks/index.jsx +++ b/client/src/Pages/Checks/index.jsx @@ -48,8 +48,8 @@ const Checks = () => { useEffect(() => { const monitorLookup = monitors?.reduce((acc, monitor) => { - acc[monitor._id] = { - _id: monitor._id, + acc[monitor.id] = { + id: monitor.id, name: monitor.name, type: monitor.type, }; From a69091bb2875bfda3f057bbdd20c61b996d47c48 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 19:49:40 +0000 Subject: [PATCH 293/400] replaced monitor with updatedMonitor --- .../SuperSimpleQueue/SuperSimpleQueueHelper.ts | 16 ++++++++++++---- .../notificationProviders/utils.ts | 2 +- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 26d45db22..9a2957ee9 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -86,20 +86,28 @@ class SuperSimpleQueueHelper { // Step 5 handle notifications (best effort, continue even in event of failure, don't wait) this.notificationsService - .handleNotifications(monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged) + .handleNotifications(statusChangeResult.monitor, status, statusChangeResult.prevStatus, statusChangeResult.statusChanged) .catch((error: any) => { this.logger.error({ message: error.message, service: SERVICE_NAME, method: "getMonitorJob", - details: `Error sending notifications for job ${monitor.id}: ${error.message}`, + details: `Error sending notifications for job ${statusChangeResult.monitor.id}: ${error.message}`, stack: error.stack, }); }); - // Step 6. Handle incidents + // Step 6. Handle incidents (best effort, don't wait) if (statusChangeResult.statusChanged) { - await this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code); + this.incidentService.handleIncident(statusChangeResult.monitor, statusChangeResult.code).catch((error: any) => { + this.logger.warn({ + message: error.message, + service: SERVICE_NAME, + method: "getMonitorJob", + details: `Error handling incident for job ${monitor.id}: ${error.message}`, + stack: error.stack, + }); + }); } } catch (error: any) { this.logger.warn({ diff --git a/server/src/service/infrastructure/notificationProviders/utils.ts b/server/src/service/infrastructure/notificationProviders/utils.ts index a7e954e55..f71933425 100644 --- a/server/src/service/infrastructure/notificationProviders/utils.ts +++ b/server/src/service/infrastructure/notificationProviders/utils.ts @@ -163,7 +163,7 @@ export const buildHardwareEmail = async (emailService: any, monitor: Monitor, al }; export const buildEmail = async (emailService: any, monitor: Monitor): Promise => { - const template = monitor.status === false ? "serverIsUpTemplate" : "serverIsDownTemplate"; + const template = monitor.status === true ? "serverIsUpTemplate" : "serverIsDownTemplate"; const context = { monitor: monitor.name, url: monitor.url }; const html = await emailService.buildEmail(template, context); return html; From 6101fe1bfa2eafd349f534775d2de6b0ea406dcb Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Wed, 21 Jan 2026 01:42:00 +0530 Subject: [PATCH 294/400] Fix: Configure Pause Button --- client/src/Components/v1/MonitorDetailsControlHeader/index.jsx | 2 +- client/src/Pages/Uptime/Create/index.jsx | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx b/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx index c5b62280f..0793e7149 100644 --- a/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx +++ b/client/src/Components/v1/MonitorDetailsControlHeader/index.jsx @@ -124,7 +124,7 @@ const MonitorDetailsControlHeader = ({ }); }} > - {monitor?.isActive ? "Pause" : "Resume"} + {monitor?.isActive ? t("pause") : t("resume")} )} {isAdmin && ( diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index b1460dba6..1d0aa64f4 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -285,6 +285,7 @@ const UptimeCreate = ({ isClone = false }) => { const handlePause = async () => { await pauseMonitor({ monitorId, triggerUpdate }); + setMonitor((prev) => ({ ...prev, isActive: !prev.isActive })); }; const handleRemove = async (event) => { From 9af25a19e52192df3c96d77507272a0679f2b294 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 20:14:23 +0000 Subject: [PATCH 295/400] findByMonitorId --- server/src/config/services.ts | 2 - server/src/db/modules/checkModule.js | 28 +++---- .../repositories/checks/IChecksRepository.ts | 9 +++ .../checks/MongoChecksRepistory.ts | 80 +++++++++++++++++++ server/src/service/business/checkService.ts | 39 +++++---- 5 files changed, 117 insertions(+), 41 deletions(-) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 74a261d05..0fb78da25 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -214,8 +214,6 @@ export const initializeServices = async ({ const checkService = new CheckService({ db, - settingsService, - stringService, errorService, monitorsRepository, logger, diff --git a/server/src/db/modules/checkModule.js b/server/src/db/modules/checkModule.js index e6304e20c..4fdf10678 100755 --- a/server/src/db/modules/checkModule.js +++ b/server/src/db/modules/checkModule.js @@ -20,29 +20,16 @@ class CheckModule { this.User = User; } - createChecks = async (checks) => { - try { - await CheckModel.insertMany(checks, { ordered: false, runValidators: true }); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createCheck"; - throw error; - } - }; - - getChecksByMonitor = async ({ monitorId, sortOrder, dateRange, filter, ack, page, rowsPerPage, status }) => { + getChecksByMonitor = async ({ monitorId, sortOrder, dateRange, filter, page, rowsPerPage, status }) => { try { status = status === "true" ? true : status === "false" ? false : undefined; page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); - const ackStage = ack === "true" ? { ack: true } : { $or: [{ ack: false }, { ack: { $exists: false } }] }; - // Match const matchStage = { "metadata.monitorId": new ObjectId(monitorId), ...(typeof status !== "undefined" && { status }), - ...(typeof ack !== "undefined" && ackStage), ...(dateRangeLookup[dateRange] && { createdAt: { $gte: dateRangeLookup[dateRange], @@ -106,17 +93,22 @@ class CheckModule { } }; - getChecksByTeam = async ({ sortOrder, dateRange, filter, ack, page, rowsPerPage, teamId }) => { + getChecksByTeam = async ({ sortOrder, dateRange, filter, page, rowsPerPage, teamId }) => { try { + console.log({ + sortOrder, + dateRange, + filter, + page, + rowsPerPage, + teamId, + }); page = parseInt(page); rowsPerPage = parseInt(rowsPerPage); - const ackStage = ack === "true" ? { ack: true } : { $or: [{ ack: false }, { ack: { $exists: false } }] }; - const matchStage = { "metadata.teamId": new ObjectId(teamId), status: false, - ...(typeof ack !== "undefined" && ackStage), ...(dateRangeLookup[dateRange] && { createdAt: { $gte: dateRangeLookup[dateRange], diff --git a/server/src/repositories/checks/IChecksRepository.ts b/server/src/repositories/checks/IChecksRepository.ts index 7b98ae7b6..72f7e632d 100644 --- a/server/src/repositories/checks/IChecksRepository.ts +++ b/server/src/repositories/checks/IChecksRepository.ts @@ -59,6 +59,15 @@ export interface IChecksRepository { // single fetch // collection fetch + findByMonitorId( + monitorId: string, + sortOrder: string, + dateRange: string, + filter: string, + page: number, + rowsPerPage: number, + status: boolean | undefined + ): Promise; findLatestChecksByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise; findDateRangeChecksByMonitor( monitorId: string, diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 7707a7b17..f89e2318a 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -18,6 +18,15 @@ import mongoose from "mongoose"; const SERVICE_NAME = "StatusService"; +const dateRangeLookup: Record = { + recent: new Date(new Date().setDate(new Date().getDate() - 2)), + hour: new Date(new Date().setHours(new Date().getHours() - 1)), + day: new Date(new Date().setDate(new Date().getDate() - 1)), + week: new Date(new Date().setDate(new Date().getDate() - 7)), + month: new Date(new Date().setMonth(new Date().getMonth() - 1)), + all: undefined, +}; + export type LatestChecksMap = Record; type DateRange = { start: Date; end: Date }; type HardwareAggregateData = { latestCheck: CheckDocument | null; totalChecks: number }; @@ -189,6 +198,77 @@ class MongoChecksRepository implements IChecksRepository { return await CheckModel.insertMany(checks); }; + findByMonitorId = async ( + monitorId: string, + sortOrder: string, + dateRange: string, + filter: string, + page: number, + rowsPerPage: number, + status: boolean | undefined + ) => { + // Match + const matchStage: Record = { + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + ...(typeof status !== "undefined" && { status }), + ...(dateRangeLookup[dateRange] && { + createdAt: { + $gte: dateRangeLookup[dateRange], + }, + }), + }; + + if (filter !== undefined) { + switch (filter) { + case "all": + break; + case "down": + break; + case "resolve": + matchStage.statusCode = 5000; + break; + default: + this.logger.warn({ + message: "invalid filter", + service: SERVICE_NAME, + method: "getChecks", + }); + break; + } + } + + //Sort + const convertedSortOrder = sortOrder === "asc" ? 1 : -1; + + // Pagination + let skip = 0; + if (page && rowsPerPage) { + skip = page * rowsPerPage; + } + + const checks = await CheckModel.aggregate([ + { $match: matchStage }, + { $sort: { createdAt: convertedSortOrder } }, + { + $facet: { + summary: [{ $count: "checksCount" }], + checks: [{ $skip: skip }, { $limit: rowsPerPage }], + }, + }, + { + $project: { + checksCount: { + $ifNull: [{ $arrayElemAt: ["$summary.checksCount", 0] }, 0], + }, + checks: { + $ifNull: ["$checks", []], + }, + }, + }, + ]); + return checks[0]; + }; + findLatestChecksByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise => { if (monitorIds.length === 0) { return {}; diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index 8dee9d215..3a1a5fb18 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -1,6 +1,7 @@ import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js"; import type { MonitorType, MonitorStatusResponse, CheckErrorInfo, Check } from "@/types/index.js"; import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; +import { ParseBoolean } from "@/utils/utils.js"; const SERVICE_NAME = "checkService"; @@ -8,32 +9,24 @@ class CheckService { static SERVICE_NAME = SERVICE_NAME; private db: any; - private settingsService: any; - private stringService: any; private errorService: any; private monitorsRepository: IMonitorsRepository; private checksRepository: IChecksRepository; private logger: any; constructor({ db, - settingsService, - stringService, errorService, monitorsRepository, logger, checksRepository, }: { db: any; - settingsService: any; - stringService: any; errorService: any; monitorsRepository: IMonitorsRepository; logger: any; checksRepository: IChecksRepository; }) { this.db = db; - this.settingsService = settingsService; - this.stringService = stringService; this.errorService = errorService; this.monitorsRepository = monitorsRepository; this.logger = logger; @@ -132,22 +125,27 @@ class CheckService { // For verificaiton, throws an error if monitor doesn't belong to team await this.monitorsRepository.findById(monitorId, teamId); - let { sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query; - const result = await this.db.checkModule.getChecksByMonitor({ - monitorId, - sortOrder, - dateRange, - filter, - ack, - page, - rowsPerPage, - status, - }); + let { sortOrder, dateRange, filter, page, rowsPerPage, status } = query; + const parsedStatus = typeof status === "undefined" ? status : ParseBoolean(status); + const parsedPage = page ? parseInt(page) : page; + const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage; + + const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, parsedStatus); + + // const result = await this.db.checkModule.getChecksByMonitor({ + // monitorId, + // sortOrder, + // dateRange, + // filter, + // page, + // rowsPerPage, + // status, + // }); return result; }; getChecksByTeam = async ({ teamId, query }: { teamId: string; query: any }) => { - let { sortOrder, dateRange, filter, ack, page, rowsPerPage } = query; + let { sortOrder, dateRange, filter, page, rowsPerPage } = query; if (!teamId) { throw this.errorService.createBadRequestError("No team ID in request"); @@ -157,7 +155,6 @@ class CheckService { sortOrder, dateRange, filter, - ack, page, rowsPerPage, teamId, From dd259fea8924790065c3371eb127cc668ca5d3ca Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Wed, 21 Jan 2026 02:02:16 +0530 Subject: [PATCH 296/400] Fix: One source of truth --- client/src/Hooks/monitorHooks.js | 9 +++++++-- client/src/Pages/Uptime/Create/index.jsx | 9 ++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index 9d46e9365..f0bf805f4 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -412,14 +412,19 @@ export const usePauseMonitor = () => { try { setIsLoading(true); const res = await networkService.pauseMonitorById({ monitorId }); + const updatedMonitor = res.data.data; createToast({ - body: res.data.data.isActive + body: updatedMonitor.isActive ? "Monitor resumed successfully" : "Monitor paused successfully", }); - triggerUpdate(); + if (triggerUpdate) { + triggerUpdate(); + } + return updatedMonitor; } catch (error) { setError(error); + return null; } finally { setIsLoading(false); } diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index 1d0aa64f4..6688f9b12 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -284,8 +284,10 @@ const UptimeCreate = ({ isClone = false }) => { }; const handlePause = async () => { - await pauseMonitor({ monitorId, triggerUpdate }); - setMonitor((prev) => ({ ...prev, isActive: !prev.isActive })); + const updatedMonitor = await pauseMonitor({ monitorId }); + if (updatedMonitor) { + setMonitor((prev) => ({ ...prev, isActive: updatedMonitor.isActive })); + } }; const handleRemove = async (event) => { @@ -425,7 +427,8 @@ const UptimeCreate = ({ isClone = false }) => { + )} + + + + + + + ); +}; + +IncidentDetailsModal.propTypes = { + open: PropTypes.bool.isRequired, + incidentId: PropTypes.string, + onClose: PropTypes.func.isRequired, + onResolved: PropTypes.func, +}; + +export default IncidentDetailsModal; diff --git a/client/src/Pages/Incidents2/Components/IncidentTable/IncidentActionsMenu.jsx b/client/src/Pages/Incidents2/Components/IncidentTable/IncidentActionsMenu.jsx index 59749fca7..2902da049 100644 --- a/client/src/Pages/Incidents2/Components/IncidentTable/IncidentActionsMenu.jsx +++ b/client/src/Pages/Incidents2/Components/IncidentTable/IncidentActionsMenu.jsx @@ -2,28 +2,25 @@ import IconButton from "@mui/material/IconButton"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; -import Settings from "../../../../../assets/icons/settings-bold.svg?react"; -import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; -import { Button, Stack } from "@mui/material"; +import Settings from "../../../../assets/icons/settings-bold.svg?react"; +import ResolveIncidentDialog from "../ResolveIncidentDialog/index.jsx"; import useFetchIncidents from "../../hooks/useFetchIncidents"; // Utils import { useState } from "react"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; -import { useNavigate } from "react-router-dom"; import { TypeToPathMap } from "@/Utils/monitorUtils.js"; +import { useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; -const IncidentActionsMenu = ({ incident, onResolve }) => { +const IncidentActionsMenu = ({ incident, onResolve, onOpenDetails }) => { const [anchorEl, setAnchorEl] = useState(null); const [isDialogOpen, setIsDialogOpen] = useState(false); - const [comment, setComment] = useState(""); const theme = useTheme(); const { t } = useTranslation(); - const navigate = useNavigate(); const { fetchIncidentById } = useFetchIncidents(); + const navigate = useNavigate(); const openMenu = (event) => { event.preventDefault(); event.stopPropagation(); @@ -39,7 +36,6 @@ const IncidentActionsMenu = ({ incident, onResolve }) => { const openResolveDialog = (e) => { closeMenu(e); - setComment(""); setIsDialogOpen(true); }; @@ -47,17 +43,6 @@ const IncidentActionsMenu = ({ incident, onResolve }) => { if (e) { e.stopPropagation(); } - setComment(""); - setIsDialogOpen(false); - }; - - const handleConfirmResolve = (e) => { - e.stopPropagation(); - if (onResolve) { - const options = comment.trim() ? { comment: comment.trim() } : {}; - onResolve(incident._id, options); - } - setComment(""); setIsDialogOpen(false); }; @@ -65,7 +50,9 @@ const IncidentActionsMenu = ({ incident, onResolve }) => { const handleGoToDetails = (e) => { e.stopPropagation(); closeMenu(e); - navigate(`/incidents/details/${incident._id}`); + if (onOpenDetails) { + onOpenDetails(incident._id); + } }; const handleGoToMonitor = async (e) => { @@ -128,6 +115,12 @@ const IncidentActionsMenu = ({ incident, onResolve }) => { }, }} > + + {t("incidentsPage.incidentsTableActionDetails", "Details")} + + + {t("incidentsPage.incidentsTableActionGoToMonitor", "Go to monitor")} + {isActive && ( { @@ -138,57 +131,13 @@ const IncidentActionsMenu = ({ incident, onResolve }) => { {t("incidentsPage.incidentsTableActionResolveManually")} )} - - {t("incidentsPage.incidentsTableActionDetails", "Details")} - - - {t("incidentsPage.incidentsTableActionGoToMonitor", "Go to monitor")} - - - Add comment - - {/* Future options will be added here */}
- - - setComment(e.target.value)} - maxWidth="100%" - /> - - - - - - + onResolve={onResolve} + /> ); }; @@ -199,6 +148,7 @@ IncidentActionsMenu.propTypes = { status: PropTypes.bool.isRequired, }).isRequired, onResolve: PropTypes.func.isRequired, + onOpenDetails: PropTypes.func, }; export default IncidentActionsMenu; diff --git a/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx index 05d73f755..157cf5103 100644 --- a/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Incidents2/Components/IncidentTable/index.jsx @@ -26,6 +26,7 @@ const IncidentTable = ({ handleChangeRowsPerPage, resolveIncident, handleUpdateTrigger, + onOpenDetails, }) => { const uiTimezone = useSelector((state) => state.ui.timezone); @@ -125,6 +126,7 @@ const IncidentTable = ({ ); }, @@ -180,6 +182,7 @@ IncidentTable.propTypes = { handleChangeRowsPerPage: PropTypes.func.isRequired, // Handler for rows per page change resolveIncident: PropTypes.func.isRequired, handleUpdateTrigger: PropTypes.func.isRequired, + onOpenDetails: PropTypes.func, }; export default IncidentTable; diff --git a/client/src/Pages/Incidents2/Components/ResolveIncidentDialog/index.jsx b/client/src/Pages/Incidents2/Components/ResolveIncidentDialog/index.jsx new file mode 100644 index 000000000..e2be5fed2 --- /dev/null +++ b/client/src/Pages/Incidents2/Components/ResolveIncidentDialog/index.jsx @@ -0,0 +1,105 @@ +import { useEffect, useState } from "react"; +import PropTypes from "prop-types"; +import { Button, Stack } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "@emotion/react"; +import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; + +const ResolveIncidentDialog = ({ + open, + incidentId, + onClose, + onResolve, + onAfterResolve, + onResolved, + isLoading, +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const [comment, setComment] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + if (open) { + setComment(""); + } + }, [open]); + + const handleConfirm = async () => { + if (!incidentId) return; + const trimmed = comment.trim(); + try { + setIsSubmitting(true); + await onResolve(incidentId, trimmed ? { comment: trimmed } : {}); + if (onAfterResolve) { + await onAfterResolve(); + } + onResolved?.(); + onClose(); + } catch (error) { + console.error("Error resolving incident:", error); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + setComment(e.target.value)} + maxWidth="100%" + /> + + + + + + + ); +}; + +ResolveIncidentDialog.propTypes = { + open: PropTypes.bool.isRequired, + incidentId: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + onResolve: PropTypes.func.isRequired, + onAfterResolve: PropTypes.func, + isLoading: PropTypes.bool, + onResolved: PropTypes.func, +}; + +ResolveIncidentDialog.defaultProps = { + isLoading: false, +}; + +export default ResolveIncidentDialog; diff --git a/client/src/Pages/Incidents2/index.jsx b/client/src/Pages/Incidents2/index.jsx index 966388b51..e3bd53ab4 100644 --- a/client/src/Pages/Incidents2/index.jsx +++ b/client/src/Pages/Incidents2/index.jsx @@ -5,6 +5,7 @@ import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; import IncidentTable from "./Components/IncidentTable/index.jsx"; import OptionsHeader from "./Components/OptionsHeader/index.jsx"; import IncidentsSummaryPanel from "./Components/IncidentsSummaryPanel/index.jsx"; +import IncidentDetailsModal from "./Components/IncidentDetailsModal/index.jsx"; //Utils import { useTheme } from "@emotion/react"; @@ -30,6 +31,8 @@ const Incidents2 = () => { const [rowsPerPage, setRowsPerPage] = useState(10); const [monitorLookup, setMonitorLookup] = useState(undefined); const [updateTrigger, setUpdateTrigger] = useState(false); + const [isDetailsOpen, setIsDetailsOpen] = useState(false); + const [selectedIncidentId, setSelectedIncidentId] = useState(null); const handleUpdateTrigger = () => { setUpdateTrigger((prev) => !prev); }; @@ -114,6 +117,17 @@ const Incidents2 = () => { setPage(0); }; + const handleOpenDetails = (id) => { + if (!id) return; + setSelectedIncidentId(id); + setIsDetailsOpen(true); + }; + + const handleCloseDetails = () => { + setIsDetailsOpen(false); + setSelectedIncidentId(null); + }; + return ( @@ -142,6 +156,13 @@ const Incidents2 = () => { handleChangeRowsPerPage={handleChangeRowsPerPage} resolveIncident={resolveIncident} handleUpdateTrigger={handleUpdateTrigger} + onOpenDetails={handleOpenDetails} + /> + ); diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 38c29fa4a..ad74b692e 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -29,10 +29,8 @@ import InfrastructureDetails from "../Pages/Infrastructure/Details/index.jsx"; import ServerUnreachable from "../Pages/ServerUnreachable.jsx"; // Incidents -import Checks from "../Pages/v1/Incidents"; +import Checks from "../Pages/Incidents/index.jsx"; import Incidents2 from "../Pages/Incidents2/index.jsx"; -import IncidentDetails from "../Pages/v1/Incidents2/Details"; - // Status pages import CreateStatus from "../Pages/StatusPage/Create/index.jsx"; @@ -136,10 +134,6 @@ const Routes = () => { path="checks/:monitorId?" element={} /> - } - /> } diff --git a/client/src/locales/en.json b/client/src/locales/en.json index ad68322ff..c543eb7e5 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -247,6 +247,7 @@ "addMonitors": "Add monitors", "window": "window", "cancel": "Cancel", + "close": "Close", "message": "Message", "low": "low", "high": "high", @@ -1203,6 +1204,23 @@ "hours": "hours", "loading": "Loading incident details...", "incidentNotFound": "Incident not found", - "incidentDetails": "Incident Details" + "incidentDetails": "INCIDENT DETAILS", + "activeIncident": "ACTIVE INCIDENT", + "resolvedIncident": "RESOLVED INCIDENT", + "overview": "OVERVIEW", + "URL": "URL", + "incidentAnalysis": "INCIDENT ANALYSIS", + "timeline": "Timeline", + "startedAt": "Started at", + "endedAt": "Ended at", + "downtime": "Downtime", + "errorDetails": "Error details", + "resolutionDetails": "Resolution details", + "statusCode": "Status code", + "resolutionMethod": "Resolution method", + "resolvedBy": "Resolved by", + "comment": "Comment", + "noCommentProvided": "No comment provided", + "loadingIncidentDetails": "Loading incident details..." } } From c6ac48b4ffad9f4e8abd037fc51a29f56055bbfc Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 22:50:34 +0000 Subject: [PATCH 305/400] rateLimiter -> ts, remove resonse handler --- server/src/app.js | 4 -- .../v1/{rateLimiter.js => rateLimiter.ts} | 4 +- server/src/middleware/v1/responseHandler.js | 66 ------------------- 3 files changed, 2 insertions(+), 72 deletions(-) rename server/src/middleware/v1/{rateLimiter.js => rateLimiter.ts} (86%) delete mode 100755 server/src/middleware/v1/responseHandler.js diff --git a/server/src/app.js b/server/src/app.js index d5978a465..b214af53a 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -1,6 +1,5 @@ import express from "express"; import path from "path"; -import { responseHandler } from "./middleware/v1/responseHandler.js"; import cors from "cors"; import helmet from "helmet"; import compression from "compression"; @@ -19,9 +18,6 @@ export const createApp = ({ services, controllers, envSettings, frontendPath, op // Static files app.use(express.static(frontendPath)); - // Response handler - app.use(responseHandler); - app.use( cors({ origin: allowedOrigin, diff --git a/server/src/middleware/v1/rateLimiter.js b/server/src/middleware/v1/rateLimiter.ts similarity index 86% rename from server/src/middleware/v1/rateLimiter.js rename to server/src/middleware/v1/rateLimiter.ts index 70b080d23..99655149d 100644 --- a/server/src/middleware/v1/rateLimiter.js +++ b/server/src/middleware/v1/rateLimiter.ts @@ -1,7 +1,7 @@ import rateLimit from "express-rate-limit"; export const generalApiLimiter = rateLimit({ - window: 60 * 1000, + windowMs: 60 * 1000, limit: 600, standardHeaders: true, legacyHeaders: false, @@ -9,7 +9,7 @@ export const generalApiLimiter = rateLimit({ }); export const authApiLimiter = rateLimit({ - window: 60 * 1000, + windowMs: 60 * 1000, limit: 15, standardHeaders: true, legacyHeaders: false, diff --git a/server/src/middleware/v1/responseHandler.js b/server/src/middleware/v1/responseHandler.js deleted file mode 100755 index 5169e7160..000000000 --- a/server/src/middleware/v1/responseHandler.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Middleware that adds standardized response methods to the Express response object. - * This allows for consistent API responses throughout the application. - * - * @param {import('express').Request} req - Express request object - * @param {import('express').Response} res - Express response object - * @param {import('express').NextFunction} next - Express next middleware function - */ -const responseHandler = (req, res, next) => { - /** - * Sends a standardized success response - * - * @param {Object} options - Success response options - * @param {number} [options.status=200] - HTTP status code - * @param {string} [options.msg="OK"] - Success message - * @param {*} [options.data=null] - Response data payload - * @returns {Object} Express response object - */ - res.success = ({ status = 200, msg = "OK", data = null, headers = {} }) => { - // Set custom headers if provided - Object.entries(headers).forEach(([key, value]) => { - res.set(key, value); - }); - - return res.status(status).json({ - success: true, - msg: msg, - data: data, - }); - }; - - /** - * Sends a standardized error response - * - * @param {Object} options - Error response options - * @param {number} [options.status=500] - HTTP status code - * @param {string} [options.msg="Internal server error"] - Error message - * @param {*} [options.data=null] - Additional error data (if any) - * @returns {Object} Express response object - */ - res.error = ({ status = 500, msg = "Internal server error", data = null }) => { - return res.status(status).json({ - success: false, - msg, - data, - }); - }; - - /** - * Sends a raw file response (for CSV, PDF, etc.) - * @param {Object} options - * @param {Buffer|string} options.data - The file content - * @param {Object} options.headers - Headers to set (e.g. Content-Type, Content-Disposition) - * @param {number} [options.status=200] - HTTP status code - */ - res.file = ({ data, headers = {}, status = 200 }) => { - Object.entries(headers).forEach(([key, value]) => { - res.setHeader(key, value); - }); - return res.status(status).send(data); - }; - - next(); -}; - -export { responseHandler }; From cc9850c9752a229362b18ac93a8578efee615737 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 22:56:14 +0000 Subject: [PATCH 306/400] logger->ts --- .../v1/{handleErrors.js => handleErrors.ts} | 7 +- server/src/utils/{logger.js => logger.ts} | 106 +++++++++--------- 2 files changed, 57 insertions(+), 56 deletions(-) rename server/src/middleware/v1/{handleErrors.js => handleErrors.ts} (57%) rename server/src/utils/{logger.js => logger.ts} (50%) diff --git a/server/src/middleware/v1/handleErrors.js b/server/src/middleware/v1/handleErrors.ts similarity index 57% rename from server/src/middleware/v1/handleErrors.js rename to server/src/middleware/v1/handleErrors.ts index 6be1b08ac..1fffc9a8c 100755 --- a/server/src/middleware/v1/handleErrors.js +++ b/server/src/middleware/v1/handleErrors.ts @@ -1,6 +1,7 @@ -import { logger } from "../../utils/logger.js"; +import type { NextFunction, Request, Response } from "express"; +import { logger } from "@/utils/logger.js"; -const handleErrors = (error, req, res, next) => { +const handleErrors = (error: any, req: Request, res: Response, next: NextFunction) => { const status = error.status || 500; const message = error.message || "Server error"; const service = error.service || "unknownService"; @@ -10,7 +11,7 @@ const handleErrors = (error, req, res, next) => { method: error.method, stack: error.stack, }); - res.error({ + res.status(status).json({ status, msg: message, }); diff --git a/server/src/utils/logger.js b/server/src/utils/logger.ts similarity index 50% rename from server/src/utils/logger.js rename to server/src/utils/logger.ts index 0917862fa..ac1cbbf7c 100755 --- a/server/src/utils/logger.js +++ b/server/src/utils/logger.ts @@ -1,46 +1,76 @@ -import { createLogger, format, transports } from "winston"; +import { createLogger, format, transports, Logger as WinstonLogger } from "winston"; +import type { Logform } from "winston"; import dotenv from "dotenv"; dotenv.config(); const SERVICE_NAME = "Logger"; +interface LogConfig { + message: string; + service?: string; + method?: string; + details?: Record; + stack?: string; +} + +interface LogEntry extends LogConfig { + level: string; + timestamp: string; +} + +interface EnvSettings { + logLevel?: string; +} + class Logger { static SERVICE_NAME = SERVICE_NAME; - constructor({ envSettings }) { + + private logger: WinstonLogger; + private envSettings: EnvSettings; + private logCache: LogEntry[]; + private maxCacheSize: number; + + constructor({ envSettings }: { envSettings: EnvSettings }) { this.envSettings = envSettings; this.logCache = []; this.maxCacheSize = 1000; - const consoleFormat = format.printf(({ level, message, service, method, details, timestamp, stack }) => { - if (message instanceof Object) { - message = JSON.stringify(message, null, 2); + const consoleFormat = format.printf((info: Logform.TransformableInfo) => { + const { level, service, method, details, timestamp, stack } = info; + let message = info.message as string; + let formattedMessage: string = message; + let formattedDetails: string | undefined; + + if (typeof message === "object" && message !== null) { + formattedMessage = JSON.stringify(message, null, 2); } - if (details instanceof Object) { - details = JSON.stringify(details, null, 2); + if (typeof details === "object" && details !== null) { + formattedDetails = JSON.stringify(details, null, 2); } + let msg = `${timestamp} ${level}:`; service && (msg += ` [${service}]`); method && (msg += `(${method})`); - message && (msg += ` ${message}`); - details && (msg += ` (details: ${details})`); + formattedMessage && (msg += ` ${formattedMessage}`); + formattedDetails && (msg += ` (details: ${formattedDetails})`); - if (typeof stack !== "undefined") { + if (typeof stack === "string") { const stackTrace = stack - ?.split("\n") + .split("\n") .slice(1) // Remove first line (error message) - .map((line) => { + .map((line: string) => { const match = line.match(/at\s+(.+?)\s+\((.+?):(\d+):(\d+)\)/); if (match) { return { function: match[1], file: match[2], - line: parseInt(match[3]), - column: parseInt(match[4]), + line: parseInt(match[3] ?? "0", 10), + column: parseInt(match[4] ?? "0", 10), }; } return line.trim(); }); - stack && (msg += ` (stack: ${JSON.stringify(stackTrace, null, 2)})`); + msg += ` (stack: ${JSON.stringify(stackTrace, null, 2)})`; } return msg; @@ -67,62 +97,31 @@ class Logger { return Logger.SERVICE_NAME; } - /** - * Logs an informational message. - * @param {Object} config - The configuration object. - * @param {string} config.message - The message to log. - * @param {string} config.service - The service name. - * @param {string} config.method - The method name. - * @param {Object} config.details - Additional details. - */ - info(config) { + info(config: LogConfig) { const logEntry = this.buildLogEntry("info", config); this.cacheLog(logEntry); this.logger.info(logEntry); } - /** - * Logs a warning message. - * @param {Object} config - The configuration object. - * @param {string} config.message - The message to log. - * @param {string} config.service - The service name. - * @param {string} config.method - The method name. - * @param {Object} config.details - Additional details. - */ - warn(config) { + warn(config: LogConfig) { const logEntry = this.buildLogEntry("warn", config); this.cacheLog(logEntry); this.logger.warn(logEntry); } - /** - * Logs an error message. - * @param {Object} config - The configuration object. - * @param {string} config.message - The message to log. - * @param {string} config.service - The service name. - * @param {string} config.method - The method name. - * @param {Object} config.details - Additional details. - */ - error(config) { + error(config: LogConfig) { const logEntry = this.buildLogEntry("error", config); this.cacheLog(logEntry); this.logger.error(logEntry); } - /** - * Logs a debug message. - * @param {Object} config - The configuration object. - * @param {string} config.message - The message to log. - * @param {string} config.service - The service name. - * @param {string} config.method - The method name. - * @param {Object} config.details - Additional details. - */ - debug(config) { + + debug(config: LogConfig) { const logEntry = this.buildLogEntry("debug", config); this.cacheLog(logEntry); this.logger.debug(logEntry); } - cacheLog(entry) { + cacheLog(entry: LogEntry) { this.logCache.push(entry); if (this.logCache.length > this.maxCacheSize) { this.logCache.shift(); @@ -133,7 +132,7 @@ class Logger { return this.logCache; } - buildLogEntry(level, config) { + buildLogEntry(level: string, config: LogConfig): LogEntry { return { level, message: config.message, @@ -151,3 +150,4 @@ export default Logger; // Legacy logger const logger = new Logger({ envSettings: { logLevel: "debug" } }); export { logger }; +export type { LogConfig, LogEntry }; From 4957209307fa5ea3414c6dbea98a4ae54afee2a2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 23:06:23 +0000 Subject: [PATCH 307/400] purge string service --- server/src/app.js | 2 - server/src/config/services.ts | 18 +- server/src/db/modules/inviteModule.js | 9 +- server/src/db/modules/recoveryModule.js | 7 +- server/src/db/modules/statusPageModule.js | 9 +- server/src/db/modules/userModule.js | 9 +- server/src/locales/en.json | 185 ------- server/src/locales/en.json.bak | 147 ------ server/src/locales/tr.json | 146 ------ .../src/middleware/v1/languageMiddleware.js | 27 -- .../src/service/business/incidentService.ts | 5 +- .../business/maintenanceWindowService.ts | 18 +- server/src/service/business/monitorService.ts | 7 +- server/src/service/business/userService.ts | 26 +- .../service/infrastructure/networkService.ts | 22 +- server/src/service/system/stringService.js | 452 ------------------ 16 files changed, 41 insertions(+), 1048 deletions(-) delete mode 100755 server/src/locales/en.json delete mode 100755 server/src/locales/en.json.bak delete mode 100755 server/src/locales/tr.json delete mode 100755 server/src/middleware/v1/languageMiddleware.js delete mode 100755 server/src/service/system/stringService.js diff --git a/server/src/app.js b/server/src/app.js index b214af53a..cd47d36e7 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -4,7 +4,6 @@ import cors from "cors"; import helmet from "helmet"; import compression from "compression"; import cookieParser from "cookie-parser"; -import languageMiddleware from "./middleware/v1/languageMiddleware.js"; import swaggerUi from "swagger-ui-express"; import { handleErrors } from "./middleware/v1/handleErrors.js"; import { setupRoutes } from "./config/routes.js"; @@ -58,7 +57,6 @@ export const createApp = ({ services, controllers, envSettings, frontendPath, op }, }) ); - app.use(languageMiddleware(services.stringService, services.translationService, services.settingsService)); // Swagger UI app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec)); diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 3f4aae9bc..4e8ff66a3 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -1,5 +1,4 @@ import TranslationService from "../service/system/translationService.js"; -import StringService from "../service/system/stringService.js"; import MongoDB from "../db/MongoDB.js"; import NetworkService from "../service/infrastructure/networkService.js"; import EmailService from "../service/infrastructure/emailService.js"; @@ -99,7 +98,6 @@ export type InitializedSerivces = { //v1 settingsService: any; translationService: any; - stringService: any; db: any; networkService: any; emailService: any; @@ -142,15 +140,13 @@ export const initializeServices = async ({ const translationService = new TranslationService(logger); await translationService.initialize(); - const stringService = new StringService(translationService); - // Create DB - const inviteModule = new InviteModule({ InviteToken, crypto, stringService }); - const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService, AppSettings }); - const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); + const inviteModule = new InviteModule({ InviteToken, crypto }); + const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, AppSettings }); + const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean }); const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); const notificationModule = new NotificationModule({ Notification: NotificationModel, Monitor }); - const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService }); + const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto }); const settingsModule = new SettingsModule({ AppSettings }); const incidentModule = new IncidentModule({ logger, Incident, Monitor, User }); @@ -191,7 +187,6 @@ export const initializeServices = async ({ http, Docker, net, - stringService, settingsService, }); const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); @@ -201,7 +196,6 @@ export const initializeServices = async ({ db, logger, errorService, - stringService, incidentsRepository, }); @@ -257,7 +251,6 @@ export const initializeServices = async ({ emailService, settingsService, logger, - stringService, jwt, errorService, jobQueue: superSimpleQueue, @@ -278,13 +271,11 @@ export const initializeServices = async ({ const maintenanceWindowService = new MaintenanceWindowService({ db, settingsService, - stringService, errorService, monitorsRepository, }); const monitorService = new MonitorService({ jobQueue: superSimpleQueue, - stringService, emailService, papaparse, logger, @@ -300,7 +291,6 @@ export const initializeServices = async ({ //v1 settingsService, translationService, - stringService, db, networkService, emailService, diff --git a/server/src/db/modules/inviteModule.js b/server/src/db/modules/inviteModule.js index 444e51efd..e3438dc3b 100755 --- a/server/src/db/modules/inviteModule.js +++ b/server/src/db/modules/inviteModule.js @@ -1,10 +1,11 @@ +import { AppError } from "@/utils/AppError.js"; + const SERVICE_NAME = "inviteModule"; class InviteModule { - constructor({ InviteToken, crypto, stringService }) { + constructor({ InviteToken, crypto }) { this.InviteToken = InviteToken; this.crypto = crypto; - this.stringService = stringService; } requestInviteToken = async (userData) => { @@ -27,7 +28,7 @@ class InviteModule { token, }); if (invite === null) { - throw new Error(this.stringService.authInviteNotFound); + throw new AppError({ message: "Invite token not found", status: 404 }); } return invite; } catch (error) { @@ -42,7 +43,7 @@ class InviteModule { token, }); if (invite === null) { - throw new Error(this.stringService.authInviteNotFound); + throw new Error("Invite not found"); } return invite; } catch (error) { diff --git a/server/src/db/modules/recoveryModule.js b/server/src/db/modules/recoveryModule.js index cc22306a3..b2f9296a6 100755 --- a/server/src/db/modules/recoveryModule.js +++ b/server/src/db/modules/recoveryModule.js @@ -1,11 +1,10 @@ const SERVICE_NAME = "recoveryModule"; class RecoveryModule { - constructor({ User, RecoveryToken, crypto, stringService }) { + constructor({ User, RecoveryToken, crypto }) { this.User = User; this.RecoveryToken = RecoveryToken; this.crypto = crypto; - this.stringService = stringService; } requestRecoveryToken = async (email) => { @@ -32,7 +31,7 @@ class RecoveryModule { if (recoveryToken !== null) { return recoveryToken; } else { - throw new Error(this.stringService.dbTokenNotFound); + throw new Error("Recovery token not found"); } } catch (error) { error.service = SERVICE_NAME; @@ -50,7 +49,7 @@ class RecoveryModule { const user = await this.User.findOne({ email: recoveryToken.email }); if (user === null) { - throw new Error(this.stringService.dbUserNotFound); + throw new Error("User not found"); } const match = await user.comparePassword(newPassword); diff --git a/server/src/db/modules/statusPageModule.js b/server/src/db/modules/statusPageModule.js index 7c40877c7..78a632cb8 100755 --- a/server/src/db/modules/statusPageModule.js +++ b/server/src/db/modules/statusPageModule.js @@ -1,10 +1,9 @@ const SERVICE_NAME = "statusPageModule"; class StatusPageModule { - constructor({ StatusPage, NormalizeData, stringService, AppSettings }) { + constructor({ StatusPage, NormalizeData, AppSettings }) { this.StatusPage = StatusPage; this.NormalizeData = NormalizeData; - this.stringService = stringService; this.AppSettings = AppSettings; } @@ -27,7 +26,7 @@ class StatusPageModule { if (error?.code === 11000) { // Handle duplicate URL errors error.status = 400; - error.message = this.stringService.statusPageUrlNotUnique; + error.message = "Status page URL must be unique"; } error.service = SERVICE_NAME; error.method = "createStatusPage"; @@ -87,7 +86,7 @@ class StatusPageModule { try { const preliminaryStatusPage = await this.StatusPage.findOne({ url }); if (!preliminaryStatusPage) { - const error = new Error(this.stringService.statusPageNotFound); + const error = new Error("Status page not found"); error.status = 404; throw error; } @@ -249,7 +248,7 @@ class StatusPageModule { }, ]); if (!statusPageQuery.length) { - const error = new Error(this.stringService.statusPageNotFound); + const error = new Error("Status page not found"); error.status = 404; throw error; } diff --git a/server/src/db/modules/userModule.js b/server/src/db/modules/userModule.js index 83b3b15f9..0051702b6 100755 --- a/server/src/db/modules/userModule.js +++ b/server/src/db/modules/userModule.js @@ -2,12 +2,11 @@ const SERVICE_NAME = "userModule"; const DUPLICATE_KEY_CODE = 11000; // MongoDB error code for duplicate key class UserModule { - constructor({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }) { + constructor({ User, Team, GenerateAvatarImage, ParseBoolean }) { this.User = User; this.Team = Team; this.GenerateAvatarImage = GenerateAvatarImage; this.ParseBoolean = ParseBoolean; - this.stringService = stringService; } checkSuperadmin = async () => { @@ -53,7 +52,7 @@ class UserModule { return await this.User.findOne({ _id: newUser._id }).select("-password").select("-profileImage"); // .select() doesn't work with create, need to save then find } catch (error) { if (error.code === DUPLICATE_KEY_CODE) { - error.message = this.stringService.dbUserExists; + error.message = "User already exists"; } error.service = SERVICE_NAME; error.method = "insertUser"; @@ -66,7 +65,7 @@ class UserModule { // We can strip the hash before returning the user const user = await this.User.findOne({ email: email }).select("-profileImage"); if (!user) { - throw new Error(this.stringService.dbUserNotFound); + throw new Error("User not found"); } return user; } catch (error) { @@ -117,7 +116,7 @@ class UserModule { try { const deletedUser = await this.User.findByIdAndDelete(userId); if (!deletedUser) { - throw new Error(this.stringService.dbUserNotFound); + throw new Error("User not found"); } return deletedUser; } catch (error) { diff --git a/server/src/locales/en.json b/server/src/locales/en.json deleted file mode 100755 index 1fbb25f79..000000000 --- a/server/src/locales/en.json +++ /dev/null @@ -1,185 +0,0 @@ -{ - "dontHaveAccount": "Don't have account", - "email": "E-mail", - "forgotPassword": "Forgot Password", - "password": "password", - "signUp": "Sign up", - "submit": "Submit", - "title": "Title", - "continue": "Continue", - "enterEmail": "Enter your email", - "authLoginTitle": "Log In", - "authLoginEnterPassword": "Enter your password", - "commonPassword": "Password", - "commonBack": "Back", - "authForgotPasswordTitle": "Forgot password?", - "authForgotPasswordResetPassword": "Reset password", - "createPassword": "Create your password", - "createAPassword": "Create a password", - "authRegisterAlreadyHaveAccount": "Already have an account?", - "commonAppName": "BlueWave Uptime", - "authLoginEnterEmail": "Enter your email", - "authRegisterTitle": "Create an account", - "authRegisterStepOneTitle": "Create your account", - "authRegisterStepOneDescription": "Enter your details to get started", - "authRegisterStepTwoTitle": "Set up your profile", - "authRegisterStepTwoDescription": "Tell us more about yourself", - "authRegisterStepThreeTitle": "Almost done!", - "authRegisterStepThreeDescription": "Review your information", - "authForgotPasswordDescription": "No worries, we'll send you reset instructions.", - "authForgotPasswordSendInstructions": "Send instructions", - "authForgotPasswordBackTo": "Back to", - "authCheckEmailTitle": "Check your email", - "authCheckEmailDescription": "We sent a password reset link to {{email}}", - "authCheckEmailResendEmail": "Resend email", - "authCheckEmailBackTo": "Back to", - "goBackTo": "Go back to", - "authCheckEmailDidntReceiveEmail": "Didn't receive the email?", - "authCheckEmailClickToResend": "Click to resend", - "authSetNewPasswordTitle": "Set new password", - "authSetNewPasswordDescription": "Your new password must be different from previously used passwords.", - "authSetNewPasswordNewPassword": "New password", - "authSetNewPasswordConfirmPassword": "Confirm password", - "confirmPassword": "Confirm your password", - "authSetNewPasswordResetPassword": "Reset password", - "authSetNewPasswordBackTo": "Back to", - "authPasswordMustBeAtLeast": "Must be at least", - "authPasswordCharactersLong": "8 characters long", - "authPasswordMustContainAtLeast": "Must contain at least", - "authPasswordSpecialCharacter": "one special character", - "authPasswordOneNumber": "one number", - "authPasswordUpperCharacter": "one upper character", - "authPasswordLowerCharacter": "one lower character", - "authPasswordConfirmAndPassword": "Confirm password and password", - "authPasswordMustMatch": "must match", - "friendlyError": "Something went wrong...", - "unknownError": "An unknown error occurred", - "unauthorized": "Unauthorized access", - "authAdminExists": "Admin already exists", - "authInviteNotFound": "Invite not found", - "unknownService": "Unknown service", - "noAuthToken": "No auth token provided", - "invalidAuthToken": "Invalid auth token", - "expiredAuthToken": "Token expired", - "noRefreshToken": "No refresh token provided", - "invalidRefreshToken": "Invalid refresh token", - "expiredRefreshToken": "Refresh token expired", - "requestNewAccessToken": "Request new access token", - "invalidPayload": "Invalid payload", - "verifyOwnerNotFound": "Document not found", - "verifyOwnerUnauthorized": "Unauthorized access", - "insufficientPermissions": "Insufficient permissions", - "dbUserExists": "User already exists", - "dbUserNotFound": "User not found", - "dbTokenNotFound": "Token not found", - "dbResetPasswordBadMatch": "New password must be different from old password", - "dbFindMonitorById": "Monitor with id ${monitorId} not found", - "dbDeleteChecks": "No checks found for monitor with id ${monitorId}", - "authIncorrectPassword": "Incorrect password", - "authUnauthorized": "Unauthorized access", - "monitorGetById": "Monitor not found", - "monitorGetByUserId": "No monitors found for user", - "jobQueueWorkerClose": "Error closing worker", - "jobQueueDeleteJob": "Job not found in queue", - "jobQueueObliterate": "Error obliterating queue", - "pingCannotResolve": "No response", - "statusPageNotFound": "Status page not found", - "statusPageUrlNotUnique": "Status page url must be unique", - "dockerFail": "Failed to fetch Docker container information", - "dockerNotFound": "Docker container not found", - "portFail": "Failed to connect to port", - "alertCreate": "Alert created successfully", - "alertGetByUser": "Got alerts successfully", - "alertGetByMonitor": "Got alerts by Monitor successfully", - "alertGetById": "Got alert by Id successfully", - "alertEdit": "Alert edited successfully", - "alertDelete": "Alert deleted successfully", - "authCreateUser": "User created successfully", - "authLoginUser": "User logged in successfully", - "authLogoutUser": "User logged out successfully", - "authUpdateUser": "User updated successfully", - "authCreateRecoveryToken": "Recovery token created successfully", - "authVerifyRecoveryToken": "Recovery token verified successfully", - "authResetPassword": "Password reset successfully", - "authAdminCheck": "Admin check completed successfully", - "authDeleteUser": "User deleted successfully", - "authTokenRefreshed": "Auth token is refreshed", - "authGetAllUsers": "Got all users successfully", - "inviteIssued": "Invite sent successfully", - "inviteVerified": "Invite verified successfully", - "checkCreate": "Check created successfully", - "checkGet": "Got checks successfully", - "checkDelete": "Checks deleted successfully", - "checkUpdateTtl": "Checks TTL updated successfully", - "monitorGetAll": "Got all monitors successfully", - "monitorStatsById": "Got monitor stats by Id successfully", - "monitorGetByIdSuccess": "Got monitor by Id successfully", - "monitorGetByTeamId": "Got monitors by Team Id successfully", - "monitorGetByUserIdSuccess": "Got monitor for ${userId} successfully", - "monitorCreate": "Monitor created successfully", - "bulkMonitorsCreate": "Monitors created successfully", - "monitorDelete": "Monitor deleted successfully", - "monitorEdit": "Monitor edited successfully", - "monitorCertificate": "Got monitor certificate successfully", - "monitorDemoAdded": "Successfully added demo monitors", - "queueGetMetrics": "Got metrics successfully", - "queueGetJobs": "Got jobs successfully", - "queueAddJob": "Job added successfully", - "queueObliterate": "Queue obliterated", - "jobQueueDeleteJobSuccess": "Job removed successfully", - "jobQueuePauseJob": "Job paused successfully", - "jobQueueResumeJob": "Job resumed successfully", - "maintenanceWindowGetById": "Got Maintenance Window by Id successfully", - "maintenanceWindowCreate": "Maintenance Window created successfully", - "maintenanceWindowGetByTeam": "Got Maintenance Windows by Team successfully", - "maintenanceWindowDelete": "Maintenance Window deleted successfully", - "maintenanceWindowEdit": "Maintenance Window edited successfully", - "pingSuccess": "Success", - "getAppSettings": "Got app settings successfully", - "updateAppSettings": "Updated app settings successfully", - "statusPageByUrl": "Got status page by url successfully", - "statusPageCreate": "Status page created successfully", - "newTermsAdded": "New terms added to POEditor", - "dockerSuccess": "Docker container status fetched successfully", - "portSuccess": "Port connected successfully", - "monitorPause": "Monitor paused successfully", - "monitorResume": "Monitor resumed successfully", - "statusPageDelete": "Status page deleted successfully", - "statusPageUpdate": "Status page updated successfully", - "statusPageByTeamId": "Got status pages by team id successfully", - "httpNetworkError": "Network error", - "httpNotJson": "Response data is not json", - "httpExpectedValueFail": "Expected value did not match", - "httpJsonPathError": "Failed to parse json data", - "httpEmptyResult": "Result is empty", - "httpMatchSuccess": "Response data match successfully", - "httpMatchFail": "Failed to match response data", - "httpJsonPathFail": "JSONPath did not match expected value", - "webhookSendSuccess": "Webhook notification sent successfully", - "telegramRequiresBotTokenAndChatId": "Telegram notifications require both botToken and chatId", - "webhookUrlRequired": "Webhook URL is required", - "platformRequired": "Platform is required", - "testNotificationFailed": "Failed to send test notification", - "monitorUpAlert": "Uptime Alert: One of your monitors is back online.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: UP\n📟 Status Code: {code}\n\u200B\n", - "monitorDownAlert": "Downtime Alert: One of your monitors went offline.\n📌 Monitor: {monitorName}\n📅 Time: {time}\n⚠️ Status: DOWN\n📟 Status Code: {code}\n\u200B\n", - "sendTestEmail": "Test email sent successfully", - "errorForValidEmailAddress": "A valid recipient email address is required.", - "testEmailSubject": "Test E-mail from Checkmate", - "monitorSummaryByTeamId": "Got monitors and summary by team id successfully", - "monitorExportSuccess": "Monitors exported successfully", - "gameListSuccess": "Got game list successfully", - "groupsByTeamId": "Got groups by team id successfully", - "discordNotification": { - "monitor": "Monitor", - "status": "Status", - "statusCode": "Status Code", - "time": "Time", - "up": "UP", - "down": "DOWN", - "checkmate": "Checkmate", - "url": "URL", - "unknown": "Unknown", - "uptimeAlert": "Uptime Alert: Your monitor {monitorName} is back online", - "downtimeAlert": "Downtime Alert: Your monitor {monitorName} went offline" - } -} diff --git a/server/src/locales/en.json.bak b/server/src/locales/en.json.bak deleted file mode 100755 index eaef58eef..000000000 --- a/server/src/locales/en.json.bak +++ /dev/null @@ -1,147 +0,0 @@ -{ - "dontHaveAccount": "Don't have account", - "email": "E-mail", - "forgotPassword": "Forgot Password", - "password": "password", - "signUp": "Sign up", - "submit": "Submit", - "title": "Title", - "continue": "Continue", - "enterEmail": "Enter your email", - "authLoginTitle": "Log In", - "authLoginEnterPassword": "Enter your password", - "commonPassword": "Password", - "commonBack": "Back", - "authForgotPasswordTitle": "Forgot password?", - "authForgotPasswordResetPassword": "Reset password", - "createPassword": "Create your password", - "createAPassword": "Create a password", - "authRegisterAlreadyHaveAccount": "Already have an account?", - "commonAppName": "BlueWave Uptime", - "authLoginEnterEmail": "Enter your email", - "authRegisterTitle": "Create an account", - "authRegisterStepOneTitle": "Create your account", - "authRegisterStepOneDescription": "Enter your details to get started", - "authRegisterStepTwoTitle": "Set up your profile", - "authRegisterStepTwoDescription": "Tell us more about yourself", - "authRegisterStepThreeTitle": "Almost done!", - "authRegisterStepThreeDescription": "Review your information", - "authForgotPasswordDescription": "No worries, we'll send you reset instructions.", - "authForgotPasswordSendInstructions": "Send instructions", - "authForgotPasswordBackTo": "Back to", - "authCheckEmailTitle": "Check your email", - "authCheckEmailDescription": "We sent a password reset link to {{email}}", - "authCheckEmailResendEmail": "Resend email", - "authCheckEmailBackTo": "Back to", - "goBackTo": "Go back to", - "authCheckEmailDidntReceiveEmail": "Didn't receive the email?", - "authCheckEmailClickToResend": "Click to resend", - "authSetNewPasswordTitle": "Set new password", - "authSetNewPasswordDescription": "Your new password must be different from previously used passwords.", - "authSetNewPasswordNewPassword": "New password", - "authSetNewPasswordConfirmPassword": "Confirm password", - "confirmPassword": "Confirm your password", - "authSetNewPasswordResetPassword": "Reset password", - "authSetNewPasswordBackTo": "Back to", - "authPasswordMustBeAtLeast": "Must be at least", - "authPasswordCharactersLong": "8 characters long", - "authPasswordMustContainAtLeast": "Must contain at least", - "authPasswordSpecialCharacter": "one special character", - "authPasswordOneNumber": "one number", - "authPasswordUpperCharacter": "one upper character", - "authPasswordLowerCharacter": "one lower character", - "authPasswordConfirmAndPassword": "Confirm password and password", - "authPasswordMustMatch": "must match", - "friendlyError": "Something went wrong...", - "unknownError": "An unknown error occurred", - "unauthorized": "Unauthorized access", - "authAdminExists": "Admin already exists", - "authInviteNotFound": "Invite not found", - "unknownService": "Unknown service", - "noAuthToken": "No auth token provided", - "invalidAuthToken": "Invalid auth token", - "expiredAuthToken": "Token expired", - "noRefreshToken": "No refresh token provided", - "invalidRefreshToken": "Invalid refresh token", - "expiredRefreshToken": "Refresh token expired", - "requestNewAccessToken": "Request new access token", - "invalidPayload": "Invalid payload", - "verifyOwnerNotFound": "Document not found", - "verifyOwnerUnauthorized": "Unauthorized access", - "insufficientPermissions": "Insufficient permissions", - "dbUserExists": "User already exists", - "dbUserNotFound": "User not found", - "dbTokenNotFound": "Token not found", - "dbResetPasswordBadMatch": "New password must be different from old password", - "dbFindMonitorById": "Monitor with id ${monitorId} not found", - "dbDeleteChecks": "No checks found for monitor with id ${monitorId}", - "authIncorrectPassword": "Incorrect password", - "authUnauthorized": "Unauthorized access", - "monitorGetById": "Monitor not found", - "monitorGetByUserId": "No monitors found for user", - "jobQueueWorkerClose": "Error closing worker", - "jobQueueDeleteJob": "Job not found in queue", - "jobQueueObliterate": "Error obliterating queue", - "pingCannotResolve": "No response", - "statusPageNotFound": "Status page not found", - "statusPageUrlNotUnique": "Status page url must be unique", - "dockerFail": "Failed to fetch Docker container information", - "dockerNotFound": "Docker container not found", - "portFail": "Failed to connect to port", - "alertCreate": "Alert created successfully", - "alertGetByUser": "Got alerts successfully", - "alertGetByMonitor": "Got alerts by Monitor successfully", - "alertGetById": "Got alert by Id successfully", - "alertEdit": "Alert edited successfully", - "alertDelete": "Alert deleted successfully", - "authCreateUser": "User created successfully", - "authLoginUser": "User logged in successfully", - "authLogoutUser": "User logged out successfully", - "authUpdateUser": "User updated successfully", - "authCreateRecoveryToken": "Recovery token created successfully", - "authVerifyRecoveryToken": "Recovery token verified successfully", - "authResetPassword": "Password reset successfully", - "authAdminCheck": "Admin check completed successfully", - "authDeleteUser": "User deleted successfully", - "authTokenRefreshed": "Auth token is refreshed", - "authGetAllUsers": "Got all users successfully", - "inviteIssued": "Invite sent successfully", - "inviteVerified": "Invite verified successfully", - "checkCreate": "Check created successfully", - "checkGet": "Got checks successfully", - "checkDelete": "Checks deleted successfully", - "checkUpdateTtl": "Checks TTL updated successfully", - "monitorGetAll": "Got all monitors successfully", - "monitorStatsById": "Got monitor stats by Id successfully", - "monitorGetByIdSuccess": "Got monitor by Id successfully", - "monitorGetByTeamId": "Got monitors by Team Id successfully", - "monitorGetByUserIdSuccess": "Got monitor for ${userId} successfully", - "monitorCreate": "Monitor created successfully", - "monitorDelete": "Monitor deleted successfully", - "monitorEdit": "Monitor edited successfully", - "monitorCertificate": "Got monitor certificate successfully", - "monitorDemoAdded": "Successfully added demo monitors", - "queueGetMetrics": "Got metrics successfully", - "queueAddJob": "Job added successfully", - "queueObliterate": "Queue obliterated", - "jobQueueDeleteJobSuccess": "Job removed successfully", - "jobQueuePauseJob": "Job paused successfully", - "jobQueueResumeJob": "Job resumed successfully", - "maintenanceWindowGetById": "Got Maintenance Window by Id successfully", - "maintenanceWindowCreate": "Maintenance Window created successfully", - "maintenanceWindowGetByTeam": "Got Maintenance Windows by Team successfully", - "maintenanceWindowDelete": "Maintenance Window deleted successfully", - "maintenanceWindowEdit": "Maintenance Window edited successfully", - "pingSuccess": "Success", - "getAppSettings": "Got app settings successfully", - "updateAppSettings": "Updated app settings successfully", - "statusPageByUrl": "Got status page by url successfully", - "statusPageCreate": "Status page created successfully", - "newTermsAdded": "New terms added to POEditor", - "dockerSuccess": "Docker container status fetched successfully", - "portSuccess": "Port connected successfully", - "monitorPause": "Monitor paused successfully", - "monitorResume": "Monitor resumed successfully", - "statusPageDelete": "Status page deleted successfully", - "statusPageUpdate": "Status page updated successfully" -} diff --git a/server/src/locales/tr.json b/server/src/locales/tr.json deleted file mode 100755 index cc1ef4c64..000000000 --- a/server/src/locales/tr.json +++ /dev/null @@ -1,146 +0,0 @@ -{ - "dontHaveAccount": "Hesabınız yok mu", - "email": "E-posta", - "forgotPassword": "Parolamı Unuttum", - "password": "Parola", - "signUp": "Kayıt ol", - "submit": "Gönder", - "title": "Başlık", - "continue": "Devam et", - "enterEmail": "E-posta adresinizi girin", - "authLoginTitle": "Giriş Yap", - "authLoginEnterPassword": "Parolanızı girin", - "commonPassword": "Parola", - "commonBack": "Geri", - "authForgotPasswordTitle": "Parolanı mi unuttun?", - "authForgotPasswordResetPassword": "Parola sıfırla", - "createPassword": "Parolanızı oluşturun", - "createAPassword": "Bir parola oluşturun", - "authRegisterAlreadyHaveAccount": "Zaten hesabınız var mı?", - "commonAppName": "BlueWave Uptime", - "authLoginEnterEmail": "E-posta adresinizi girin", - "authRegisterTitle": "Hesap oluştur", - "authRegisterStepOneTitle": "Hesabınızı oluşturun", - "authRegisterStepOneDescription": "Başlamak için bilgilerinizi girin", - "authRegisterStepTwoTitle": "Profilinizi ayarlayın", - "authRegisterStepTwoDescription": "Kendiniz hakkında daha fazla bilgi verin", - "authRegisterStepThreeTitle": "Neredeyse bitti!", - "authRegisterStepThreeDescription": "Bilgilerinizi gözden geçirin", - "authForgotPasswordDescription": "Endişelenmeyin, size sıfırlama talimatlarını göndereceğiz.", - "authForgotPasswordSendInstructions": "Talimatları gönder", - "authForgotPasswordBackTo": "Geri dön", - "authCheckEmailTitle": "E-postanızı kontrol edin", - "authCheckEmailDescription": "{{email}} adresine bir şifre sıfırlama bağlantısı gönderdik", - "authCheckEmailResendEmail": "E-postayı yeniden gönder", - "authCheckEmailBackTo": "Geri dön", - "goBackTo": "Geri dön", - "authCheckEmailDidntReceiveEmail": "E-postayı almadınız mı?", - "authCheckEmailClickToResend": "Yeniden göndermek için tıklayın", - "authSetNewPasswordTitle": "Yeni şifre belirleyin", - "authSetNewPasswordDescription": "Yeni şifreniz, daha önce kullanılan şifrelerden farklı olmalıdır.", - "authSetNewPasswordNewPassword": "Yeni şifre", - "authSetNewPasswordConfirmPassword": "Parolayı onayla", - "confirmPassword": "Parolanızı onaylayın", - "authSetNewPasswordResetPassword": "Parolayı sıfırla", - "authSetNewPasswordBackTo": "Geri dön", - "authPasswordMustBeAtLeast": "En az", - "authPasswordCharactersLong": "8 karakter uzunluğunda olmalı", - "authPasswordMustContainAtLeast": "En az içermeli", - "authPasswordSpecialCharacter": "bir özel karakter", - "authPasswordOneNumber": "bir rakam", - "authPasswordUpperCharacter": "bir büyük harf", - "authPasswordLowerCharacter": "bir küçük harf", - "authPasswordConfirmAndPassword": "Onay şifresi ve şifre", - "authPasswordMustMatch": "eşleşmelidir", - "friendlyError": "Bir şeyler yanlış gitti...", - "unknownError": "Bilinmeyen bir hata oluştu", - "unauthorized": "Yetkisiz erişim", - "authAdminExists": "Yönetici zaten mevcut", - "authInviteNotFound": "Davet bulunamadı", - "unknownService": "Bilinmeyen servis", - "noAuthToken": "Kimlik doğrulama belirteci sağlanmadı", - "invalidAuthToken": "Geçersiz kimlik doğrulama belirteci", - "expiredAuthToken": "Belirteç süresi doldu", - "noRefreshToken": "Yenileme belirteci sağlanmadı", - "invalidRefreshToken": "Geçersiz yenileme belirteci", - "expiredRefreshToken": "Yenileme belirteci süresi doldu", - "requestNewAccessToken": "Yeni erişim belirteci isteyin", - "invalidPayload": "Geçersiz veri", - "verifyOwnerNotFound": "Belge bulunamadı", - "verifyOwnerUnauthorized": "Yetkisiz erişim", - "insufficientPermissions": "Yetersiz izinler", - "dbUserExists": "Kullanıcı zaten mevcut", - "dbUserNotFound": "Kullanıcı bulunamadı", - "dbTokenNotFound": "Belirteç bulunamadı", - "dbResetPasswordBadMatch": "Yeni şifre eski şifreden farklı olmalıdır", - "dbFindMonitorById": "${monitorId} kimlikli monitör bulunamadı", - "dbDeleteChecks": "${monitorId} kimlikli monitör için kontrol bulunamadı", - "authIncorrectPassword": "Geçersiz parola", - "authUnauthorized": "Yetkisiz erişim", - "monitorGetById": "Monitör bulunamadı", - "monitorGetByUserId": "Kullanıcı için monitör bulunamadı", - "jobQueueWorkerClose": "İşçi kapatılırken hata oluştu", - "jobQueueDeleteJob": "İş kuyrukta bulunamadı", - "jobQueueObliterate": "Kuyruk yok edilirken hata oluştu", - "pingCannotResolve": "Yanıt yok", - "statusPageNotFound": "Durum sayfası bulunamadı", - "statusPageUrlNotUnique": "Durum sayfası URL'si benzersiz olmalıdır", - "dockerFail": "Docker konteyner bilgisi alınamadı", - "dockerNotFound": "Docker konteyner bulunamadı", - "portFail": "Porta bağlanılamadı", - "alertCreate": "Uyarı başarıyla oluşturuldu", - "alertGetByUser": "Uyarılar başarıyla alındı", - "alertGetByMonitor": "Monitöre göre uyarılar başarıyla alındı", - "alertGetById": "Kimliğe göre uyarı başarıyla alındı", - "alertEdit": "Uyarı başarıyla düzenlendi", - "alertDelete": "Uyarı başarıyla silindi", - "authCreateUser": "Kullanıcı başarıyla oluşturuldu", - "authLoginUser": "Kullanıcı başarıyla giriş yaptı", - "authLogoutUser": "Kullanıcı başarıyla çıkış yaptı", - "authUpdateUser": "Kullanıcı başarıyla güncellendi", - "authCreateRecoveryToken": "Kurtarma belirteci başarıyla oluşturuldu", - "authVerifyRecoveryToken": "Kurtarma belirteci başarıyla doğrulandı", - "authResetPassword": "Şifre başarıyla sıfırlandı", - "authAdminCheck": "Yönetici kontrolü başarıyla tamamlandı", - "authDeleteUser": "Kullanıcı başarıyla silindi", - "authTokenRefreshed": "Kimlik doğrulama belirteci yenilendi", - "authGetAllUsers": "Tüm kullanıcılar başarıyla alındı", - "inviteIssued": "Davet başarıyla gönderildi", - "inviteVerified": "Davet başarıyla doğrulandı", - "checkCreate": "Kontrol başarıyla oluşturuldu", - "checkGet": "Kontroller başarıyla alındı", - "checkDelete": "Kontroller başarıyla silindi", - "checkUpdateTtl": "Kontrol TTL başarıyla güncellendi", - "monitorGetAll": "Tüm monitörler başarıyla alındı", - "monitorStatsById": "Kimliğe göre monitör istatistikleri başarıyla alındı", - "monitorGetByIdSuccess": "Kimliğe göre monitör başarıyla alındı", - "monitorGetByTeamId": "Takım kimliğine göre monitörler başarıyla alındı", - "monitorGetByUserIdSuccess": "${userId} için monitör başarıyla alındı", - "monitorCreate": "Monitör başarıyla oluşturuldu", - "monitorDelete": "Monitör başarıyla silindi", - "monitorEdit": "Monitör başarıyla düzenlendi", - "monitorCertificate": "Monitör sertifikası başarıyla alındı", - "monitorDemoAdded": "Demo monitörler başarıyla eklendi", - "queueGetMetrics": "Metrikler başarıyla alındı", - "queueAddJob": "İş başarıyla eklendi", - "queueObliterate": "Kuyruk yok edildi", - "jobQueueDeleteJobSuccess": "İş başarıyla kaldırıldı", - "jobQueuePauseJob": "İş başarıyla duraklatıldı", - "jobQueueResumeJob": "İş başarıyla devam ettirildi", - "maintenanceWindowGetById": "Kimliğe göre bakım penceresi başarıyla alındı", - "maintenanceWindowCreate": "Bakım penceresi başarıyla oluşturuldu", - "maintenanceWindowGetByTeam": "Takıma göre bakım pencereleri başarıyla alındı", - "maintenanceWindowDelete": "Bakım penceresi başarıyla silindi", - "maintenanceWindowEdit": "Bakım penceresi başarıyla düzenlendi", - "pingSuccess": "Başarılı", - "getAppSettings": "Uygulama ayarları başarıyla alındı", - "updateAppSettings": "Uygulama ayarları başarıyla güncellendi", - "statusPageByUrl": "URL'ye göre durum sayfası başarıyla alındı", - "statusPageCreate": "Durum sayfası başarıyla oluşturuldu", - "dockerSuccess": "Docker konteyner durumu başarıyla alındı", - "portSuccess": "Porta başarıyla bağlanıldı", - "newTermsAdded": "POEditor'a yeni terimler eklendi", - "monitorPause": "Monitör başarıyla duraklatıldı", - "monitorResume": "Monitör başarıyla devam ettirildi", - "monitorDelete2": "" -} diff --git a/server/src/middleware/v1/languageMiddleware.js b/server/src/middleware/v1/languageMiddleware.js deleted file mode 100755 index 819858a21..000000000 --- a/server/src/middleware/v1/languageMiddleware.js +++ /dev/null @@ -1,27 +0,0 @@ -import { logger } from "../../utils/logger.js"; - -const languageMiddleware = (stringService, translationService) => async (req, res, next) => { - try { - const acceptLanguage = req.headers["accept-language"] || "en"; - const language = acceptLanguage.split(",")[0].slice(0, 2).toLowerCase(); - - translationService.setLanguage(language); - stringService.setLanguage(language); - - next(); - } catch (error) { - logger.error({ - message: error.message, - service: "languageMiddleware", - }); - const acceptLanguage = req.headers["accept-language"] || "en"; - const language = acceptLanguage.split(",")[0].slice(0, 2).toLowerCase(); - - translationService.setLanguage(language); - stringService.setLanguage(language); - - next(); - } -}; - -export default languageMiddleware; diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index 82211bc4e..5ababc5b4 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -20,26 +20,23 @@ class IncidentService { private db: any; private logger: any; private errorService: any; - private stringService: any; + private: any; private incidentsRepository: IIncidentsRepository; constructor({ db, logger, errorService, - stringService, incidentsRepository, }: { db: any; logger: any; errorService: any; - stringService: any; incidentsRepository: IIncidentsRepository; }) { this.db = db; this.logger = logger; this.errorService = errorService; - this.stringService = stringService; this.incidentsRepository = incidentsRepository; } diff --git a/server/src/service/business/maintenanceWindowService.ts b/server/src/service/business/maintenanceWindowService.ts index bb82821cf..c1ba5b8d7 100644 --- a/server/src/service/business/maintenanceWindowService.ts +++ b/server/src/service/business/maintenanceWindowService.ts @@ -5,27 +5,11 @@ const SERVICE_NAME = "maintenanceWindowService"; class MaintenanceWindowService { static SERVICE_NAME = SERVICE_NAME; private db: any; - private settingsService: any; - private stringService: any; private errorService: any; private monitorsRepository: IMonitorsRepository; - constructor({ - db, - settingsService, - stringService, - errorService, - monitorsRepository, - }: { - db: any; - settingsService: any; - stringService: any; - errorService: any; - monitorsRepository: IMonitorsRepository; - }) { + constructor({ db, errorService, monitorsRepository }: { db: any; errorService: any; monitorsRepository: IMonitorsRepository }) { this.db = db; - this.settingsService = settingsService; - this.stringService = stringService; this.errorService = errorService; this.monitorsRepository = monitorsRepository; } diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index fee3a9ffc..e9e10de04 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -65,9 +65,7 @@ export interface IMonitorService { export class MonitorService implements IMonitorService { static SERVICE_NAME = SERVICE_NAME; - private db: any; private jobQueue: any; - private stringService: any; private emailService: any; private papaparse: any; private logger: any; @@ -80,7 +78,6 @@ export class MonitorService implements IMonitorService { constructor({ jobQueue, - stringService, emailService, papaparse, logger, @@ -92,7 +89,6 @@ export class MonitorService implements IMonitorService { statusPagesRepository, }: { jobQueue: any; - stringService: any; emailService: any; papaparse: any; logger: any; @@ -104,7 +100,6 @@ export class MonitorService implements IMonitorService { statusPagesRepository: IStatusPagesRepository; }) { this.jobQueue = jobQueue; - this.stringService = stringService; this.emailService = emailService; this.papaparse = papaparse; this.logger = logger; @@ -465,7 +460,7 @@ export class MonitorService implements IMonitorService { }; sendTestEmail = async ({ to }: { to: string }): Promise => { - const subject = this.stringService.testEmailSubject; + const subject = "Test email from Checkmate"; const context = { testName: "Monitoring System" }; const html = await this.emailService.buildEmail("testEmailTemplate", context); diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index 5f73508dd..f6f489b8e 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -3,6 +3,7 @@ import Team from "@/db/models/Team.js"; import type { User } from "@/types/index.js"; import bcrypt from "bcryptjs"; import { AppError } from "@/utils/AppError.js"; +import { ISuperSimpleQueue } from "../infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; const SERVICE_NAME = "userService"; @@ -12,10 +13,8 @@ class UserService { private emailService: any; private settingsService: any; private logger: any; - private stringService: any; private jwt: any; - private errorService: any; - private jobQueue: any; + private jobQueue: ISuperSimpleQueue; private crypto: any; private monitorsRepository: IMonitorsRepository; private usersRepository: IUsersRepository; @@ -28,9 +27,7 @@ class UserService { emailService, settingsService, logger, - stringService, jwt, - errorService, jobQueue, monitorsRepository, usersRepository, @@ -42,10 +39,9 @@ class UserService { emailService: any; settingsService: any; logger: any; - stringService: any; jwt: any; errorService: any; - jobQueue: any; + jobQueue: ISuperSimpleQueue; monitorsRepository: IMonitorsRepository; usersRepository: IUsersRepository; invitesRepository: IInvitesRepository; @@ -55,9 +51,7 @@ class UserService { this.emailService = emailService; this.settingsService = settingsService; this.logger = logger; - this.stringService = stringService; this.jwt = jwt; - this.errorService = errorService; this.jobQueue = jobQueue; this.crypto = crypto; this.monitorsRepository = monitorsRepository; @@ -144,7 +138,7 @@ class UserService { const match = await bcrypt.compare(password, user.password); if (match !== true) { - throw this.errorService.createAuthenticationError(this.stringService.authIncorrectPassword); + throw new AppError({ message: "Incorrect password", service: SERVICE_NAME, status: 401 }); } // Remove password from user object. Should this be abstracted to DB layer? @@ -173,7 +167,7 @@ class UserService { // If not a match, throw a 403 // 403 instead of 401 to avoid triggering axios interceptor if (!match) { - throw this.errorService.createAuthorizationError(this.stringService.authIncorrectPassword); + throw new AppError({ message: "Incorrect current password", service: SERVICE_NAME, status: 403 }); } // If a match, update the password updates.password = updates.newPassword; @@ -234,23 +228,23 @@ class UserService { deleteUser = async (user: User) => { const email = user?.email; if (!email) { - throw this.errorService.createBadRequestError("No email in request"); + throw new AppError({ message: "No email in request", service: SERVICE_NAME, method: "deleteUser", status: 400 }); } const teamId = user?.teamId; const userId = user?.id; if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); + throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteUser", status: 400 }); } if (!userId) { - throw this.errorService.createBadRequestError("No user ID in request"); + throw new AppError({ message: "No user ID in request", service: SERVICE_NAME, method: "deleteUser", status: 400 }); } const roles = user?.role; if (roles.includes("demo")) { - throw this.errorService.createBadRequestError("Demo user cannot be deleted"); + throw new AppError({ message: "Demo user cannot be deleted", service: SERVICE_NAME, method: "deleteUser", status: 400 }); } // 1. Find all the monitors associated with the team ID if superadmin @@ -262,7 +256,7 @@ class UserService { res?.length > 0 && (await Promise.all( res.map(async (monitor) => { - await this.jobQueue.deleteJob(monitor); + await this.jobQueue.deleteJob(monitor.id); }) )); } diff --git a/server/src/service/infrastructure/networkService.ts b/server/src/service/infrastructure/networkService.ts index e4e5972af..7db8fc6e2 100644 --- a/server/src/service/infrastructure/networkService.ts +++ b/server/src/service/infrastructure/networkService.ts @@ -53,10 +53,8 @@ class NetworkService implements INetworkService { private GameDig: any; private ping: any; private logger: any; - private http: any; private Docker: any; private net: any; - private stringService: any; private settingsService: any; private buildStatusResponse = ({ @@ -124,7 +122,6 @@ class NetworkService implements INetworkService { http, Docker, net, - stringService, settingsService, }: { axios: any; @@ -137,7 +134,6 @@ class NetworkService implements INetworkService { http: any; Docker: any; net: any; - stringService: any; settingsService: any; }) { this.TYPE_PING = "ping"; @@ -152,14 +148,12 @@ class NetworkService implements INetworkService { this.PING_ERROR = 5001; this.axios = axios; this.https = https; - this.http = http; this.jmespath = jmespath; this.GameDig = GameDig; this.ping = ping; this.logger = logger; this.Docker = Docker; this.net = net; - this.stringService = stringService; this.settingsService = settingsService; const cacheable = new CacheableLookup(); @@ -326,7 +320,7 @@ class NetworkService implements INetworkService { } else { httpResponse.code = 500; httpResponse.status = false; - httpResponse.message = this.stringService.httpExpectedValueFail; + httpResponse.message = "Expected value did not match"; return httpResponse; } } @@ -336,7 +330,7 @@ class NetworkService implements INetworkService { const isJson = contentType?.includes("application/json"); if (!isJson) { httpResponse.status = false; - httpResponse.message = this.stringService.httpNotJson; + httpResponse.message = "Response is not JSON"; return httpResponse; } try { @@ -353,7 +347,7 @@ class NetworkService implements INetworkService { } else { httpResponse.status = false; httpResponse.code = 500; - httpResponse.message = this.stringService.httpJsonPathFail; + httpResponse.message = "Expected value did not match"; httpResponse.extracted = extracted; return httpResponse; } @@ -365,14 +359,14 @@ class NetworkService implements INetworkService { } else { httpResponse.status = false; httpResponse.code = 500; - httpResponse.message = this.stringService.httpJsonPathFail; + httpResponse.message = "Expected value did not match"; httpResponse.extracted = extracted; return httpResponse; } } } catch { httpResponse.status = false; - httpResponse.message = this.stringService.httpJsonPathError; + httpResponse.message = "Error evaluating JSON path"; return httpResponse; } } @@ -486,7 +480,7 @@ class NetworkService implements INetworkService { dockerResponse.code = 404; dockerResponse.status = false; - dockerResponse.message = this.stringService.dockerNotFound; + dockerResponse.message = "Docker container not found"; return dockerResponse; } @@ -568,7 +562,7 @@ class NetworkService implements INetworkService { overrides: { code: 200, status: (response as { success?: boolean })?.success ?? false, - message: this.stringService.portSuccess, + message: "Port check successful", responseTime, }, }); @@ -576,7 +570,7 @@ class NetworkService implements INetworkService { if (error) { portResponse.code = this.NETWORK_ERROR; portResponse.status = false; - portResponse.message = this.stringService.portFail; + portResponse.message = "Port check failed"; return portResponse; } diff --git a/server/src/service/system/stringService.js b/server/src/service/system/stringService.js deleted file mode 100755 index 231550be7..000000000 --- a/server/src/service/system/stringService.js +++ /dev/null @@ -1,452 +0,0 @@ -const SERVICE_NAME = "StringService"; - -class StringService { - static SERVICE_NAME = SERVICE_NAME; - - constructor(translationService) { - if (StringService.instance) { - return StringService.instance; - } - - this.translationService = translationService; - this._language = "en"; // default language - StringService.instance = this; - } - - get serviceName() { - return StringService.SERVICE_NAME; - } - - setLanguage(language) { - this._language = language; - } - - get language() { - return this._language; - } - - // Auth Messages - get dontHaveAccount() { - return this.translationService.getTranslation("dontHaveAccount"); - } - - get email() { - return this.translationService.getTranslation("email"); - } - - get forgotPassword() { - return this.translationService.getTranslation("forgotPassword"); - } - - get password() { - return this.translationService.getTranslation("password"); - } - - get signUp() { - return this.translationService.getTranslation("signUp"); - } - - get submit() { - return this.translationService.getTranslation("submit"); - } - - get title() { - return this.translationService.getTranslation("title"); - } - - get continue() { - return this.translationService.getTranslation("continue"); - } - - get enterEmail() { - return this.translationService.getTranslation("enterEmail"); - } - - get authLoginTitle() { - return this.translationService.getTranslation("authLoginTitle"); - } - - get authLoginEnterPassword() { - return this.translationService.getTranslation("authLoginEnterPassword"); - } - - get commonPassword() { - return this.translationService.getTranslation("commonPassword"); - } - - get commonBack() { - return this.translationService.getTranslation("commonBack"); - } - - get authForgotPasswordTitle() { - return this.translationService.getTranslation("authForgotPasswordTitle"); - } - - get authForgotPasswordResetPassword() { - return this.translationService.getTranslation("authForgotPasswordResetPassword"); - } - - get createPassword() { - return this.translationService.getTranslation("createPassword"); - } - - get createAPassword() { - return this.translationService.getTranslation("createAPassword"); - } - - get authRegisterAlreadyHaveAccount() { - return this.translationService.getTranslation("authRegisterAlreadyHaveAccount"); - } - - get commonAppName() { - return this.translationService.getTranslation("commonAppName"); - } - - get authLoginEnterEmail() { - return this.translationService.getTranslation("authLoginEnterEmail"); - } - - get authRegisterTitle() { - return this.translationService.getTranslation("authRegisterTitle"); - } - - get monitorGetAll() { - return this.translationService.getTranslation("monitorGetAll"); - } - - get monitorGetById() { - return this.translationService.getTranslation("monitorGetById"); - } - - get monitorGetByIdSuccess() { - return this.translationService.getTranslation("monitorGetByIdSuccess"); - } - - get monitorCreate() { - return this.translationService.getTranslation("monitorCreate"); - } - - get bulkMonitorsCreate() { - return this.translationService.getTranslation("bulkMonitorsCreate"); - } - - get monitorEdit() { - return this.translationService.getTranslation("monitorEdit"); - } - - get monitorDelete() { - return this.translationService.getTranslation("monitorDelete"); - } - - get monitorPause() { - return this.translationService.getTranslation("monitorPause"); - } - - get monitorResume() { - return this.translationService.getTranslation("monitorResume"); - } - - get monitorDemoAdded() { - return this.translationService.getTranslation("monitorDemoAdded"); - } - - get monitorStatsById() { - return this.translationService.getTranslation("monitorStatsById"); - } - - get monitorCertificate() { - return this.translationService.getTranslation("monitorCertificate"); - } - - // Maintenance Window Messages - get maintenanceWindowCreate() { - return this.translationService.getTranslation("maintenanceWindowCreate"); - } - - get maintenanceWindowGetById() { - return this.translationService.getTranslation("maintenanceWindowGetById"); - } - - get maintenanceWindowGetByTeam() { - return this.translationService.getTranslation("maintenanceWindowGetByTeam"); - } - - get maintenanceWindowDelete() { - return this.translationService.getTranslation("maintenanceWindowDelete"); - } - - get maintenanceWindowEdit() { - return this.translationService.getTranslation("maintenanceWindowEdit"); - } - - // Webhook Messages - get webhookUnsupportedPlatform() { - return this.translationService.getTranslation("webhookUnsupportedPlatform"); - } - - get webhookSendError() { - return this.translationService.getTranslation("webhookSendError"); - } - - get webhookSendSuccess() { - return this.translationService.getTranslation("webhookSendSuccess"); - } - - get telegramRequiresBotTokenAndChatId() { - return this.translationService.getTranslation("telegramRequiresBotTokenAndChatId"); - } - - get webhookUrlRequired() { - return this.translationService.getTranslation("webhookUrlRequired"); - } - - get platformRequired() { - return this.translationService.getTranslation("platformRequired"); - } - - get testNotificationFailed() { - return this.translationService.getTranslation("testNotificationFailed"); - } - - get monitorUpAlert() { - return this.translationService.getTranslation("monitorUpAlert"); - } - - get monitorDownAlert() { - return this.translationService.getTranslation("monitorDownAlert"); - } - - get discordNotification() { - return this.translationService.getTranslation("discordNotification"); - } - - getWebhookUnsupportedPlatform(platform) { - return this.translationService.getTranslation("webhookUnsupportedPlatform").replace("{platform}", platform); - } - - getWebhookSendError(platform) { - return this.translationService.getTranslation("webhookSendError").replace("{platform}", platform); - } - - getMonitorStatus(name, status, url) { - const translationKey = status === true ? "monitorStatusUp" : "monitorStatusDown"; - return this.translationService.getTranslation(translationKey).replace("{name}", name).replace("{url}", url); - } - - // Error Messages - get unknownError() { - return this.translationService.getTranslation("unknownError"); - } - - get friendlyError() { - return this.translationService.getTranslation("friendlyError"); - } - - get authIncorrectPassword() { - return this.translationService.getTranslation("authIncorrectPassword"); - } - - get unauthorized() { - return this.translationService.getTranslation("unauthorized"); - } - - get authAdminExists() { - return this.translationService.getTranslation("authAdminExists"); - } - - get authInviteNotFound() { - return this.translationService.getTranslation("authInviteNotFound"); - } - - get unknownService() { - return this.translationService.getTranslation("unknownService"); - } - - get noAuthToken() { - return this.translationService.getTranslation("noAuthToken"); - } - - get invalidAuthToken() { - return this.translationService.getTranslation("invalidAuthToken"); - } - - get expiredAuthToken() { - return this.translationService.getTranslation("expiredAuthToken"); - } - - // Queue Messages - get queueGetMetrics() { - return this.translationService.getTranslation("queueGetMetrics"); - } - - get queueGetJobs() { - return this.translationService.getTranslation("queueGetJobs"); - } - - get queueAddJob() { - return this.translationService.getTranslation("queueAddJob"); - } - - get queueObliterate() { - return this.translationService.getTranslation("queueObliterate"); - } - - // Job Queue Messages - get jobQueueDeleteJobSuccess() { - return this.translationService.getTranslation("jobQueueDeleteJobSuccess"); - } - - get jobQueuePauseJob() { - return this.translationService.getTranslation("jobQueuePauseJob"); - } - - get jobQueueResumeJob() { - return this.translationService.getTranslation("jobQueueResumeJob"); - } - - // Status Page Messages - get statusPageByUrl() { - return this.translationService.getTranslation("statusPageByUrl"); - } - - get statusPageCreate() { - return this.translationService.getTranslation("statusPageCreate"); - } - - get statusPageDelete() { - return this.translationService.getTranslation("statusPageDelete"); - } - - get statusPageUpdate() { - return this.translationService.getTranslation("statusPageUpdate"); - } - - get statusPageNotFound() { - return this.translationService.getTranslation("statusPageNotFound"); - } - - get statusPageByTeamId() { - return this.translationService.getTranslation("statusPageByTeamId"); - } - - get statusPageUrlNotUnique() { - return this.translationService.getTranslation("statusPageUrlNotUnique"); - } - - // Docker Messages - get dockerFail() { - return this.translationService.getTranslation("dockerFail"); - } - - get dockerNotFound() { - return this.translationService.getTranslation("dockerNotFound"); - } - - get dockerSuccess() { - return this.translationService.getTranslation("dockerSuccess"); - } - - // Port Messages - get portFail() { - return this.translationService.getTranslation("portFail"); - } - - get portSuccess() { - return this.translationService.getTranslation("portSuccess"); - } - - // Alert Messages - get alertCreate() { - return this.translationService.getTranslation("alertCreate"); - } - - get alertGetByUser() { - return this.translationService.getTranslation("alertGetByUser"); - } - - get alertGetByMonitor() { - return this.translationService.getTranslation("alertGetByMonitor"); - } - - get alertGetById() { - return this.translationService.getTranslation("alertGetById"); - } - - get alertEdit() { - return this.translationService.getTranslation("alertEdit"); - } - - get alertDelete() { - return this.translationService.getTranslation("alertDelete"); - } - - getDeletedCount(count) { - return this.translationService.getTranslation("deletedCount").replace("{count}", count); - } - - get pingSuccess() { - return this.translationService.getTranslation("pingSuccess"); - } - - get getAppSettings() { - return this.translationService.getTranslation("getAppSettings"); - } - - get httpNetworkError() { - return this.translationService.getTranslation("httpNetworkError"); - } - - get httpNotJson() { - return this.translationService.getTranslation("httpNotJson"); - } - - get httpJsonPathError() { - return this.translationService.getTranslation("httpJsonPathError"); - } - - get httpEmptyResult() { - return this.translationService.getTranslation("httpEmptyResult"); - } - - get httpMatchSuccess() { - return this.translationService.getTranslation("httpMatchSuccess"); - } - - get httpMatchFail() { - return this.translationService.getTranslation("httpMatchFail"); - } - - get updateAppSettings() { - return this.translationService.getTranslation("updateAppSettings"); - } - - get insufficientPermissions() { - return this.translationService.getTranslation("insufficientPermissions"); - } - - getDbFindMonitorById(monitorId) { - return this.translationService.getTranslation("dbFindMonitorById").replace("${monitorId}", monitorId); - } - - get dbUserExists() { - return this.translationService.getTranslation("dbUserExists"); - } - - get testEmailSubject() { - return this.translationService.getTranslation("testEmailSubject"); - } - - get verifyOwnerNotFound() { - return this.translationService.getTranslation("verifyOwnerNotFound"); - } - - get verifyOwnerUnauthorized() { - return this.translationService.getTranslation("verifyOwnerUnauthorized"); - } - - get dbUserNotFound() { - return this.translationService.getTranslation("dbUserNotFound"); - } -} - -export default StringService; From 952c70fca445599c9a46062fc29862f64eb86c51 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 23:07:31 +0000 Subject: [PATCH 308/400] remove v1 dir --- server/src/app.js | 6 +++--- server/src/config/routes.ts | 4 ++-- server/src/middleware/{v1 => }/handleErrors.ts | 0 server/src/middleware/{v1 => }/isAllowed.ts | 0 server/src/middleware/{v1 => }/rateLimiter.ts | 0 server/src/middleware/{v1 => }/sanitization.ts | 0 server/src/middleware/{v1 => }/verifyJWT.ts | 0 server/src/routes/v1/authRoute.js | 2 +- server/src/routes/v1/checkRoute.js | 2 +- server/src/routes/v1/diagnosticRoute.js | 2 +- server/src/routes/v1/incidentRoute.js | 2 +- server/src/routes/v1/inviteRoute.js | 2 +- server/src/routes/v1/logRoutes.js | 2 +- server/src/routes/v1/monitorRoute.ts | 2 +- server/src/routes/v1/queueRoute.js | 2 +- server/src/routes/v1/settingsRoute.js | 2 +- 16 files changed, 14 insertions(+), 14 deletions(-) rename server/src/middleware/{v1 => }/handleErrors.ts (100%) rename server/src/middleware/{v1 => }/isAllowed.ts (100%) rename server/src/middleware/{v1 => }/rateLimiter.ts (100%) rename server/src/middleware/{v1 => }/sanitization.ts (100%) rename server/src/middleware/{v1 => }/verifyJWT.ts (100%) diff --git a/server/src/app.js b/server/src/app.js index cd47d36e7..c83726a49 100644 --- a/server/src/app.js +++ b/server/src/app.js @@ -5,10 +5,10 @@ import helmet from "helmet"; import compression from "compression"; import cookieParser from "cookie-parser"; import swaggerUi from "swagger-ui-express"; -import { handleErrors } from "./middleware/v1/handleErrors.js"; +import { handleErrors } from "./middleware/handleErrors.js"; import { setupRoutes } from "./config/routes.js"; -import { generalApiLimiter } from "./middleware/v1/rateLimiter.js"; -import { sanitizeBody, sanitizeQuery } from "./middleware/v1/sanitization.js"; +import { generalApiLimiter } from "./middleware/rateLimiter.js"; +import { sanitizeBody, sanitizeQuery } from "./middleware/sanitization.js"; export const createApp = ({ services, controllers, envSettings, frontendPath, openApiSpec }) => { const allowedOrigin = envSettings.clientHost; diff --git a/server/src/config/routes.ts b/server/src/config/routes.ts index 835480b56..024387c25 100644 --- a/server/src/config/routes.ts +++ b/server/src/config/routes.ts @@ -1,5 +1,5 @@ -import { createVerifyJWT } from "../middleware/v1/verifyJWT.js"; -import { authApiLimiter } from "../middleware/v1/rateLimiter.js"; +import { createVerifyJWT } from "../middleware/verifyJWT.js"; +import { authApiLimiter } from "../middleware/rateLimiter.js"; import AuthRoutes from "../routes/v1/authRoute.js"; import InviteRoutes from "../routes/v1/inviteRoute.js"; diff --git a/server/src/middleware/v1/handleErrors.ts b/server/src/middleware/handleErrors.ts similarity index 100% rename from server/src/middleware/v1/handleErrors.ts rename to server/src/middleware/handleErrors.ts diff --git a/server/src/middleware/v1/isAllowed.ts b/server/src/middleware/isAllowed.ts similarity index 100% rename from server/src/middleware/v1/isAllowed.ts rename to server/src/middleware/isAllowed.ts diff --git a/server/src/middleware/v1/rateLimiter.ts b/server/src/middleware/rateLimiter.ts similarity index 100% rename from server/src/middleware/v1/rateLimiter.ts rename to server/src/middleware/rateLimiter.ts diff --git a/server/src/middleware/v1/sanitization.ts b/server/src/middleware/sanitization.ts similarity index 100% rename from server/src/middleware/v1/sanitization.ts rename to server/src/middleware/sanitization.ts diff --git a/server/src/middleware/v1/verifyJWT.ts b/server/src/middleware/verifyJWT.ts similarity index 100% rename from server/src/middleware/v1/verifyJWT.ts rename to server/src/middleware/verifyJWT.ts diff --git a/server/src/routes/v1/authRoute.js b/server/src/routes/v1/authRoute.js index 0be1e4750..0eca250e5 100755 --- a/server/src/routes/v1/authRoute.js +++ b/server/src/routes/v1/authRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; import multer from "multer"; const upload = multer(); diff --git a/server/src/routes/v1/checkRoute.js b/server/src/routes/v1/checkRoute.js index 6565425d4..2c34049bb 100755 --- a/server/src/routes/v1/checkRoute.js +++ b/server/src/routes/v1/checkRoute.js @@ -1,6 +1,6 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; class CheckRoutes { constructor(checkController) { diff --git a/server/src/routes/v1/diagnosticRoute.js b/server/src/routes/v1/diagnosticRoute.js index d0d1be8a2..9ec470e53 100755 --- a/server/src/routes/v1/diagnosticRoute.js +++ b/server/src/routes/v1/diagnosticRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; class DiagnosticRoutes { constructor(diagnosticController, verifyJWT) { diff --git a/server/src/routes/v1/incidentRoute.js b/server/src/routes/v1/incidentRoute.js index 6825ce21e..f87ef17a7 100644 --- a/server/src/routes/v1/incidentRoute.js +++ b/server/src/routes/v1/incidentRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; class IncidentRoutes { constructor(incidentController) { diff --git a/server/src/routes/v1/inviteRoute.js b/server/src/routes/v1/inviteRoute.js index 6ef9671a3..44ec0495f 100755 --- a/server/src/routes/v1/inviteRoute.js +++ b/server/src/routes/v1/inviteRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; class InviteRoutes { constructor(inviteController, verifyJWT) { diff --git a/server/src/routes/v1/logRoutes.js b/server/src/routes/v1/logRoutes.js index 43da8b75a..feabe669a 100755 --- a/server/src/routes/v1/logRoutes.js +++ b/server/src/routes/v1/logRoutes.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; class LogRoutes { constructor(logController) { this.router = Router(); diff --git a/server/src/routes/v1/monitorRoute.ts b/server/src/routes/v1/monitorRoute.ts index 2e6db630d..4488a2c49 100755 --- a/server/src/routes/v1/monitorRoute.ts +++ b/server/src/routes/v1/monitorRoute.ts @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; import multer from "multer"; import { fetchMonitorCertificate } from "../../controllers/controllerUtils.js"; const upload = multer({ diff --git a/server/src/routes/v1/queueRoute.js b/server/src/routes/v1/queueRoute.js index c6cfa05da..59e1cb744 100755 --- a/server/src/routes/v1/queueRoute.js +++ b/server/src/routes/v1/queueRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; class QueueRoutes { constructor(queueController) { this.router = Router(); diff --git a/server/src/routes/v1/settingsRoute.js b/server/src/routes/v1/settingsRoute.js index e88186af2..76556e923 100755 --- a/server/src/routes/v1/settingsRoute.js +++ b/server/src/routes/v1/settingsRoute.js @@ -1,5 +1,5 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/v1/isAllowed.js"; +import { isAllowed } from "../../middleware/isAllowed.js"; class SettingsRoutes { constructor(settingsController) { From f436e15569a93dca768e0c9284de1a197ab2ec34 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 20 Jan 2026 23:10:08 +0000 Subject: [PATCH 309/400] fix dep --- server/src/config/services.ts | 1 - server/src/utils/cookieHelpers.js | 26 -------------------------- 2 files changed, 27 deletions(-) delete mode 100644 server/src/utils/cookieHelpers.js diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 4e8ff66a3..de4d5115f 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -270,7 +270,6 @@ export const initializeServices = async ({ }); const maintenanceWindowService = new MaintenanceWindowService({ db, - settingsService, errorService, monitorsRepository, }); diff --git a/server/src/utils/cookieHelpers.js b/server/src/utils/cookieHelpers.js deleted file mode 100644 index 7005b66ca..000000000 --- a/server/src/utils/cookieHelpers.js +++ /dev/null @@ -1,26 +0,0 @@ -/** - * Get standardized cookie options for authentication tokens - * @param {Object} options - Additional cookie options - * @returns {Object} Cookie options object - */ -export const getAuthCookieOptions = (options = {}) => { - return { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - maxAge: 2 * 60 * 60 * 1000, // 2 hours (matches JWT TTL) - ...options, - }; -}; - -/** - * Clear cookie options for authentication tokens - * @returns {Object} Cookie clear options object - */ -export const getClearAuthCookieOptions = () => { - return { - httpOnly: true, - secure: process.env.NODE_ENV === "production", - sameSite: "strict", - }; -}; From 63b4779961895714caece5b2ad9a0102c66a7143 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 21 Jan 2026 00:01:38 +0000 Subject: [PATCH 310/400] monitorSerivce return types --- .../monitors/IMonitorsRepository.ts | 4 +- .../monitors/MongoMonitorsRepository.ts | 4 +- server/src/service/business/monitorService.ts | 39 +- server/src/types/check.ts | 20 +- server/src/types/monitor.ts | 75 + .../src/utils/{dataUtils.js => dataUtils.ts} | 55 +- server/src/utils/demoMonitorsOld.json | 1271 ----------------- server/src/utils/roleUtils.js | 13 - server/src/validation/joi.js | 6 +- 9 files changed, 132 insertions(+), 1355 deletions(-) rename server/src/utils/{dataUtils.js => dataUtils.ts} (53%) delete mode 100755 server/src/utils/demoMonitorsOld.json delete mode 100644 server/src/utils/roleUtils.js diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 29ebe37be..f29cf9d15 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -1,4 +1,4 @@ -import { type MonitorType, type Monitor } from "@/types/index.js"; +import { type MonitorType, type Monitor, type MonitorsSummary } from "@/types/index.js"; export interface TeamQueryConfig { limit?: number; @@ -37,6 +37,6 @@ export interface IMonitorsRepository { findMonitorCountByTeamIdAndType(teamId: string, config: TeamQueryConfig): Promise; // other - findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise; + findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise; findGroupsByTeamId(teamId: string): Promise; } diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 878d6f7cf..4a659b1c2 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -1,6 +1,6 @@ import { MonitorModel } from "@/db/models/index.js"; import type { MonitorDocument } from "@/db/models/index.js"; -import type { Monitor } from "@/types/index.js"; +import type { Monitor, MonitorsSummary } from "@/types/index.js"; import mongoose, { type FilterQuery } from "mongoose"; import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js"; import { AppError } from "@/utils/AppError.js"; @@ -149,7 +149,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { findMonitorsSummaryByTeamId = async ( teamId: string, config?: SummaryConfig - ): Promise<{ totalMonitors: number; upMonitors: number; downMonitors: number; pausedMonitors: number }> => { + ): Promise => { const match: FilterQuery = { teamId: new mongoose.Types.ObjectId(teamId) }; if (config?.type !== undefined) { match.type = Array.isArray(config.type) ? { $in: config.type } : config.type; diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index e9e10de04..4972bdee7 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -1,13 +1,14 @@ import { createMonitorsBodyValidation } from "@/validation/joi.js"; import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js"; import { type Monitor } from "@/types/index.js"; -import type { MonitorType } from "@/types/monitor.js"; +import type { MonitorType, MonitorsWithChecksByTeamIdResult, UptimeDetailsResult, HardwareDetailsResult, PageSpeedDetailsResult } from "@/types/monitor.js"; import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository, IStatusPagesRepository } from "@/repositories/index.js"; import fs from "fs"; import { fileURLToPath } from "url"; import path from "path"; import { AppError } from "../infrastructure/errorService.js"; +import { ISuperSimpleQueue } from "../infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; const SERVICE_NAME = "MonitorService"; type DateRangeKey = "recent" | "day" | "week" | "month" | "all"; @@ -17,13 +18,13 @@ export interface IMonitorService { // create createMonitor(teamId: string, userId: string, body: Monitor): Promise; - createBulkMonitors(fileData: string, userId: string, teamId: string): Promise; - addDemoMonitors(args: { userId: string; teamId: string }): Promise; + createBulkMonitors(fileData: string, userId: string, teamId: string): Promise; + addDemoMonitors(args: { userId: string; teamId: string }): Promise; // read - getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise; - getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; - getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; + getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string; normalize?: boolean }): Promise; + getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; + getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; getMonitorById(args: { teamId: string; monitorId: string }): Promise; getMonitorsByTeamId(args: { teamId: string; @@ -34,7 +35,7 @@ export interface IMonitorService { filter?: string; field?: string; order?: "asc" | "desc"; - }): Promise; + }): Promise; getMonitorsWithChecksByTeamId(args: { teamId: string; limit?: number; @@ -45,7 +46,7 @@ export interface IMonitorService { field?: string; order?: "asc" | "desc"; explain?: boolean; - }): Promise<{ summary: any; count: number; monitors: any[] }>; + }): Promise; getAllGames(): any; getGroupsByTeamId(args: { teamId: string }): Promise; @@ -59,13 +60,13 @@ export interface IMonitorService { // other sendTestEmail(args: { to: string }): Promise; - exportMonitorsToJSON(args: { teamId: string }): Promise; + exportMonitorsToJSON(args: { teamId: string }): Promise; } export class MonitorService implements IMonitorService { static SERVICE_NAME = SERVICE_NAME; - private jobQueue: any; + private jobQueue: ISuperSimpleQueue; private emailService: any; private papaparse: any; private logger: any; @@ -88,7 +89,7 @@ export class MonitorService implements IMonitorService { monitorStatsRepository, statusPagesRepository, }: { - jobQueue: any; + jobQueue: ISuperSimpleQueue; emailService: any; papaparse: any; logger: any; @@ -149,10 +150,10 @@ export class MonitorService implements IMonitorService { this.jobQueue.addJob(monitor.id, monitor); }; - createBulkMonitors = async (fileData: string, userId: string, teamId: string): Promise => { + createBulkMonitors = async (fileData: string, userId: string, teamId: string): Promise => { const { parse } = this.papaparse; - return new Promise((resolve, reject) => { + return new Promise((resolve, reject) => { parse(fileData, { header: true, skipEmptyLines: true, @@ -241,7 +242,7 @@ export class MonitorService implements IMonitorService { monitorId: string; dateRange: string; normalize?: boolean; - }): Promise => { + }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); @@ -276,7 +277,7 @@ export class MonitorService implements IMonitorService { }; }; - getHardwareDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise => { + getHardwareDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); @@ -307,7 +308,7 @@ export class MonitorService implements IMonitorService { }; }; - getPageSpeedDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise => { + getPageSpeedDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); @@ -336,12 +337,12 @@ export class MonitorService implements IMonitorService { monitorStats, }; }; - getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { + getMonitorById = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); return monitor; }; - getMonitorsByTeamId = async ({ teamId, type, filter }: { teamId: string; type?: MonitorType | MonitorType[]; filter?: string }): Promise => { + getMonitorsByTeamId = async ({ teamId, type, filter }: { teamId: string; type?: MonitorType | MonitorType[]; filter?: string }): Promise => { const monitors = await this.monitorsRepository.findByTeamId(teamId, { type, filter, @@ -369,7 +370,7 @@ export class MonitorService implements IMonitorService { field?: string; order?: "asc" | "desc"; explain?: boolean; - }): Promise<{ summary: any; count: number; monitors: any[] }> => { + }): Promise => { const summary = await this.monitorsRepository.findMonitorsSummaryByTeamId(teamId); const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter }); const monitors = await this.monitorsRepository.findByTeamId(teamId, { diff --git a/server/src/types/check.ts b/server/src/types/check.ts index f8ee4f618..3195cc06a 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -156,11 +156,17 @@ export interface HardwareChecksResult { }>; } +export interface GroupedCheck { + _id: string; + avgResponseTime: number; + totalChecks: number; +} + export interface UptimeChecksResult { monitorType: Exclude; - groupedChecks: Array<{ _id: string; avgResponseTime: number; totalChecks: number }>; - groupedUpChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; - groupedDownChecks: Array<{ _id: string; totalChecks: number; avgResponseTime: number }>; + groupedChecks: GroupedCheck[]; + groupedUpChecks: GroupedCheck[]; + groupedDownChecks: GroupedCheck[]; uptimePercentage: number; avgResponseTime: number; } @@ -171,3 +177,11 @@ export interface ChecksSummary { downChecks: number; cannotResolveChecks: number; } + +export type NormalizedCheck = T & { + originalResponseTime: number; +}; + +export type NormalizedUptimeCheck = T & { + originalAvgResponseTime: number; +}; diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index dc3bd8c09..1d41a7333 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -44,3 +44,78 @@ export interface Monitor { createdAt: string; updatedAt: string; } + +export interface MonitorsSummary { + totalMonitors: number; + upMonitors: number; + downMonitors: number; + pausedMonitors: number; +} + +export interface MonitorWithChecks extends Monitor { + checks: import("./check.js").Check[]; +} + +export interface MonitorsWithChecksByTeamIdResult { + summary: MonitorsSummary | null; + count: number; + monitors: MonitorWithChecks[]; +} + +export interface UptimeDetailsResult { + monitorData: { + monitor: Monitor; + groupedChecks: import("./check.js").GroupedCheck[]; + groupedUpChecks: import("./check.js").GroupedCheck[]; + groupedDownChecks: import("./check.js").GroupedCheck[]; + groupedAvgResponseTime: number; + groupedUptimePercentage: number; + }; + monitorStats: import("./monitorStats.js").MonitorStats | null; +} + +export interface HardwareDetailsResult extends Monitor { + stats: { + aggregateData: { + latestCheck: import("./check.js").Check | null; + totalChecks: number; + }; + upChecks: { + totalChecks: number; + }; + checks: Array<{ + _id: string; + avgCpuUsage: number; + avgMemoryUsage: number; + avgTemperature: number[]; + disks: Array<{ + name: string; + readSpeed: number; + writeSpeed: number; + totalBytes: number; + freeBytes: number; + usagePercent: number; + }>; + net: Array<{ + name: string; + bytesSentPerSecond: number; + deltaBytesRecv: number; + deltaPacketsSent: number; + deltaPacketsRecv: number; + deltaErrIn: number; + deltaErrOut: number; + deltaDropIn: number; + deltaDropOut: number; + deltaFifoIn: number; + deltaFifoOut: number; + }>; + }>; + }; +} + +export interface PageSpeedDetailsResult { + monitor: Monitor & { + checks: import("./check.js").Check[]; + }; + monitorStats: import("./monitorStats.js").MonitorStats | null; +} diff --git a/server/src/utils/dataUtils.js b/server/src/utils/dataUtils.ts similarity index 53% rename from server/src/utils/dataUtils.js rename to server/src/utils/dataUtils.ts index e7fd334dd..fac7cecef 100755 --- a/server/src/utils/dataUtils.js +++ b/server/src/utils/dataUtils.ts @@ -1,35 +1,34 @@ -const calculatePercentile = (arr, percentile) => { +import type { Check, GroupedCheck, NormalizedCheck, NormalizedUptimeCheck } from "@/types/index.js"; + + +const calculatePercentile = (arr: Check[], percentile: number): number => { const sorted = arr.slice().sort((a, b) => a.responseTime - b.responseTime); const index = (percentile / 100) * (sorted.length - 1); const lower = Math.floor(index); const upper = lower + 1; const weight = index % 1; - if (upper >= sorted.length) return sorted[lower].responseTime; - return sorted[lower].responseTime * (1 - weight) + sorted[upper].responseTime * weight; + if (upper >= sorted.length) return sorted[lower]!.responseTime; + return sorted[lower]!.responseTime * (1 - weight) + sorted[upper]!.responseTime * weight; }; -const calculatePercentileUptimeDetails = (arr, percentile) => { +const calculatePercentileUptimeDetails = (arr: GroupedCheck[], percentile: number): number => { const sorted = arr.slice().sort((a, b) => a.avgResponseTime - b.avgResponseTime); const index = (percentile / 100) * (sorted.length - 1); const lower = Math.floor(index); const upper = lower + 1; const weight = index % 1; - if (upper >= sorted.length) return sorted[lower].avgResponseTime; - return sorted[lower].avgResponseTime * (1 - weight) + sorted[upper].avgResponseTime * weight; + if (upper >= sorted.length) return sorted[lower]!.avgResponseTime; + return sorted[lower]!.avgResponseTime * (1 - weight) + sorted[upper]!.avgResponseTime * weight; }; -const NormalizeData = (checks, rangeMin, rangeMax) => { +export const NormalizeData = (checks: T[], rangeMin: number, rangeMax: number): NormalizedCheck[] => { if (checks.length > 1) { - // Get the 5th and 95th percentile const min = calculatePercentile(checks, 0); const max = calculatePercentile(checks, 95); const normalizedChecks = checks.map((check) => { const originalResponseTime = check.responseTime; - // Normalize the response time between 1 and 100 let normalizedResponseTime = rangeMin + ((check.responseTime - min) * (rangeMax - rangeMin)) / (max - min); - // Put a floor on the response times so we don't have extreme outliers - // Better visuals normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime)); return { ...check, @@ -45,19 +44,16 @@ const NormalizeData = (checks, rangeMin, rangeMax) => { }); } }; -const NormalizeDataUptimeDetails = (checks, rangeMin, rangeMax) => { + +export const NormalizeDataUptimeDetails = (checks: T[], rangeMin: number, rangeMax: number): NormalizedUptimeCheck[] => { if (checks.length > 1) { - // Get the 5th and 95th percentile const min = calculatePercentileUptimeDetails(checks, 0); const max = calculatePercentileUptimeDetails(checks, 95); const normalizedChecks = checks.map((check) => { const originalResponseTime = check.avgResponseTime; - // Normalize the response time between 1 and 100 let normalizedResponseTime = rangeMin + ((check.avgResponseTime - min) * (rangeMax - rangeMin)) / (max - min); - // Put a floor on the response times so we don't have extreme outliers - // Better visuals normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime)); return { ...check, @@ -69,32 +65,7 @@ const NormalizeDataUptimeDetails = (checks, rangeMin, rangeMax) => { return normalizedChecks; } else { return checks.map((check) => { - return { ...check, originalResponseTime: check.responseTime }; + return { ...check, originalAvgResponseTime: check.avgResponseTime }; }); } }; - -const safelyParseFloat = (value, defaultValue = 0) => { - if (value === null || typeof value === "undefined") { - return defaultValue; - } - const stringValue = String(value).trim(); - - if (typeof value === "number" && !isNaN(value)) { - return value; - } - - if (stringValue === "") { - return defaultValue; - } - - const parsedValue = parseFloat(stringValue); - - if (isNaN(parsedValue) || !isFinite(parsedValue)) { - return defaultValue; - } - - return parsedValue; -}; - -export { safelyParseFloat, calculatePercentile, NormalizeData, calculatePercentileUptimeDetails, NormalizeDataUptimeDetails }; diff --git a/server/src/utils/demoMonitorsOld.json b/server/src/utils/demoMonitorsOld.json deleted file mode 100755 index ffce7d226..000000000 --- a/server/src/utils/demoMonitorsOld.json +++ /dev/null @@ -1,1271 +0,0 @@ -[ - { - "name": "0to255", - "url": "https://0to255.com" - }, - { - "name": "10015.io", - "url": "https://10015.io" - }, - { - "name": "3DIcons", - "url": "https://3dicons.co" - }, - { - "name": "About.me", - "url": "https://about.me" - }, - { - "name": "Alias", - "url": "https://alias.co" - }, - { - "name": "All About Berlin", - "url": "https://allaboutberlin.com" - }, - { - "name": "All Acronyms", - "url": "https://allacronyms.com" - }, - { - "name": "All You Can Read ", - "url": "https://allyoucanread.com" - }, - { - "name": "AllTrails", - "url": "https://alltrails.com" - }, - { - "name": "Anotepad", - "url": "https://anotepad.com" - }, - { - "name": "AnswerSocrates", - "url": "https://answersocrates.com" - }, - { - "name": "AnswerThePublic ", - "url": "https://answerthepublic.com" - }, - { - "name": "Apollo ", - "url": "https://apollo.io" - }, - { - "name": "ArrayList", - "url": "https://arraylist.org" - }, - { - "name": "Ask Difference", - "url": "https://askdifference.com" - }, - { - "name": "Audd.io", - "url": "https://audd.io" - }, - { - "name": "Audiocheck", - "url": "https://audiocheck.net" - }, - { - "name": "Audionautix", - "url": "https://audionautix.com" - }, - { - "name": "Authentic Jobs", - "url": "https://authenticjobs.com" - }, - { - "name": "Behind the Name", - "url": "https://behindthename.com" - }, - { - "name": "Bilim Terimleri", - "url": "https://terimler.org" - }, - { - "name": "BitBof", - "url": "https://bitbof.com" - }, - { - "name": "Blank Page", - "url": "https://blank.page" - }, - { - "name": "Bonanza", - "url": "https://bonanza.com" - }, - { - "name": "BookCrossing", - "url": "https://bookcrossing.com" - }, - { - "name": "Browse AI", - "url": "https://browse.ai" - }, - { - "name": "Bubbl.us", - "url": "https://bubbl.us" - }, - { - "name": "Business Model Toolbox", - "url": "https://bmtoolbox.net" - }, - { - "name": "ByClickDownloader", - "url": "https://byclickdownloader.com" - }, - { - "name": "Calligraphr", - "url": "https://calligraphr.com" - }, - { - "name": "CertificateClaim", - "url": "https://certificateclaim.com" - }, - { - "name": "Chosic", - "url": "https://chosic.com" - }, - { - "name": "ClipDrop", - "url": "https://clipdrop.co" - }, - { - "name": "CloudConvert", - "url": "https://cloudconvert.com" - }, - { - "name": "CodingFont", - "url": "https://codingfont.com" - }, - { - "name": "Color Hunt", - "url": "https://colorhunt.co" - }, - { - "name": "ColorHexa", - "url": "https://colorhexa.com" - }, - { - "name": "Conversion-Tool", - "url": "https://conversion-tool.com" - }, - { - "name": "Cool Startup Jobs", - "url": "https://coolstartupjobs.com" - }, - { - "name": "Coroflot", - "url": "https://coroflot.com" - }, - { - "name": "Corrupt-a-File", - "url": "https://corrupt-a-file.net" - }, - { - "name": "Couchsurfing", - "url": "https://couchsurfing.com" - }, - { - "name": "Countries Been", - "url": "https://countriesbeen.com" - }, - { - "name": "Country Code", - "url": "https://countrycode.org" - }, - { - "name": "Creately", - "url": "https://creately.com" - }, - { - "name": "Creately ", - "url": "https://creately.com" - }, - { - "name": "Crossfade.io", - "url": "https://crossfade.io" - }, - { - "name": "Crunchbase", - "url": "https://crunchbase.com" - }, - { - "name": "CVmkr", - "url": "https://cvwizard.com" - }, - { - "name": "Daily Remote", - "url": "https://dailyremote.com" - }, - { - "name": "David Li", - "url": "https://david.li" - }, - { - "name": "DemandHunt", - "url": "https://demandhunt.com" - }, - { - "name": "Designify", - "url": "https://designify.com" - }, - { - "name": "Diff Checker", - "url": "https://diffchecker.com" - }, - { - "name": "DifferenceBetween.info", - "url": "https://differencebetween.info" - }, - { - "name": "Digital Glossary", - "url": "https://digital-glossary.com" - }, - { - "name": "Dimensions", - "url": "https://dimensions.com" - }, - { - "name": "Discoverify Music", - "url": "https://discoverifymusic.com" - }, - { - "name": "discu.eu", - "url": "https://discu.eu" - }, - { - "name": "Do It Yourself", - "url": "https://doityourself.com" - }, - { - "name": "draw.io", - "url": "https://drawio.com" - }, - { - "name": "Drumeo", - "url": "https://drumeo.com" - }, - { - "name": "Dummies", - "url": "https://dummies.com" - }, - { - "name": "Easel.ly", - "url": "https://easel.ly" - }, - { - "name": "Educalingo", - "url": "https://educalingo.com" - }, - { - "name": "Emoji Combos", - "url": "https://emojicombos.com" - }, - { - "name": "EquityBee", - "url": "https://equitybee.com" - }, - { - "name": "EquityZen", - "url": "https://equityzen.com" - }, - { - "name": "Escape Room Tips", - "url": "https://escaperoomtips.com" - }, - { - "name": "Every Noise", - "url": "https://everynoise.com" - }, - { - "name": "Every Time Zone", - "url": "https://everytimezone.com" - }, - { - "name": "Excalideck", - "url": "https://excalideck.com" - }, - { - "name": "Excalidraw", - "url": "https://excalidraw.com" - }, - { - "name": "Extract pics", - "url": "https://extract.pics" - }, - { - "name": "EZGIF", - "url": "https://ezgif.com" - }, - { - "name": "FactSlides", - "url": "https://factslides.com" - }, - { - "name": "FIGR ", - "url": "https://figr.app" - }, - { - "name": "Fine Dictionary", - "url": "https://finedictionary.com" - }, - { - "name": "Fiverr", - "url": "https://fiverr.com" - }, - { - "name": "Fix It Club", - "url": "https://fixitclub.com" - }, - { - "name": "Flightradar24", - "url": "https://flightradar24.com" - }, - { - "name": "FlowCV ", - "url": "https://flowcv.com" - }, - { - "name": "Font Squirrel", - "url": "https://fontsquirrel.com" - }, - { - "name": "FontAwesome", - "url": "https://fontawesome.com" - }, - { - "name": "Fontello ", - "url": "https://fontello.com" - }, - { - "name": "Form to Chatbot", - "url": "https://formtochatbot.com" - }, - { - "name": "Founder Resources", - "url": "https://founderresources.io" - }, - { - "name": "Franz", - "url": "https://meetfranz.com" - }, - { - "name": "Fraze It", - "url": "https://fraze.it" - }, - { - "name": "Freecycle", - "url": "https://freecycle.org" - }, - { - "name": "FreeType", - "url": "https://freetype.org" - }, - { - "name": "FutureM", - "url": "https://futureme.org" - }, - { - "name": "Generated.Photos", - "url": "https://generated.photos" - }, - { - "name": "Get Human", - "url": "https://gethuman.com" - }, - { - "name": "Go Bento", - "url": "https://gobento.com" - }, - { - "name": "Good CV", - "url": "https://goodcv.com" - }, - { - "name": "Grammar Monster", - "url": "https://grammar-monster.com" - }, - { - "name": "Grammar Book", - "url": "https://grammarbook.com" - }, - { - "name": "Gummy Search", - "url": "https://gummysearch.com" - }, - { - "name": "Gumroad", - "url": "https://gumroad.com" - }, - { - "name": "HealthIcons", - "url": "https://healthicons.org" - }, - { - "name": "HexColor", - "url": "https://hexcolor.co" - }, - { - "name": "Hidden Life Radio", - "url": "https://hiddenliferadio.com" - }, - { - "name": "Hired", - "url": "https://lhh.com" - }, - { - "name": "Honey", - "url": "https://joinhoney.com" - }, - { - "name": "HowStuffWorks", - "url": "https://howstuffworks.com" - }, - { - "name": "HugeIcons Pro", - "url": "https://hugeicons.com" - }, - { - "name": "Humble Bundle", - "url": "https://humblebundle.com" - }, - { - "name": "I Have No TV", - "url": "https://ihavenotv.com" - }, - { - "name": "I Miss the Office", - "url": "https://imisstheoffice.eu" - }, - { - "name": "IcoMoon", - "url": "https://icomoon.io" - }, - { - "name": "Iconfinder", - "url": "https://iconfinder.com" - }, - { - "name": "Icon Packs", - "url": "https://iconpacks.net" - }, - { - "name": "Iconshock", - "url": "https://iconshock.com" - }, - { - "name": "Iconz Design", - "url": "https://iconz.design" - }, - { - "name": "iFixit", - "url": "https://ifixit.com" - }, - { - "name": "IFTTT", - "url": "https://ifttt.com" - }, - { - "name": "Illlustrations", - "url": "https://illlustrations.co" - }, - { - "name": "Illustration Kit", - "url": "https://illustrationkit.com" - }, - { - "name": "IMSDB", - "url": "https://imsdb.com" - }, - { - "name": "Incompetech", - "url": "https://incompetech.com" - }, - { - "name": "Incredibox", - "url": "https://incredibox.com" - }, - { - "name": "InnerBod", - "url": "https://innerbody.com" - }, - { - "name": "Instructables", - "url": "https://instructables.com" - }, - { - "name": "Integromat", - "url": "https://make.com" - }, - { - "name": "Investopedia", - "url": "https://investopedia.com" - }, - { - "name": "Japanese Wiki Corpus", - "url": "https://japanesewiki.com" - }, - { - "name": "Jitter.Video", - "url": "https://jitter.video" - }, - { - "name": "Jobspresso", - "url": "https://jobspresso.co" - }, - { - "name": "JPEG-Optimizer", - "url": "https://jpeg-optimizer.com" - }, - { - "name": "JS Remotely", - "url": "https://jsremotely.com" - }, - { - "name": "JScreenFix", - "url": "https://jscreenfix.com" - }, - { - "name": "JSON Resume", - "url": "https://jsonresume.io" - }, - { - "name": "Just Join", - "url": "https://justjoin.it" - }, - { - "name": "Just the Recipe", - "url": "https://justtherecipe.com" - }, - { - "name": "JustRemote", - "url": "https://justremote.co" - }, - { - "name": "JustWatch", - "url": "https://justwatch.com" - }, - { - "name": "Kanopy", - "url": "https://kanopy.com" - }, - { - "name": "Kassellabs", - "url": "https://kassellabs.io" - }, - { - "name": "Key Differences", - "url": "https://keydifferences.com" - }, - { - "name": "Keybase", - "url": "https://keybase.io" - }, - { - "name": "KeyValues", - "url": "https://keyvalues.com" - }, - { - "name": "KHInsider", - "url": "https://khinsider.com" - }, - { - "name": "Killed by Google", - "url": "https://killedbygoogle.com" - }, - { - "name": "Kimovil", - "url": "https://kimovil.com" - }, - { - "name": "Lalal.ai", - "url": "https://www.lalal.ai" - }, - { - "name": "Learn Anything", - "url": "https://learn-anything.xyz" - }, - { - "name": "LendingTree", - "url": "https://lendingtree.com" - }, - { - "name": "Lightyear.fm", - "url": "https://lightyear.fm" - }, - { - "name": "LittleSis", - "url": "https://littlesis.org" - }, - { - "name": "Looria", - "url": "https://looria.com" - }, - { - "name": "Lucidchart", - "url": "https://lucidchart.com" - }, - { - "name": "Lunar", - "url": "https://lunar.fyi" - }, - { - "name": "Manuals Lib", - "url": "https://manualslib.com" - }, - { - "name": "Map Crunch", - "url": "https://mapcrunch.com" - }, - { - "name": "Masterworks", - "url": "https://masterworks.com" - }, - { - "name": "MediaFire", - "url": "https://mediafire.com" - }, - { - "name": "Mixlr", - "url": "https://mixlr.com" - }, - { - "name": "Moises AI", - "url": "https://moises.ai" - }, - { - "name": "Money", - "url": "https://money.com" - }, - { - "name": "Mountain Project", - "url": "https://mountainproject.com" - }, - { - "name": "Movie Map", - "url": "https://movie-map.com" - }, - { - "name": "Movie Sounds", - "url": "https://movie-sounds.org" - }, - { - "name": "MP3Cut", - "url": "https://mp3cut.net" - }, - { - "name": "Murmel", - "url": "https://murmel.social" - }, - { - "name": "Muscle Wiki", - "url": "https://musclewiki.com" - }, - { - "name": "Music-Map", - "url": "https://music-map.com" - }, - { - "name": "MusicTheory.net", - "url": "https://musictheory.net" - }, - { - "name": "MyFonts", - "url": "https://myfonts.com" - }, - { - "name": "MyFridgeFood", - "url": "https://myfridgefood.com" - }, - { - "name": "Nameberry", - "url": "https://nameberry.com" - }, - { - "name": "Namechk", - "url": "https://namechk.com" - }, - { - "name": "Ncase", - "url": "https://ncase.me" - }, - { - "name": "News in Levels", - "url": "https://newsinlevels.com" - }, - { - "name": "Noisli", - "url": "https://noisli.com" - }, - { - "name": "Notes.io", - "url": "https://notes.io" - }, - { - "name": "Novoresume", - "url": "https://novoresume.com" - }, - { - "name": "Ocoya", - "url": "https://ocoya.com" - }, - { - "name": "Old Computers Museum", - "url": "https://oldcomputers.net" - }, - { - "name": "Online Tone Generator", - "url": "https://onlinetonegenerator.com" - }, - { - "name": "Online-Convert", - "url": "https://online-convert.com" - }, - { - "name": "OnlineConversion", - "url": "https://onlineconversion.com" - }, - { - "name": "Online OCR", - "url": "https://onlineocr.net" - }, - { - "name": "OpenWeatherMap", - "url": "https://openweathermap.org" - }, - { - "name": "OrgPad", - "url": "https://orgpad.com" - }, - { - "name": "Passport Index", - "url": "https://passportindex.org" - }, - { - "name": "PDF Candy", - "url": "https://pdfcandy.com" - }, - { - "name": "PDF2DOC", - "url": "https://pdf2doc.com" - }, - { - "name": "PDFescape", - "url": "https://pdfescape.com" - }, - { - "name": "PfpMaker", - "url": "https://pfpmaker.com" - }, - { - "name": "PIDGI Wiki ", - "url": "https://pidgi.net" - }, - { - "name": "PimEyes", - "url": "https://pimeyes.com" - }, - { - "name": "Pipl ", - "url": "https://pipl.com" - }, - { - "name": "PixelBazaar", - "url": "https://pixelbazaar.com" - }, - { - "name": "PixelPaper", - "url": "https://pixelpaper.io" - }, - { - "name": "Ponly", - "url": "https://ponly.com" - }, - { - "name": "PowerToFly", - "url": "https://powertofly.com" - }, - { - "name": "Pretzel Rocks", - "url": "https://pretzel.rocks" - }, - { - "name": "PrintIt", - "url": "https://printit.work" - }, - { - "name": "Prismatext", - "url": "https://prismatext.com" - }, - { - "name": "Puffin Maps", - "url": "https://puffinmaps.com" - }, - { - "name": "Puzzle Loop ", - "url": "https://puzzle-loop.com" - }, - { - "name": "QuoteMaster", - "url": "https://quotemaster.org" - }, - { - "name": "Radio Garden", - "url": "https://radio.garden" - }, - { - "name": "Radiooooo", - "url": "https://radiooooo.com" - }, - { - "name": "Radiosondy", - "url": "https://radiosondy.info" - }, - { - "name": "Rainy Mood", - "url": "https://rainymood.com" - }, - { - "name": "Random Street View", - "url": "https://randomstreetview.com" - }, - { - "name": "Rap4Ever", - "url": "https://rap4all.com" - }, - { - "name": "RareFilm", - "url": "https://rarefilm.net" - }, - { - "name": "Rattibha", - "url": "https://rattibha.com" - }, - { - "name": "Reddit List ", - "url": "https://redditlist.com" - }, - { - "name": "RedditSearch.io", - "url": "https://redditsearch.io" - }, - { - "name": "Reelgood", - "url": "https://reelgood.com" - }, - { - "name": "Reface", - "url": "https://reface.ai" - }, - { - "name": "Rejected.us", - "url": "https://rejected.us" - }, - { - "name": "Relanote", - "url": "https://relanote.com" - }, - { - "name": "Remote Leaf", - "url": "https://remoteleaf.com" - }, - { - "name": "Remote OK", - "url": "https://remoteok.com" - }, - { - "name": "Remote Starter Kit ", - "url": "https://remotestarterkit.com" - }, - { - "name": "Remote.co", - "url": "https://remote.co" - }, - { - "name": "Remote Base ", - "url": "https://remotebase.com" - }, - { - "name": "Remote Bear", - "url": "https://remotebear.io" - }, - { - "name": "Remove.bg", - "url": "https://remove.bg" - }, - { - "name": "Respresso", - "url": "https://respresso.io" - }, - { - "name": "Reveddit", - "url": "https://reveddit.com" - }, - { - "name": "Rhymer", - "url": "https://rhymer.com" - }, - { - "name": "RhymeZone", - "url": "https://rhymezone.com" - }, - { - "name": "Ribbet", - "url": "https://ribbet.com" - }, - { - "name": "Roadmap.sh", - "url": "https://roadmap.sh" - }, - { - "name": "Roadtrippers", - "url": "https://roadtrippers.com" - }, - { - "name": "RxResu.me", - "url": "https://rxresu.me" - }, - { - "name": "SchemeColor", - "url": "https://schemecolor.com" - }, - { - "name": "Screenshot.Guru", - "url": "https://screenshot.guru" - }, - { - "name": "SeatGuru", - "url": "https://seatguru.com" - }, - { - "name": "Sessions", - "url": "https://sessions.us" - }, - { - "name": "Shottr", - "url": "https://shottr.cc" - }, - { - "name": "Signature Maker", - "url": "https://signature-maker.net" - }, - { - "name": "Skip The Drive", - "url": "https://skipthedrive.com" - }, - { - "name": "Slowby", - "url": "https://slowby.travel" - }, - { - "name": "Small World", - "url": "https://smallworld.kiwi" - }, - { - "name": "SmallPDF", - "url": "https://smallpdf.com" - }, - { - "name": "Social Image Maker", - "url": "https://socialimagemaker.io" - }, - { - "name": "Social Sizes", - "url": "https://socialsizes.io" - }, - { - "name": "SoundLove", - "url": "https://soundlove.se" - }, - { - "name": "Spline", - "url": "https://spline.design" - }, - { - "name": "Starkey Comics", - "url": "https://starkeycomics.com" - }, - - { - "name": "Statista", - "url": "https://statista.com" - }, - { - "name": "Stolen Camera Finder", - "url": "https://stolencamerafinder.com" - }, - { - "name": "Strobe.Cool", - "url": "https://strobe.cool" - }, - { - "name": "Sumo", - "url": "https://sumo.app" - }, - { - "name": "SuperMeme AI", - "url": "https://supermeme.ai" - }, - { - "name": "Synthesia", - "url": "https://synthesia.io" - }, - { - "name": "TablerIcons", - "url": "https://tablericons.com" - }, - { - "name": "Tango", - "url": "https://tango.us" - }, - { - "name": "TasteDive", - "url": "https://tastedive.com" - }, - { - "name": "TechSpecs", - "url": "https://techspecs.io" - }, - { - "name": "Teoria", - "url": "https://teoria.com" - }, - { - "name": "Text Faces", - "url": "https://textfac.es" - }, - { - "name": "The Balance Money", - "url": "https://thebalancemoney.com" - }, - { - "name": "The Punctuation Guide", - "url": "https://thepunctuationguide.com" - }, - { - "name": "This to That", - "url": "https://thistothat.com" - }, - { - "name": "This vs That", - "url": "https://thisvsthat.io" - }, - { - "name": "ThreadReaderApp ", - "url": "https://threadreaderapp.com" - }, - { - "name": "Thumbly", - "url": "https://tokee.ai" - }, - { - "name": "Tiii.me", - "url": "https://tiii.me" - }, - { - "name": "TikTok Video Downloader", - "url": "https://ttvdl.com" - }, - { - "name": "Time and Date", - "url": "https://timeanddate.com" - }, - { - "name": "Time.is", - "url": "https://time.is" - }, - { - "name": "Title Case", - "url": "https://titlecase.com" - }, - { - "name": "Toaster Central", - "url": "https://toastercentral.com" - }, - { - "name": "Tongue-Twister ", - "url": "https://tongue-twister.net" - }, - { - "name": "TradingView", - "url": "https://tradingview.com" - }, - { - "name": "Transparent Textures", - "url": "https://transparenttextures.com" - }, - { - "name": "Tubi TV", - "url": "https://tubitv.com" - }, - { - "name": "Tunefind", - "url": "https://tunefind.com" - }, - { - "name": "TuneMyMusic", - "url": "https://tunemymusic.com" - }, - { - "name": "Tweepsmap", - "url": "https://fedica.com" - }, - { - "name": "Two Peas and Their Pod", - "url": "https://twopeasandtheirpod.com" - }, - { - "name": "Typatone", - "url": "https://typatone.com" - }, - { - "name": "Under Glass", - "url": "https://underglass.io" - }, - { - "name": "UniCorner", - "url": "https://unicorner.news" - }, - { - "name": "Unita", - "url": "https://unita.co" - }, - { - "name": "UnitConverters", - "url": "https://unitconverters.net" - }, - { - "name": "Unreadit", - "url": "https://unreadit.com" - }, - { - "name": "Unscreen", - "url": "https://unscreen.com" - }, - { - "name": "UnTools ", - "url": "https://untools.co" - }, - { - "name": "Upwork", - "url": "https://upwork.com" - }, - { - "name": "UTF8 Icons", - "url": "https://utf8icons.com" - }, - { - "name": "Vector Magic", - "url": "https://vectormagic.com" - }, - { - "name": "Virtual Vacation", - "url": "https://virtualvacation.us" - }, - { - "name": "Virtual Vocations", - "url": "https://virtualvocations.com" - }, - { - "name": "Visiwig", - "url": "https://visiwig.com" - }, - { - "name": "Visual CV", - "url": "https://visualcv.com" - }, - { - "name": "Vocus.io", - "url": "https://vocus.io" - }, - { - "name": "Voscreen", - "url": "https://voscreen.com" - }, - { - "name": "Wanderprep", - "url": "https://wanderprep.com" - }, - { - "name": "Warmshowers", - "url": "https:/warmshowers.org" - }, - { - "name": "Watch Documentaries", - "url": "https://watchdocumentaries.com" - }, - { - "name": "We Work Remotely", - "url": "https://weworkremotely.com" - }, - { - "name": "Web2PDFConvert", - "url": "https://web2pdfconvert.com" - }, - { - "name": "Welcome to My Garden", - "url": "https://welcometomygarden.org" - }, - { - "name": "When2meet ", - "url": "https://when2meet.com" - }, - { - "name": "Where's George", - "url": "https://wheresgeorge.com" - }, - { - "name": "Where's Willy", - "url": "https://whereswilly.com" - }, - { - "name": "WikiHow", - "url": "https://wikihow.com" - }, - { - "name": "Windy", - "url": "https://www.windy.com" - }, - { - "name": "WonderHowTo", - "url": "https://wonderhowto.com" - }, - { - "name": "Working Nomads", - "url": "https://workingnomads.com" - }, - { - "name": "Wormhole", - "url": "https://wormhole.app" - }, - { - "name": "Y Combinator Jobs", - "url": "https://ycombinator.com" - }, - { - "name": "Yes Promo", - "url": "https://yespromo.me" - }, - { - "name": "YouGlish", - "url": "https://youglish.com" - }, - { - "name": "Zamzar", - "url": "https://zamzar.com" - }, - { - "name": "Zippyshare", - "url": "https://zippyshare.com" - }, - { - "name": "Zoom Earth", - "url": "https://zoom.earth" - }, - { - "name": "Zoom.it", - "url": "https://zoom.it" - } -] diff --git a/server/src/utils/roleUtils.js b/server/src/utils/roleUtils.js deleted file mode 100644 index f359246b2..000000000 --- a/server/src/utils/roleUtils.js +++ /dev/null @@ -1,13 +0,0 @@ -export const ROLES = { - SUPERADMIN: "superadmin", - ADMIN: "admin", - USER: "user", - DEMO: "demo", -}; - -export const VALID_ROLES = [ROLES.ADMIN, ROLES.USER, ROLES.DEMO]; - -export const EDITABLE_ROLES = [ - { role: ROLES.ADMIN, _id: ROLES.ADMIN }, - { role: ROLES.USER, _id: ROLES.USER }, -]; diff --git a/server/src/validation/joi.js b/server/src/validation/joi.js index 8533b4e44..474feebe5 100755 --- a/server/src/validation/joi.js +++ b/server/src/validation/joi.js @@ -1,5 +1,5 @@ import joi from "joi"; -import { ROLES, VALID_ROLES } from "../utils/roleUtils.js"; +import { UserRoles } from "@/types/user.js"; //**************************************** // Custom Validators @@ -689,7 +689,7 @@ const editUserByIdBodyValidation = joi.object({ email: joi.string().email().required(), role: joi .array() - .items(joi.string().valid(...VALID_ROLES)) + .items(joi.string().valid(...UserRoles)) .min(1) .required(), }); @@ -700,7 +700,7 @@ const editSuperadminUserByIdBodyValidation = joi.object({ email: joi.string().email().required(), role: joi .array() - .items(joi.string().valid(...VALID_ROLES, ROLES.SUPERADMIN)) + .items(joi.string().valid(...UserRoles)) .min(1) .required(), }); From 63416ceb4bc85b4b577021f2556832a0631b6b49 Mon Sep 17 00:00:00 2001 From: karenvicent Date: Tue, 20 Jan 2026 20:58:35 -0500 Subject: [PATCH 311/400] fix import routes --- .../Components/IncidentDetailsModal/index.jsx | 6 +- .../IncidentTable/IncidentActionsMenu.jsx | 4 +- .../Components/IncidentTable/index.jsx | 2 +- .../ResolveIncidentDialog/index.jsx | 105 ++++++++++++++++++ client/src/Pages/Incidents/index.jsx | 2 +- 5 files changed, 112 insertions(+), 7 deletions(-) create mode 100644 client/src/Pages/Incidents/Components/ResolveIncidentDialog/index.jsx diff --git a/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx index 25af835b9..1290c69ff 100644 --- a/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx +++ b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx @@ -2,7 +2,7 @@ import { Stack, Typography, Grid, Box, Button, Divider } from "@mui/material"; import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import SummaryCard from "@/Pages/Incidents2/Components/SummaryCard"; +import SummaryCard from "../SummaryCard/index.jsx"; import ResolveIncidentDialog from "../ResolveIncidentDialog/index.jsx"; // Utils @@ -14,8 +14,8 @@ import { useSelector } from "react-redux"; import PropTypes from "prop-types"; // Hooks -import useFetchIncidents from "@/Pages/Incidents2/hooks/useFetchIncidents"; -import useGetIncidentsDuration from "@/Pages/Incidents2/hooks/useGetIncidentsDuration"; +import useFetchIncidents from "../../hooks/useFetchIncidents.js"; +import useGetIncidentsDuration from "../../hooks/useGetIncidentsDuration.js"; const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => { const theme = useTheme(); diff --git a/client/src/Pages/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx b/client/src/Pages/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx index 2902da049..1e9a3be85 100644 --- a/client/src/Pages/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx +++ b/client/src/Pages/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx @@ -2,7 +2,7 @@ import IconButton from "@mui/material/IconButton"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; -import Settings from "../../../../assets/icons/settings-bold.svg?react"; +//import Settings from "../../../../assets/icons/settings-bold.svg?react"; import ResolveIncidentDialog from "../ResolveIncidentDialog/index.jsx"; import useFetchIncidents from "../../hooks/useFetchIncidents"; @@ -94,7 +94,7 @@ const IncidentActionsMenu = ({ incident, onResolve, onOpenDetails }) => { }, }} > - + {/* */}M { const uiTimezone = useSelector((state) => state.ui.timezone); diff --git a/client/src/Pages/Incidents/Components/ResolveIncidentDialog/index.jsx b/client/src/Pages/Incidents/Components/ResolveIncidentDialog/index.jsx new file mode 100644 index 000000000..e2be5fed2 --- /dev/null +++ b/client/src/Pages/Incidents/Components/ResolveIncidentDialog/index.jsx @@ -0,0 +1,105 @@ +import { useEffect, useState } from "react"; +import PropTypes from "prop-types"; +import { Button, Stack } from "@mui/material"; +import { useTranslation } from "react-i18next"; +import { useTheme } from "@emotion/react"; +import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; +import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; + +const ResolveIncidentDialog = ({ + open, + incidentId, + onClose, + onResolve, + onAfterResolve, + onResolved, + isLoading, +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const [comment, setComment] = useState(""); + const [isSubmitting, setIsSubmitting] = useState(false); + + useEffect(() => { + if (open) { + setComment(""); + } + }, [open]); + + const handleConfirm = async () => { + if (!incidentId) return; + const trimmed = comment.trim(); + try { + setIsSubmitting(true); + await onResolve(incidentId, trimmed ? { comment: trimmed } : {}); + if (onAfterResolve) { + await onAfterResolve(); + } + onResolved?.(); + onClose(); + } catch (error) { + console.error("Error resolving incident:", error); + } finally { + setIsSubmitting(false); + } + }; + + return ( + + + setComment(e.target.value)} + maxWidth="100%" + /> + + + + + + + ); +}; + +ResolveIncidentDialog.propTypes = { + open: PropTypes.bool.isRequired, + incidentId: PropTypes.string.isRequired, + onClose: PropTypes.func.isRequired, + onResolve: PropTypes.func.isRequired, + onAfterResolve: PropTypes.func, + isLoading: PropTypes.bool, + onResolved: PropTypes.func, +}; + +ResolveIncidentDialog.defaultProps = { + isLoading: false, +}; + +export default ResolveIncidentDialog; diff --git a/client/src/Pages/Incidents/index.jsx b/client/src/Pages/Incidents/index.jsx index 2721f42ad..abf24989b 100644 --- a/client/src/Pages/Incidents/index.jsx +++ b/client/src/Pages/Incidents/index.jsx @@ -157,7 +157,7 @@ const Incidents2 = () => { handleUpdateTrigger={handleUpdateTrigger} onOpenDetails={handleOpenDetails} /> - Date: Wed, 21 Jan 2026 18:47:38 +0530 Subject: [PATCH 312/400] Fix: updateTrigger val --- client/src/Hooks/monitorHooks.js | 9 ++------- client/src/Pages/Uptime/Create/index.jsx | 21 +++++++++------------ 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index f0bf805f4..9d46e9365 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -412,19 +412,14 @@ export const usePauseMonitor = () => { try { setIsLoading(true); const res = await networkService.pauseMonitorById({ monitorId }); - const updatedMonitor = res.data.data; createToast({ - body: updatedMonitor.isActive + body: res.data.data.isActive ? "Monitor resumed successfully" : "Monitor paused successfully", }); - if (triggerUpdate) { - triggerUpdate(); - } - return updatedMonitor; + triggerUpdate(); } catch (error) { setError(error); - return null; } finally { setIsLoading(false); } diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index 6688f9b12..01fc9eb7a 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -80,13 +80,13 @@ const UptimeCreate = ({ isClone = false }) => { const [isFetchingMonitor] = useFetchMonitorById({ monitorId, setMonitor, - updateTrigger: true, + updateTrigger, }); // Fetch games const [isFetchingGames] = useFetchMonitorGames({ setGames, - triggerUpdate: true, + updateTrigger, }); // Combine the loading states @@ -283,13 +283,6 @@ const UptimeCreate = ({ isClone = false }) => { })); }; - const handlePause = async () => { - const updatedMonitor = await pauseMonitor({ monitorId }); - if (updatedMonitor) { - setMonitor((prev) => ({ ...prev, isActive: updatedMonitor.isActive })); - } - }; - const handleRemove = async (event) => { event.preventDefault(); const TEMP_MONITOR = { id: monitor.id }; @@ -427,8 +420,7 @@ const UptimeCreate = ({ isClone = false }) => { From 3fa370765e7b3af770ad9ec66b6381da154a067a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 21 Jan 2026 09:52:31 -0800 Subject: [PATCH 313/400] purge papaparse --- server/package-lock.json | 7 -- server/package.json | 1 - server/src/config/services.ts | 2 - server/src/controllers/incidentController.ts | 1 + server/src/controllers/monitorController.ts | 22 +--- server/src/service/business/monitorService.ts | 107 +++++++----------- 6 files changed, 48 insertions(+), 92 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index a1a392b18..2dd8d23e7 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -36,7 +36,6 @@ "mongoose": "^8.3.3", "multer": "^1.4.5-lts.1", "nodemailer": "^6.9.14", - "papaparse": "^5.5.2", "ping": "0.4.4", "sharp": "0.33.5", "ssl-checker": "2.0.10", @@ -11567,12 +11566,6 @@ "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", "license": "BlueOak-1.0.0" }, - "node_modules/papaparse": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.5.3.tgz", - "integrity": "sha512-5QvjGxYVjxO59MGU2lHVYpRWBBtKHnlIAcSe1uNFCkkptUh63NFRj0FJQm7nR67puEruUci/ZkjmEFrjCAyP4A==", - "license": "MIT" - }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", diff --git a/server/package.json b/server/package.json index 2bf898c94..2cbacea39 100755 --- a/server/package.json +++ b/server/package.json @@ -48,7 +48,6 @@ "mongoose": "^8.3.3", "multer": "^1.4.5-lts.1", "nodemailer": "^6.9.14", - "papaparse": "^5.5.2", "ping": "0.4.4", "sharp": "0.33.5", "ssl-checker": "2.0.10", diff --git a/server/src/config/services.ts b/server/src/config/services.ts index de4d5115f..423e23ca5 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -24,7 +24,6 @@ import InviteService from "../service/business/inviteService.js"; import MaintenanceWindowService from "../service/business/maintenanceWindowService.js"; import { MonitorService } from "@/service/index.js"; import IncidentService from "../service/business/incidentService.js"; -import papaparse from "papaparse"; import axios from "axios"; import got from "got"; import ping from "ping"; @@ -276,7 +275,6 @@ export const initializeServices = async ({ const monitorService = new MonitorService({ jobQueue: superSimpleQueue, emailService, - papaparse, logger, errorService, games, diff --git a/server/src/controllers/incidentController.ts b/server/src/controllers/incidentController.ts index 93783b20c..a5477e019 100644 --- a/server/src/controllers/incidentController.ts +++ b/server/src/controllers/incidentController.ts @@ -73,6 +73,7 @@ class IncidentController { resolveIncidentManually = async (req: Request, res: Response, next: NextFunction) => { try { + console.log({ user: req.user }); const resolvedIncident = await this.incidentService.resolveIncident( req?.params?.incidentId, req?.user?.id, diff --git a/server/src/controllers/monitorController.ts b/server/src/controllers/monitorController.ts index 2ebb113ae..30ceabbc5 100644 --- a/server/src/controllers/monitorController.ts +++ b/server/src/controllers/monitorController.ts @@ -174,27 +174,15 @@ class MonitorController { createBulkMonitors = async (req: Request, res: Response, next: NextFunction) => { try { - if (!req.file) { - throw new AppError({ message: "No file uploaded", status: 400 }); + const userId = req.user?.id; + if (!userId) { + throw new AppError({ message: "User ID is required", status: 400 }); } - if (!req.file.mimetype.includes("csv")) { - throw new AppError({ message: "File is not a CSV", status: 400 }); - } - - if (req.file.size === 0) { - throw new AppError({ message: "File is empty", status: 400 }); - } - - const userId = requireString(req?.user?.id, "User ID"); const teamId = requireTeamId(req?.user?.teamId); - const fileData = req?.file?.buffer?.toString("utf-8"); - if (!fileData) { - throw new AppError({ message: "Cannot get file from buffer", status: 400 }); - } - - const monitors = await this.monitorService.createBulkMonitors(fileData, userId, teamId); + const data = req.body; + const monitors = await this.monitorService.createBulkMonitors(data, userId, teamId); return res.status(200).json({ success: true, diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 4972bdee7..4f97bdf0f 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -1,7 +1,13 @@ import { createMonitorsBodyValidation } from "@/validation/joi.js"; import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js"; import { type Monitor } from "@/types/index.js"; -import type { MonitorType, MonitorsWithChecksByTeamIdResult, UptimeDetailsResult, HardwareDetailsResult, PageSpeedDetailsResult } from "@/types/monitor.js"; +import type { + MonitorType, + MonitorsWithChecksByTeamIdResult, + UptimeDetailsResult, + HardwareDetailsResult, + PageSpeedDetailsResult, +} from "@/types/monitor.js"; import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository, IStatusPagesRepository } from "@/repositories/index.js"; import fs from "fs"; import { fileURLToPath } from "url"; @@ -18,7 +24,7 @@ export interface IMonitorService { // create createMonitor(teamId: string, userId: string, body: Monitor): Promise; - createBulkMonitors(fileData: string, userId: string, teamId: string): Promise; + createBulkMonitors(monitors: Array, userId: string, teamId: string): Promise; addDemoMonitors(args: { userId: string; teamId: string }): Promise; // read @@ -68,7 +74,6 @@ export class MonitorService implements IMonitorService { private jobQueue: ISuperSimpleQueue; private emailService: any; - private papaparse: any; private logger: any; private errorService: any; private games: any; @@ -80,7 +85,6 @@ export class MonitorService implements IMonitorService { constructor({ jobQueue, emailService, - papaparse, logger, errorService, games, @@ -91,7 +95,6 @@ export class MonitorService implements IMonitorService { }: { jobQueue: ISuperSimpleQueue; emailService: any; - papaparse: any; logger: any; errorService: any; games: any; @@ -102,7 +105,6 @@ export class MonitorService implements IMonitorService { }) { this.jobQueue = jobQueue; this.emailService = emailService; - this.papaparse = papaparse; this.logger = logger; this.errorService = errorService; this.games = games; @@ -150,63 +152,14 @@ export class MonitorService implements IMonitorService { this.jobQueue.addJob(monitor.id, monitor); }; - createBulkMonitors = async (fileData: string, userId: string, teamId: string): Promise => { - const { parse } = this.papaparse; + createBulkMonitors = async (monitors: Array, userId: string, teamId: string): Promise => { + const createdMonitors = await this.monitorsRepository.createBulkMonitors(monitors); + if (!monitors || monitors.length === 0) { + throw new AppError("Failed to create monitors", 500); + } - return new Promise((resolve, reject) => { - parse(fileData, { - header: true, - skipEmptyLines: true, - transform: (value: string, header: string): string | number | undefined => { - if (value === "") return undefined; // Empty fields become undefined - - // Handle 'port' and 'interval' fields, check if they're valid numbers - if (["port", "interval"].includes(header)) { - const num = parseInt(value, 10); - if (isNaN(num)) { - throw this.errorService.createBadRequestError(`${header} should be a valid number, got: ${value}`); - } - return num; - } - - return value; - }, - complete: async ({ data, errors }: { data: any[]; errors: Error[] }): Promise => { - try { - if (errors.length > 0) { - throw this.errorService.createServerError("Error parsing CSV"); - } - - if (!data || data.length === 0) { - throw this.errorService.createServerError("CSV file contains no data rows"); - } - - const enrichedData = data.map((monitor: Monitor) => ({ - ...monitor, - userId, - teamId, - description: monitor.description || monitor.name || monitor.url, - name: monitor.name || monitor.url, - type: monitor.type || "http", - })); - - await createMonitorsBodyValidation.validateAsync(enrichedData); - - const monitors = await this.monitorsRepository.createBulkMonitors(enrichedData); - - await Promise.all( - monitors.map(async (monitor: Monitor) => { - this.jobQueue.addJob(monitor.id, monitor); - }) - ); - - resolve(monitors); - } catch (error) { - reject(error); - } - }, - }); - }); + await Promise.all(createdMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor))); + return createdMonitors; }; addDemoMonitors = async ({ userId, teamId }: { userId: string; teamId: string }): Promise => { @@ -277,7 +230,15 @@ export class MonitorService implements IMonitorService { }; }; - getHardwareDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise => { + getHardwareDetailsById = async ({ + teamId, + monitorId, + dateRange, + }: { + teamId: string; + monitorId: string; + dateRange: string; + }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); @@ -308,7 +269,15 @@ export class MonitorService implements IMonitorService { }; }; - getPageSpeedDetailsById = async ({ teamId, monitorId, dateRange }: { teamId: string; monitorId: string; dateRange: string }): Promise => { + getPageSpeedDetailsById = async ({ + teamId, + monitorId, + dateRange, + }: { + teamId: string; + monitorId: string; + dateRange: string; + }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); @@ -342,7 +311,15 @@ export class MonitorService implements IMonitorService { return monitor; }; - getMonitorsByTeamId = async ({ teamId, type, filter }: { teamId: string; type?: MonitorType | MonitorType[]; filter?: string }): Promise => { + getMonitorsByTeamId = async ({ + teamId, + type, + filter, + }: { + teamId: string; + type?: MonitorType | MonitorType[]; + filter?: string; + }): Promise => { const monitors = await this.monitorsRepository.findByTeamId(teamId, { type, filter, From ef61a63da09526b5d9f1de4c9645f9e0c82c9c30 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 21 Jan 2026 10:15:13 -0800 Subject: [PATCH 314/400] parse query --- server/src/controllers/incidentController.ts | 19 ++++++++++++++----- .../src/service/business/incidentService.ts | 12 ++++++++++-- 2 files changed, 24 insertions(+), 7 deletions(-) diff --git a/server/src/controllers/incidentController.ts b/server/src/controllers/incidentController.ts index a5477e019..6231f3cf4 100644 --- a/server/src/controllers/incidentController.ts +++ b/server/src/controllers/incidentController.ts @@ -1,5 +1,6 @@ import { AppError } from "@/utils/AppError.js"; import { Request, Response, NextFunction } from "express"; +import { requireTeamId } from "./controllerUtils.js"; const SERVICE_NAME = "incidentController"; @@ -17,10 +18,19 @@ class IncidentController { getIncidentsByTeam = async (req: Request, res: Response, next: NextFunction) => { try { - const result = await this.incidentService.getIncidentsByTeam({ - teamId: req?.user?.teamId, - query: req?.query, - }); + const { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = req.query || {}; + + const teamId = requireTeamId(req.user?.teamId); + const result = await this.incidentService.getIncidentsByTeam( + teamId, + sortOrder, + dateRange, + page, + rowsPerPage, + status, + monitorId, + resolutionType + ); return res.status(200).json({ success: true, @@ -73,7 +83,6 @@ class IncidentController { resolveIncidentManually = async (req: Request, res: Response, next: NextFunction) => { try { - console.log({ user: req.user }); const resolvedIncident = await this.incidentService.resolveIncident( req?.params?.incidentId, req?.user?.id, diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index 5ababc5b4..e3fb4cfc3 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -124,13 +124,21 @@ class IncidentService { } }; - getIncidentsByTeam = async ({ teamId, query }: { teamId: string; query?: any }) => { + getIncidentsByTeam = async ( + teamId: string, + sortOrder: string, + dateRange: string, + page: string, + rowsPerPage: string, + status: string, + monitorId: string, + resolutionType: string + ) => { try { if (!teamId) { throw this.errorService.createBadRequestError("No team ID in request"); } - const { sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType } = query || {}; const startDate = dateRangeLookup[dateRange]; const parsedPage = Number.isFinite(parseInt(page)) ? parseInt(page) : 0; From d242382abfc0428078bfda6abe5df96ddc1716cb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 21 Jan 2026 10:16:17 -0800 Subject: [PATCH 315/400] format --- server/src/repositories/monitors/MongoMonitorsRepository.ts | 5 +---- server/src/utils/dataUtils.ts | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 4a659b1c2..17b9ce243 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -146,10 +146,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { return { monitors: this.mapDocuments(monitors), deletedCount }; }; - findMonitorsSummaryByTeamId = async ( - teamId: string, - config?: SummaryConfig - ): Promise => { + findMonitorsSummaryByTeamId = async (teamId: string, config?: SummaryConfig): Promise => { const match: FilterQuery = { teamId: new mongoose.Types.ObjectId(teamId) }; if (config?.type !== undefined) { match.type = Array.isArray(config.type) ? { $in: config.type } : config.type; diff --git a/server/src/utils/dataUtils.ts b/server/src/utils/dataUtils.ts index fac7cecef..0bf3e6416 100755 --- a/server/src/utils/dataUtils.ts +++ b/server/src/utils/dataUtils.ts @@ -1,6 +1,5 @@ import type { Check, GroupedCheck, NormalizedCheck, NormalizedUptimeCheck } from "@/types/index.js"; - const calculatePercentile = (arr: Check[], percentile: number): number => { const sorted = arr.slice().sort((a, b) => a.responseTime - b.responseTime); const index = (percentile / 100) * (sorted.length - 1); From 81c7c992e2b8b5bb56f272a716da17a265b734ae Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 21 Jan 2026 10:36:03 -0800 Subject: [PATCH 316/400] routes -> ts --- server/src/config/routes.ts | 25 +++++++++---------- .../routes/{v1/authRoute.js => authRoute.ts} | 11 +++++--- .../{v1/checkRoute.js => checkRoute.ts} | 7 ++++-- .../diagnosticRoute.js => diagnosticRoute.ts} | 11 +++++--- .../{v1/incidentRoute.js => incidentRoute.ts} | 7 ++++-- .../{v1/inviteRoute.js => inviteRoute.ts} | 11 +++++--- .../routes/{v1/logRoutes.js => logRoutes.ts} | 7 ++++-- ...ndowRoute.js => maintenanceWindowRoute.ts} | 5 +++- server/src/routes/{v1 => }/monitorRoute.ts | 4 +-- ...ificationRoute.js => notificationRoute.ts} | 5 +++- .../{v1/queueRoute.js => queueRoute.ts} | 7 ++++-- .../{v1/settingsRoute.js => settingsRoute.ts} | 7 ++++-- .../statusPageRoute.js => statusPageRoute.ts} | 9 ++++--- 13 files changed, 74 insertions(+), 42 deletions(-) rename server/src/routes/{v1/authRoute.js => authRoute.ts} (83%) rename server/src/routes/{v1/checkRoute.js => checkRoute.ts} (83%) rename server/src/routes/{v1/diagnosticRoute.js => diagnosticRoute.ts} (54%) rename server/src/routes/{v1/incidentRoute.js => incidentRoute.ts} (80%) rename server/src/routes/{v1/inviteRoute.js => inviteRoute.ts} (64%) rename server/src/routes/{v1/logRoutes.js => logRoutes.ts} (68%) rename server/src/routes/{v1/maintenanceWindowRoute.js => maintenanceWindowRoute.ts} (87%) rename server/src/routes/{v1 => }/monitorRoute.ts (94%) rename server/src/routes/{v1/notificationRoute.js => notificationRoute.ts} (88%) rename server/src/routes/{v1/queueRoute.js => queueRoute.ts} (84%) rename server/src/routes/{v1/settingsRoute.js => settingsRoute.ts} (77%) rename server/src/routes/{v1/statusPageRoute.js => statusPageRoute.ts} (78%) diff --git a/server/src/config/routes.ts b/server/src/config/routes.ts index 024387c25..d38ac69ae 100644 --- a/server/src/config/routes.ts +++ b/server/src/config/routes.ts @@ -1,23 +1,22 @@ import { createVerifyJWT } from "../middleware/verifyJWT.js"; import { authApiLimiter } from "../middleware/rateLimiter.js"; -import AuthRoutes from "../routes/v1/authRoute.js"; -import InviteRoutes from "../routes/v1/inviteRoute.js"; -import MonitorRoutes from "../routes/v1/monitorRoute.js"; -import CheckRoutes from "../routes/v1/checkRoute.js"; -import SettingsRoutes from "../routes/v1/settingsRoute.js"; -import MaintenanceWindowRoutes from "../routes/v1/maintenanceWindowRoute.js"; -import StatusPageRoutes from "../routes/v1/statusPageRoute.js"; -import QueueRoutes from "../routes/v1/queueRoute.js"; -import LogRoutes from "../routes/v1/logRoutes.js"; -import DiagnosticRoutes from "../routes/v1/diagnosticRoute.js"; -import NotificationRoutes from "../routes/v1/notificationRoute.js"; +import AuthRoutes from "../routes/authRoute.js"; +import InviteRoutes from "../routes/inviteRoute.js"; +import MonitorRoutes from "../routes/monitorRoute.js"; +import CheckRoutes from "../routes/checkRoute.js"; +import SettingsRoutes from "../routes/settingsRoute.js"; +import MaintenanceWindowRoutes from "../routes/maintenanceWindowRoute.js"; +import StatusPageRoutes from "../routes/statusPageRoute.js"; +import QueueRoutes from "../routes/queueRoute.js"; +import LogRoutes from "../routes/logRoutes.js"; +import DiagnosticRoutes from "../routes/diagnosticRoute.js"; +import NotificationRoutes from "../routes/notificationRoute.js"; -import IncidentRoutes from "../routes/v1/incidentRoute.js"; +import IncidentRoutes from "../routes/incidentRoute.js"; export const setupRoutes = (app: any, controllers: Record, services: Record) => { const verifyJWT = createVerifyJWT(services.settingsService); - // V1 const authRoutes = new AuthRoutes(controllers.authController, verifyJWT); const monitorRoutes = new MonitorRoutes(controllers.monitorController); const settingsRoutes = new SettingsRoutes(controllers.settingsController); diff --git a/server/src/routes/v1/authRoute.js b/server/src/routes/authRoute.ts similarity index 83% rename from server/src/routes/v1/authRoute.js rename to server/src/routes/authRoute.ts index 0eca250e5..eec6ea11b 100755 --- a/server/src/routes/v1/authRoute.js +++ b/server/src/routes/authRoute.ts @@ -1,17 +1,20 @@ -import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { Router, RequestHandler } from "express"; +import { isAllowed } from "../middleware/isAllowed.js"; import multer from "multer"; const upload = multer(); class AuthRoutes { - constructor(authController, verifyJWT) { + private router: Router; + private authController: any; + + constructor(authController: any, verifyJWT: RequestHandler) { this.router = Router(); this.authController = authController; this.initRoutes(verifyJWT); } - initRoutes(verifyJWT) { + initRoutes(verifyJWT: RequestHandler) { this.router.post("/register", upload.single("profileImage"), this.authController.registerUser); this.router.post("/login", this.authController.loginUser); diff --git a/server/src/routes/v1/checkRoute.js b/server/src/routes/checkRoute.ts similarity index 83% rename from server/src/routes/v1/checkRoute.js rename to server/src/routes/checkRoute.ts index 2c34049bb..4df286846 100755 --- a/server/src/routes/v1/checkRoute.js +++ b/server/src/routes/checkRoute.ts @@ -1,9 +1,12 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "../middleware/isAllowed.js"; class CheckRoutes { - constructor(checkController) { + private router: Router; + private checkController: any; + + constructor(checkController: any) { this.router = Router(); this.checkController = checkController; this.initRoutes(); diff --git a/server/src/routes/v1/diagnosticRoute.js b/server/src/routes/diagnosticRoute.ts similarity index 54% rename from server/src/routes/v1/diagnosticRoute.js rename to server/src/routes/diagnosticRoute.ts index 9ec470e53..b2cefe4aa 100755 --- a/server/src/routes/v1/diagnosticRoute.js +++ b/server/src/routes/diagnosticRoute.ts @@ -1,14 +1,17 @@ -import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { RequestHandler, Router } from "express"; +import { isAllowed } from "../middleware/isAllowed.js"; class DiagnosticRoutes { - constructor(diagnosticController, verifyJWT) { + private router: Router; + private diagnosticController: any; + + constructor(diagnosticController: any, verifyJWT: RequestHandler) { this.router = Router(); this.diagnosticController = diagnosticController; this.initRoutes(verifyJWT); } - initRoutes(verifyJWT) { + initRoutes(verifyJWT: RequestHandler) { this.router.get("/system", verifyJWT, isAllowed(["admin", "superadmin"]), this.diagnosticController.getSystemStats); } diff --git a/server/src/routes/v1/incidentRoute.js b/server/src/routes/incidentRoute.ts similarity index 80% rename from server/src/routes/v1/incidentRoute.js rename to server/src/routes/incidentRoute.ts index f87ef17a7..83f1ff8f9 100644 --- a/server/src/routes/v1/incidentRoute.js +++ b/server/src/routes/incidentRoute.ts @@ -1,8 +1,11 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "../middleware/isAllowed.js"; class IncidentRoutes { - constructor(incidentController) { + private router: Router; + private incidentController: any; + + constructor(incidentController: any) { this.router = Router(); this.incidentController = incidentController; this.initRoutes(); diff --git a/server/src/routes/v1/inviteRoute.js b/server/src/routes/inviteRoute.ts similarity index 64% rename from server/src/routes/v1/inviteRoute.js rename to server/src/routes/inviteRoute.ts index 44ec0495f..b91f922df 100755 --- a/server/src/routes/v1/inviteRoute.js +++ b/server/src/routes/inviteRoute.ts @@ -1,14 +1,17 @@ -import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { RequestHandler, Router } from "express"; +import { isAllowed } from "../middleware/isAllowed.js"; class InviteRoutes { - constructor(inviteController, verifyJWT) { + private router: Router; + private inviteController: any; + + constructor(inviteController: any, verifyJWT: RequestHandler) { this.router = Router(); this.inviteController = inviteController; this.initRoutes(verifyJWT); } - initRoutes(verifyJWT) { + initRoutes(verifyJWT: RequestHandler) { this.router.post("/send", verifyJWT, isAllowed(["admin", "superadmin"]), this.inviteController.sendInviteEmail); this.router.post("/verify", this.inviteController.verifyInviteToken); this.router.post("/", verifyJWT, isAllowed(["admin", "superadmin"]), this.inviteController.getInviteToken); diff --git a/server/src/routes/v1/logRoutes.js b/server/src/routes/logRoutes.ts similarity index 68% rename from server/src/routes/v1/logRoutes.js rename to server/src/routes/logRoutes.ts index feabe669a..c35361fd0 100755 --- a/server/src/routes/v1/logRoutes.js +++ b/server/src/routes/logRoutes.ts @@ -1,7 +1,10 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "../middleware/isAllowed.js"; class LogRoutes { - constructor(logController) { + private router: Router; + private logController: any; + + constructor(logController: any) { this.router = Router(); this.logController = logController; this.initRoutes(); diff --git a/server/src/routes/v1/maintenanceWindowRoute.js b/server/src/routes/maintenanceWindowRoute.ts similarity index 87% rename from server/src/routes/v1/maintenanceWindowRoute.js rename to server/src/routes/maintenanceWindowRoute.ts index 345f2c13c..0230ba4bd 100755 --- a/server/src/routes/v1/maintenanceWindowRoute.js +++ b/server/src/routes/maintenanceWindowRoute.ts @@ -1,7 +1,10 @@ import { Router } from "express"; class MaintenanceWindowRoutes { - constructor(maintenanceWindowController) { + private router: Router; + private mwController: any; + + constructor(maintenanceWindowController: any) { this.router = Router(); this.mwController = maintenanceWindowController; this.initRoutes(); diff --git a/server/src/routes/v1/monitorRoute.ts b/server/src/routes/monitorRoute.ts similarity index 94% rename from server/src/routes/v1/monitorRoute.ts rename to server/src/routes/monitorRoute.ts index 4488a2c49..eb5604c24 100755 --- a/server/src/routes/v1/monitorRoute.ts +++ b/server/src/routes/monitorRoute.ts @@ -1,7 +1,7 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "@/middleware/isAllowed.js"; import multer from "multer"; -import { fetchMonitorCertificate } from "../../controllers/controllerUtils.js"; +import { fetchMonitorCertificate } from "@/controllers/controllerUtils.js"; const upload = multer({ storage: multer.memoryStorage(), // Store file in memory as Buffer }); diff --git a/server/src/routes/v1/notificationRoute.js b/server/src/routes/notificationRoute.ts similarity index 88% rename from server/src/routes/v1/notificationRoute.js rename to server/src/routes/notificationRoute.ts index f88b0b96c..7fe4bf5cb 100755 --- a/server/src/routes/v1/notificationRoute.js +++ b/server/src/routes/notificationRoute.ts @@ -1,6 +1,9 @@ import { Router } from "express"; class NotificationRoutes { - constructor(notificationController) { + private router: Router; + private notificationController: any; + + constructor(notificationController: any) { this.router = Router(); this.notificationController = notificationController; this.initializeRoutes(); diff --git a/server/src/routes/v1/queueRoute.js b/server/src/routes/queueRoute.ts similarity index 84% rename from server/src/routes/v1/queueRoute.js rename to server/src/routes/queueRoute.ts index 59e1cb744..d5b036b15 100755 --- a/server/src/routes/v1/queueRoute.js +++ b/server/src/routes/queueRoute.ts @@ -1,7 +1,10 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "../middleware/isAllowed.js"; class QueueRoutes { - constructor(queueController) { + private router: Router; + private queueController: any; + + constructor(queueController: any) { this.router = Router(); this.queueController = queueController; this.initRoutes(); diff --git a/server/src/routes/v1/settingsRoute.js b/server/src/routes/settingsRoute.ts similarity index 77% rename from server/src/routes/v1/settingsRoute.js rename to server/src/routes/settingsRoute.ts index 76556e923..c7db1ed3b 100755 --- a/server/src/routes/v1/settingsRoute.js +++ b/server/src/routes/settingsRoute.ts @@ -1,8 +1,11 @@ import { Router } from "express"; -import { isAllowed } from "../../middleware/isAllowed.js"; +import { isAllowed } from "../middleware/isAllowed.js"; class SettingsRoutes { - constructor(settingsController) { + private router: Router; + private settingsController: any; + + constructor(settingsController: any) { this.router = Router(); this.settingsController = settingsController; this.initRoutes(); diff --git a/server/src/routes/v1/statusPageRoute.js b/server/src/routes/statusPageRoute.ts similarity index 78% rename from server/src/routes/v1/statusPageRoute.js rename to server/src/routes/statusPageRoute.ts index 7d915cb86..9d6902aac 100755 --- a/server/src/routes/v1/statusPageRoute.js +++ b/server/src/routes/statusPageRoute.ts @@ -1,15 +1,18 @@ -import { Router } from "express"; +import { RequestHandler, Router } from "express"; import multer from "multer"; const upload = multer(); class StatusPageRoutes { - constructor(statusPageController, verifyJWT) { + private router: Router; + private statusPageController: any; + + constructor(statusPageController: any, verifyJWT: RequestHandler) { this.router = Router(); this.statusPageController = statusPageController; this.initRoutes(verifyJWT); } - initRoutes(verifyJWT) { + initRoutes(verifyJWT: RequestHandler) { this.router.get("/", this.statusPageController.getStatusPage); this.router.get("/team", verifyJWT, this.statusPageController.getStatusPagesByTeamId); From 2a8d2cb06abf16939f42c30b44bb32e67c3f307f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 21 Jan 2026 11:18:13 -0800 Subject: [PATCH 317/400] app -> ts --- server/package-lock.json | 47 +++++++++++++++++ server/package.json | 4 ++ server/src/{app.js => app.ts} | 25 +++++++-- server/src/config/controllers.ts | 51 +++++++++++-------- server/src/config/index.ts | 25 --------- server/src/config/services.ts | 12 +++-- server/src/{index.js => index.ts} | 6 +-- .../service/infrastructure/bufferService.ts | 6 +-- ...{settingsService.js => settingsService.ts} | 47 +++++++++-------- server/src/utils/logger.ts | 18 +++++-- 10 files changed, 152 insertions(+), 89 deletions(-) rename server/src/{app.js => app.ts} (69%) delete mode 100644 server/src/config/index.ts rename server/src/{index.js => index.ts} (92%) rename server/src/service/system/{settingsService.js => settingsService.ts} (65%) diff --git a/server/package-lock.json b/server/package-lock.json index 2dd8d23e7..3f5b824b0 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -45,6 +45,9 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/compression": "1.8.1", + "@types/cookie-parser": "1.4.10", + "@types/cors": "2.8.19", "@types/dockerode": "^4.0.0", "@types/express": "5.0.3", "@types/gamedig": "^5.0.3", @@ -57,6 +60,7 @@ "@types/nodemailer": "7.0.1", "@types/papaparse": "^5.5.2", "@types/ping": "0.4.4", + "@types/swagger-ui-express": "4.1.8", "c8": "10.1.3", "eslint": "^9.17.0", "eslint-plugin-mocha": "^10.5.0", @@ -4323,6 +4327,17 @@ "@types/node": "*" } }, + "node_modules/@types/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@types/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-kCFuWS0ebDbmxs0AXYn6e2r2nrGAb5KwQhknjSPSPgJcGd8+HVSILlUyFhGqML2gk39HcG7D1ydW9/qpYkN00Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/node": "*" + } + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", @@ -4333,6 +4348,26 @@ "@types/node": "*" } }, + "node_modules/@types/cookie-parser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.10.tgz", + "integrity": "sha512-B4xqkqfZ8Wek+rCOeRxsjMS9OgvzebEzzLYw7NHYuvzb7IdxOkI0ZHGgeEBX4PUM7QGVvNSK60T3OvWj3YfBRg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/cors": { + "version": "2.8.19", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz", + "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/docker-modem": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/@types/docker-modem/-/docker-modem-3.0.6.tgz", @@ -4369,6 +4404,7 @@ "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -4649,6 +4685,17 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/swagger-ui-express": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@types/swagger-ui-express/-/swagger-ui-express-4.1.8.tgz", + "integrity": "sha512-AhZV8/EIreHFmBV5wAs0gzJUNq9JbbSXgJLQubCC0jtIo6prnI9MIRRxnU4MZX9RB9yXxF1V4R7jtLl/Wcj31g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/express": "*", + "@types/serve-static": "*" + } + }, "node_modules/@types/tough-cookie": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", diff --git a/server/package.json b/server/package.json index 2cbacea39..2421aeb1c 100755 --- a/server/package.json +++ b/server/package.json @@ -57,6 +57,9 @@ }, "devDependencies": { "@eslint/js": "^9.17.0", + "@types/compression": "1.8.1", + "@types/cookie-parser": "1.4.10", + "@types/cors": "2.8.19", "@types/dockerode": "^4.0.0", "@types/express": "5.0.3", "@types/gamedig": "^5.0.3", @@ -69,6 +72,7 @@ "@types/nodemailer": "7.0.1", "@types/papaparse": "^5.5.2", "@types/ping": "0.4.4", + "@types/swagger-ui-express": "4.1.8", "c8": "10.1.3", "eslint": "^9.17.0", "eslint-plugin-mocha": "^10.5.0", diff --git a/server/src/app.js b/server/src/app.ts similarity index 69% rename from server/src/app.js rename to server/src/app.ts index c83726a49..04699964d 100644 --- a/server/src/app.js +++ b/server/src/app.ts @@ -5,12 +5,27 @@ import helmet from "helmet"; import compression from "compression"; import cookieParser from "cookie-parser"; import swaggerUi from "swagger-ui-express"; -import { handleErrors } from "./middleware/handleErrors.js"; -import { setupRoutes } from "./config/routes.js"; -import { generalApiLimiter } from "./middleware/rateLimiter.js"; -import { sanitizeBody, sanitizeQuery } from "./middleware/sanitization.js"; +import { handleErrors } from "@/middleware/handleErrors.js"; +import { generalApiLimiter } from "@/middleware/rateLimiter.js"; +import { sanitizeBody, sanitizeQuery } from "@/middleware/sanitization.js"; +import { setupRoutes } from "@/config/routes.js"; +import { InitializedServices } from "@/config/services.js"; +import { InitializedControllers } from "@/config/controllers.js"; +import { EnvConfig } from "@/service/system/settingsService.js"; -export const createApp = ({ services, controllers, envSettings, frontendPath, openApiSpec }) => { +export const createApp = ({ + services, + controllers, + envSettings, + frontendPath, + openApiSpec, +}: { + services: InitializedServices; + controllers: InitializedControllers; + envSettings: EnvConfig; + frontendPath: string; + openApiSpec: any; +}) => { const allowedOrigin = envSettings.clientHost; const app = express(); app.use(generalApiLimiter); diff --git a/server/src/config/controllers.ts b/server/src/config/controllers.ts index 5ddbf2a06..d72e4f76f 100644 --- a/server/src/config/controllers.ts +++ b/server/src/config/controllers.ts @@ -10,26 +10,35 @@ import StatusPageController from "../controllers/statusPageController.js"; import NotificationController from "../controllers/notificationController.js"; import DiagnosticController from "../controllers/diagnosticController.js"; import IncidentController from "../controllers/incidentController.js"; -import type { InitializedSerivces } from "@/config/services.js"; -export const initializeControllers = (services: InitializedSerivces) => { - const controllers: Record = {}; +import type { InitializedServices } from "@/config/services.js"; - controllers.authController = new AuthController(services.userService); - - controllers.monitorController = new MonitorController(services.monitorService); - - controllers.settingsController = new SettingsController(services.settingsService, services.emailService, services.db); - controllers.checkController = new CheckController(services.checkService); - controllers.inviteController = new InviteController(services.inviteService); - - controllers.maintenanceWindowController = new MaintenanceWindowController(services.maintenanceWindowService); - controllers.queueController = new QueueController(services.jobQueue); - controllers.logController = new LogController(services.logger); - controllers.statusPageController = new StatusPageController(services.db); - controllers.notificationController = new NotificationController(services.notificationsService, services.db, services.monitorsRepository); - controllers.diagnosticController = new DiagnosticController(services.diagnosticService); - - controllers.incidentController = new IncidentController(services.incidentService); - - return controllers; +export interface InitializedControllers { + authController: AuthController; + monitorController: MonitorController; + settingsController: SettingsController; + checkController: CheckController; + inviteController: InviteController; + maintenanceWindowController: MaintenanceWindowController; + queueController: QueueController; + logController: LogController; + statusPageController: StatusPageController; + notificationController: NotificationController; + diagnosticController: DiagnosticController; + incidentController: IncidentController; +} +export const initializeControllers = (services: InitializedServices): InitializedControllers => { + return { + authController: new AuthController(services.userService), + monitorController: new MonitorController(services.monitorService), + settingsController: new SettingsController(services.settingsService, services.emailService, services.db), + checkController: new CheckController(services.checkService), + inviteController: new InviteController(services.inviteService), + maintenanceWindowController: new MaintenanceWindowController(services.maintenanceWindowService), + queueController: new QueueController(services.jobQueue), + logController: new LogController(services.logger), + statusPageController: new StatusPageController(services.db), + notificationController: new NotificationController(services.notificationsService, services.db, services.monitorsRepository), + diagnosticController: new DiagnosticController(services.diagnosticService), + incidentController: new IncidentController(services.incidentService), + }; }; diff --git a/server/src/config/index.ts b/server/src/config/index.ts deleted file mode 100644 index 7ace6f99c..000000000 --- a/server/src/config/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -import dotenv from "dotenv"; - -export interface IEevConfig { - NODE_ENV: string; - JWT_SECRET: string; - PORT: number; - PAGESPEED_API_KEY: string; - SMTP_HOST: string; - SMTP_PORT: number; - SMTP_USER: string; - SMTP_PASS: string; -} - -dotenv.config(); - -export const config: IEevConfig = { - NODE_ENV: process.env.NODE_ENV || "development", - JWT_SECRET: process.env.JWT_SECRET || "your_jwt_secret", - PORT: process.env.PORT ? parseInt(process.env.PORT, 10) : 3000, - PAGESPEED_API_KEY: process.env.PAGESPEED_API_KEY || "", - SMTP_HOST: process.env.SMTP_HOST || "smtp.example.com", - SMTP_PORT: process.env.SMTP_PORT ? parseInt(process.env.SMTP_PORT, 10) : 587, - SMTP_USER: process.env.SMTP_USER || "user@example.com", - SMTP_PASS: process.env.SMTP_PASS || "your_smtp_password", -}; diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 423e23ca5..0cf55590b 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -92,8 +92,10 @@ import { INotificationsRepository, IIncidentsRepository, } from "@/repositories/index.js"; +import { ILogger } from "@/utils/logger.js"; +import { EnvConfig } from "@/service/system/settingsService.js"; -export type InitializedSerivces = { +export type InitializedServices = { //v1 settingsService: any; translationService: any; @@ -132,10 +134,10 @@ export const initializeServices = async ({ envSettings, settingsService, }: { - logger: any; - envSettings: any; + logger: ILogger; + envSettings: EnvConfig; settingsService: any; -}): Promise => { +}): Promise => { const translationService = new TranslationService(logger); await translationService.initialize(); @@ -205,7 +207,7 @@ export const initializeServices = async ({ checksRepository, }); - const bufferService = new BufferService({ logger, checkService }); + const bufferService = new BufferService({ logger, checkService, settingsService }); const statusService = new StatusService({ db, logger, buffer: bufferService, monitorsRepository }); diff --git a/server/src/index.js b/server/src/index.ts similarity index 92% rename from server/src/index.js rename to server/src/index.ts index 28dc5e8fd..b74404228 100755 --- a/server/src/index.js +++ b/server/src/index.ts @@ -7,12 +7,12 @@ import path from "path"; import fs from "fs"; import { runMigrations } from "./db/migration/index.js"; -import Logger from "./utils/logger.js"; -import SettingsService from "./service/system/settingsService.js"; +import Logger, { ILogger } from "@/utils/logger.js"; +import SettingsService from "@/service/system/settingsService.js"; import AppSettings from "./db/models/AppSettings.js"; const SERVICE_NAME = "Server"; -let logger; +let logger: ILogger; const startApp = async () => { // FE path diff --git a/server/src/service/infrastructure/bufferService.ts b/server/src/service/infrastructure/bufferService.ts index 89cd6a55f..461001800 100755 --- a/server/src/service/infrastructure/bufferService.ts +++ b/server/src/service/infrastructure/bufferService.ts @@ -1,4 +1,3 @@ -import { config } from "@/config/index.js"; import type { Check } from "@/types/index.js"; const SERVICE_NAME = "BufferService"; @@ -12,8 +11,9 @@ class BufferService { private bufferTimer: NodeJS.Timeout | null = null; private checksService: any; - constructor({ logger, checkService }: { logger: any; checkService: any }) { - this.BUFFER_TIMEOUT = config.NODE_ENV === "development" ? 10 : 1000 * 60 * 1; // 1 minute + constructor({ logger, checkService, settingsService }: { logger: any; checkService: any; settingsService: any }) { + this.BUFFER_TIMEOUT = settingsService.getSettings().nodeEnv === "development" ? 10 : 1000 * 60 * 1; // 1 minute + console.log(this.BUFFER_TIMEOUT); this.logger = logger; this.checksService = checkService; this.SERVICE_NAME = SERVICE_NAME; diff --git a/server/src/service/system/settingsService.js b/server/src/service/system/settingsService.ts similarity index 65% rename from server/src/service/system/settingsService.js rename to server/src/service/system/settingsService.ts index cea46f7ed..15f86995d 100755 --- a/server/src/service/system/settingsService.js +++ b/server/src/service/system/settingsService.ts @@ -1,6 +1,17 @@ const SERVICE_NAME = "SettingsService"; -const envConfig = { +export type EnvConfig = { + jwtSecret: string | undefined; + jwtTTL: string | undefined; + systemEmailHost: string | undefined; + nodeEnv: string | undefined; + logLevel: string | undefined; + clientHost: string | undefined; + dbConnectionString: string | undefined; + port: string | undefined; +}; + +const envConfig: EnvConfig = { jwtSecret: process.env.JWT_SECRET, jwtTTL: process.env.TOKEN_TTL, systemEmailHost: process.env.SYSTEM_EMAIL_HOST, @@ -10,19 +21,20 @@ const envConfig = { dbConnectionString: process.env.DB_CONNECTION_STRING, port: process.env.PORT, }; -/** - * SettingsService - * - * This service is responsible for loading and managing the application settings. - */ -class SettingsService { - static SERVICE_NAME = "SettingsService"; - /** - * Constructs a new SettingsService - * @constructor - * @throws {Error} - */ constructor(AppSettings) { +export interface ISettingsService { + readonly serviceName: string; + loadSettings(): EnvConfig; + getSettings(): EnvConfig; + getDBSettings(): Promise>; +} + +class SettingsService implements ISettingsService { + static SERVICE_NAME = "SettingsService"; + private AppSettings: any; + private settings: EnvConfig; + + constructor(AppSettings: any) { this.AppSettings = AppSettings; this.settings = { ...envConfig }; } @@ -31,19 +43,10 @@ class SettingsService { return SettingsService.SERVICE_NAME; } - /** - * Load settings from env settings - * @returns {Object>} The settings. - */ loadSettings() { return this.settings; } - /** - * Get the current settings. - * @returns {Object} The current settings. - * @throws Will throw an error if settings have not been loaded. - */ getSettings() { if (!this.settings) { throw new Error("Settings have not been loaded"); diff --git a/server/src/utils/logger.ts b/server/src/utils/logger.ts index ac1cbbf7c..4053f4a82 100755 --- a/server/src/utils/logger.ts +++ b/server/src/utils/logger.ts @@ -1,6 +1,7 @@ import { createLogger, format, transports, Logger as WinstonLogger } from "winston"; import type { Logform } from "winston"; import dotenv from "dotenv"; +import { EnvConfig } from "@/service/system/settingsService.js"; dotenv.config(); const SERVICE_NAME = "Logger"; @@ -18,19 +19,26 @@ interface LogEntry extends LogConfig { timestamp: string; } -interface EnvSettings { - logLevel?: string; +export interface ILogger { + readonly serviceName: string; + info(config: LogConfig): void; + warn(config: LogConfig): void; + error(config: LogConfig): void; + debug(config: LogConfig): void; + cacheLog(entry: LogEntry): void; + getLogs(): LogEntry[]; + buildLogEntry(level: string, config: LogConfig): LogEntry; } -class Logger { +class Logger implements ILogger { static SERVICE_NAME = SERVICE_NAME; private logger: WinstonLogger; - private envSettings: EnvSettings; + private envSettings: Partial; private logCache: LogEntry[]; private maxCacheSize: number; - constructor({ envSettings }: { envSettings: EnvSettings }) { + constructor({ envSettings }: { envSettings: Partial }) { this.envSettings = envSettings; this.logCache = []; this.maxCacheSize = 1000; From 371e8cc154c8bf666755331a68661be65877c797 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 21 Jan 2026 11:20:41 -0800 Subject: [PATCH 318/400] purge translationService --- server/src/config/services.ts | 7 -- .../src/service/system/translationService.js | 92 ------------------- 2 files changed, 99 deletions(-) delete mode 100755 server/src/service/system/translationService.js diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 0cf55590b..b48738866 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -1,4 +1,3 @@ -import TranslationService from "../service/system/translationService.js"; import MongoDB from "../db/MongoDB.js"; import NetworkService from "../service/infrastructure/networkService.js"; import EmailService from "../service/infrastructure/emailService.js"; @@ -96,9 +95,7 @@ import { ILogger } from "@/utils/logger.js"; import { EnvConfig } from "@/service/system/settingsService.js"; export type InitializedServices = { - //v1 settingsService: any; - translationService: any; db: any; networkService: any; emailService: any; @@ -138,9 +135,6 @@ export const initializeServices = async ({ envSettings: EnvConfig; settingsService: any; }): Promise => { - const translationService = new TranslationService(logger); - await translationService.initialize(); - // Create DB const inviteModule = new InviteModule({ InviteToken, crypto }); const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, AppSettings }); @@ -289,7 +283,6 @@ export const initializeServices = async ({ const services = { //v1 settingsService, - translationService, db, networkService, emailService, diff --git a/server/src/service/system/translationService.js b/server/src/service/system/translationService.js deleted file mode 100755 index 0d3893852..000000000 --- a/server/src/service/system/translationService.js +++ /dev/null @@ -1,92 +0,0 @@ -import fs from "fs"; -import path from "path"; - -class TranslationService { - static SERVICE_NAME = "TranslationService"; - constructor(logger) { - this.logger = logger; - this.translations = {}; - this._language = "en"; - this.localesDir = path.join(process.cwd(), "src/locales"); - } - - get serviceName() { - return TranslationService.SERVICE_NAME; - } - - setLanguage(language) { - this._language = language; - } - - get language() { - return this._language; - } - - async initialize() { - try { - await this.loadFromFiles(); - } catch (error) { - this.logger.error({ - message: error.message, - service: "TranslationService", - method: "initialize", - stack: error.stack, - }); - } - } - - async loadFromFiles() { - try { - if (!fs.existsSync(this.localesDir)) { - return false; - } - - const files = fs.readdirSync(this.localesDir).filter((file) => file.endsWith(".json")); - - if (files.length === 0) { - return false; - } - - for (const file of files) { - const language = file.replace(".json", ""); - const filePath = path.join(this.localesDir, file); - const content = fs.readFileSync(filePath, "utf8"); - this.translations[language] = JSON.parse(content); - } - - this.logger.info({ - message: "Translations loaded from files successfully", - service: "TranslationService", - method: "loadFromFiles", - }); - - return true; - } catch (error) { - this.logger.error({ - message: error.message, - service: "TranslationService", - method: "loadFromFiles", - stack: error.stack, - }); - return false; - } - } - - getTranslation(key) { - let language = this._language; - - try { - return this.translations[language]?.[key] || this.translations["en"]?.[key] || key; - } catch (error) { - this.logger.error({ - message: error.message, - service: "TranslationService", - method: "getTranslation", - stack: error.stack, - }); - return key; - } - } -} - -export default TranslationService; From 9fa0113f2b557e2403ca411aba8516150dcc09a5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 21 Jan 2026 11:21:00 -0800 Subject: [PATCH 319/400] remove database.js --- server/src/config/database.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 server/src/config/database.js diff --git a/server/src/config/database.js b/server/src/config/database.js deleted file mode 100644 index e69de29bb..000000000 From 36e02e0a9a09ea2f851465fc64eec5014ceb540e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 21 Jan 2026 11:46:49 -0800 Subject: [PATCH 320/400] team -> ts --- server/src/config/services.ts | 7 ++++ server/src/db/models/Team.js | 14 -------- server/src/db/models/Team.ts | 27 ++++++++++++++ server/src/db/models/index.ts | 3 ++ server/src/repositories/index.ts | 3 ++ .../repositories/teams/ITeamsRepository.ts | 9 +++++ .../teams/MongoTeamsRepository.ts | 36 +++++++++++++++++++ server/src/service/business/userService.ts | 25 +++++++++---- server/src/types/index.ts | 1 + server/src/types/team.ts | 6 ++++ 10 files changed, 110 insertions(+), 21 deletions(-) delete mode 100755 server/src/db/models/Team.js create mode 100755 server/src/db/models/Team.ts create mode 100644 server/src/repositories/teams/ITeamsRepository.ts create mode 100644 server/src/repositories/teams/MongoTeamsRepository.ts create mode 100644 server/src/types/team.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index b48738866..8cfcb400b 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -80,6 +80,7 @@ import { MongoSettingsRepository, MongoNotificationsRepository, MongoIncidentRepository, + MongoTeamsRepository, IMonitorsRepository, IChecksRepository, IMonitorStatsRepository, @@ -90,6 +91,7 @@ import { ISettingsRepository, INotificationsRepository, IIncidentsRepository, + ITeamsRepository, } from "@/repositories/index.js"; import { ILogger } from "@/utils/logger.js"; import { EnvConfig } from "@/service/system/settingsService.js"; @@ -124,6 +126,7 @@ export type InitializedServices = { settingsRepository: ISettingsRepository; notificationsRepository: INotificationsRepository; incidentsRepository: IIncidentsRepository; + teamsRepository: ITeamsRepository; }; export const initializeServices = async ({ @@ -171,6 +174,8 @@ export const initializeServices = async ({ const settingsRepository = new MongoSettingsRepository(); const notificationsRepository = new MongoNotificationsRepository(); const incidentsRepository = new MongoIncidentRepository(); + const teamsRepository = new MongoTeamsRepository(); + const networkService = new NetworkService({ axios, got, @@ -254,6 +259,7 @@ export const initializeServices = async ({ invitesRepository, recoveryTokensRepository, settingsRepository, + teamsRepository, }); const diagnosticService = new DiagnosticService(); @@ -311,6 +317,7 @@ export const initializeServices = async ({ settingsRepository, notificationsRepository, incidentsRepository, + teamsRepository, }; return services; diff --git a/server/src/db/models/Team.js b/server/src/db/models/Team.js deleted file mode 100755 index 95337c201..000000000 --- a/server/src/db/models/Team.js +++ /dev/null @@ -1,14 +0,0 @@ -import mongoose from "mongoose"; -const TeamSchema = mongoose.Schema( - { - email: { - type: String, - required: true, - unique: true, - }, - }, - { - timestamps: true, - } -); -export default mongoose.model("Team", TeamSchema); diff --git a/server/src/db/models/Team.ts b/server/src/db/models/Team.ts new file mode 100755 index 000000000..1b177a108 --- /dev/null +++ b/server/src/db/models/Team.ts @@ -0,0 +1,27 @@ +import { Schema, model, type Types } from "mongoose"; + +import type { Team } from "@/types/index.js"; + +interface TeamDocument extends Team { + _id: Types.ObjectId; + createdAt: Date; + updatedAt: Date; +} + +const TeamSchema = new Schema( + { + email: { + type: String, + required: true, + unique: true, + }, + }, + { + timestamps: true, + } +); +const TeamModel = model("Team", TeamSchema); + +export type { TeamDocument }; +export { TeamModel }; +export default TeamModel; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index 4b71937bb..f762027ec 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -27,3 +27,6 @@ export { default as NotificationModel } from "@/db/models/Notification.js"; export * from "@/db/models/Incident.js"; export { default as IncidentModel } from "@/db/models/Incident.js"; + +export * from "@/db/models/Team.js"; +export { default as TeamModel } from "@/db/models/Team.js"; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index 72dfd2f40..ac5125af9 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -27,3 +27,6 @@ export { default as MongoNotificationsRepository } from "@/repositories/notifica export * from "@/repositories/incidents/IIncidentsRepository.js"; export { default as MongoIncidentRepository } from "@/repositories/incidents/MongoIncidentRepository.js"; + +export * from "@/repositories/teams/ITeamsRepository.js"; +export { default as MongoTeamsRepository } from "@/repositories/teams/MongoTeamsRepository.js"; diff --git a/server/src/repositories/teams/ITeamsRepository.ts b/server/src/repositories/teams/ITeamsRepository.ts new file mode 100644 index 000000000..5ba4b36dd --- /dev/null +++ b/server/src/repositories/teams/ITeamsRepository.ts @@ -0,0 +1,9 @@ +import type { Team } from "@/types/index.js"; +export interface ITeamRepository { + // create + create(email: string): Promise; + // fetch + // update + // delete + // other +} diff --git a/server/src/repositories/teams/MongoTeamsRepository.ts b/server/src/repositories/teams/MongoTeamsRepository.ts new file mode 100644 index 000000000..1a2d8ae57 --- /dev/null +++ b/server/src/repositories/teams/MongoTeamsRepository.ts @@ -0,0 +1,36 @@ +import { Team } from "@/types/index.js"; +import { TeamDocument, TeamModel } from "@/db/models/index.js"; +import { ITeamRepository } from "@/repositories/index.js"; +import mongoose from "mongoose"; + +class MongoTeamRepository implements ITeamRepository { + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + private toEntity = (doc: TeamDocument): Team => { + return { + id: this.toStringId(doc._id), + email: doc.email, + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + create = async (email: string) => { + const team = await TeamModel.create({ email }); + return this.toEntity(team); + }; +} + +export default MongoTeamRepository; diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index f6f489b8e..2c31cff9e 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -1,9 +1,15 @@ -import { IInvitesRepository, IMonitorsRepository, IRecoveryTokensRepository, IUsersRepository, ISettingsRepository } from "@/repositories/index.js"; -import Team from "@/db/models/Team.js"; +import { + IInvitesRepository, + IMonitorsRepository, + IRecoveryTokensRepository, + IUsersRepository, + ISettingsRepository, + ITeamRepository, +} from "@/repositories/index.js"; import type { User } from "@/types/index.js"; import bcrypt from "bcryptjs"; import { AppError } from "@/utils/AppError.js"; -import { ISuperSimpleQueue } from "../infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; +import { ISuperSimpleQueue } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; const SERVICE_NAME = "userService"; @@ -21,6 +27,7 @@ class UserService { private invitesRepository: IInvitesRepository; private recoveryTokensRepository: IRecoveryTokensRepository; private settingsRepository: ISettingsRepository; + private teamsRepository: ITeamRepository; constructor({ crypto, @@ -34,6 +41,7 @@ class UserService { invitesRepository, recoveryTokensRepository, settingsRepository, + teamsRepository, }: { crypto: any; emailService: any; @@ -47,6 +55,7 @@ class UserService { invitesRepository: IInvitesRepository; recoveryTokensRepository: IRecoveryTokensRepository; settingsRepository: ISettingsRepository; + teamsRepository: ITeamRepository; }) { this.emailService = emailService; this.settingsService = settingsService; @@ -59,6 +68,7 @@ class UserService { this.invitesRepository = invitesRepository; this.recoveryTokensRepository = recoveryTokensRepository; this.settingsRepository = settingsRepository; + this.teamsRepository = teamsRepository; } get serviceName() { @@ -85,10 +95,11 @@ class UserService { const jwtSecret = this.crypto.randomBytes(64).toString("hex"); await this.settingsRepository.update({ jwtSecret }); // Create a new team - const team = new Team({ - email: user.email, - }); - user.teamId = team._id; + if (!user.email) { + throw new AppError({ message: "Email is required for first user", service: SERVICE_NAME, method: "registerUser", status: 400 }); + } + const team = await this.teamsRepository.create(user.email); + user.teamId = team.id; } const newUser = await this.usersRepository.create({ ...user }, file); diff --git a/server/src/types/index.ts b/server/src/types/index.ts index b0b6b07c5..9464622ac 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -11,3 +11,4 @@ export * from "@/types/notification.js"; export * from "@/types/alert.js"; export * from "@/types/incident.js"; export * from "@/types/email.js"; +export * from "@/types/team.js"; diff --git a/server/src/types/team.ts b/server/src/types/team.ts new file mode 100644 index 000000000..1bca943a0 --- /dev/null +++ b/server/src/types/team.ts @@ -0,0 +1,6 @@ +export interface Team { + id: string; + email: string; + createdAt: string; + updatedAt: string; +} From 5b96075c0358854e1849b3e5b97a6b993696fe82 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 21 Jan 2026 11:56:05 -0800 Subject: [PATCH 321/400] build errs --- server/src/db/models/Team.ts | 3 ++- server/src/repositories/teams/ITeamsRepository.ts | 2 +- server/src/repositories/teams/MongoTeamsRepository.ts | 6 +++--- server/src/service/business/userService.ts | 6 +++--- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/server/src/db/models/Team.ts b/server/src/db/models/Team.ts index 1b177a108..608f90ad8 100755 --- a/server/src/db/models/Team.ts +++ b/server/src/db/models/Team.ts @@ -2,7 +2,8 @@ import { Schema, model, type Types } from "mongoose"; import type { Team } from "@/types/index.js"; -interface TeamDocument extends Team { +type TeamDocumentBase = Omit & {}; +interface TeamDocument extends TeamDocumentBase { _id: Types.ObjectId; createdAt: Date; updatedAt: Date; diff --git a/server/src/repositories/teams/ITeamsRepository.ts b/server/src/repositories/teams/ITeamsRepository.ts index 5ba4b36dd..9ccdd97fb 100644 --- a/server/src/repositories/teams/ITeamsRepository.ts +++ b/server/src/repositories/teams/ITeamsRepository.ts @@ -1,5 +1,5 @@ import type { Team } from "@/types/index.js"; -export interface ITeamRepository { +export interface ITeamsRepository { // create create(email: string): Promise; // fetch diff --git a/server/src/repositories/teams/MongoTeamsRepository.ts b/server/src/repositories/teams/MongoTeamsRepository.ts index 1a2d8ae57..93a306787 100644 --- a/server/src/repositories/teams/MongoTeamsRepository.ts +++ b/server/src/repositories/teams/MongoTeamsRepository.ts @@ -1,9 +1,9 @@ import { Team } from "@/types/index.js"; import { TeamDocument, TeamModel } from "@/db/models/index.js"; -import { ITeamRepository } from "@/repositories/index.js"; +import { ITeamsRepository } from "@/repositories/index.js"; import mongoose from "mongoose"; -class MongoTeamRepository implements ITeamRepository { +class MongoTeamsRepository implements ITeamsRepository { private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { if (!value) { return ""; @@ -33,4 +33,4 @@ class MongoTeamRepository implements ITeamRepository { }; } -export default MongoTeamRepository; +export default MongoTeamsRepository; diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index 2c31cff9e..b9883f594 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -4,7 +4,7 @@ import { IRecoveryTokensRepository, IUsersRepository, ISettingsRepository, - ITeamRepository, + ITeamsRepository, } from "@/repositories/index.js"; import type { User } from "@/types/index.js"; import bcrypt from "bcryptjs"; @@ -27,7 +27,7 @@ class UserService { private invitesRepository: IInvitesRepository; private recoveryTokensRepository: IRecoveryTokensRepository; private settingsRepository: ISettingsRepository; - private teamsRepository: ITeamRepository; + private teamsRepository: ITeamsRepository; constructor({ crypto, @@ -55,7 +55,7 @@ class UserService { invitesRepository: IInvitesRepository; recoveryTokensRepository: IRecoveryTokensRepository; settingsRepository: ISettingsRepository; - teamsRepository: ITeamRepository; + teamsRepository: ITeamsRepository; }) { this.emailService = emailService; this.settingsService = settingsService; From 09513696ee585dd11e44939d5fe17dc20c314502 Mon Sep 17 00:00:00 2001 From: karenvicent Date: Wed, 21 Jan 2026 15:33:00 -0500 Subject: [PATCH 322/400] detail modal fixes and filters options --- .../Components/IncidentDetailsModal/index.jsx | 2 - .../IncidentTable/IncidentActionsMenu.jsx | 16 +- .../Components/IncidentTable/index.jsx | 5 +- .../Components/LatestIncidentsPanel/index.jsx | 2 +- .../Components/OptionsHeader/index.jsx | 1 + .../ResolveIncidentDialog/index.jsx | 8 +- .../Incidents/hooks/useFetchIncidents.js | 26 +- client/src/Pages/Incidents/index.jsx | 8 + .../Components/IncidentDetailsModal/index.jsx | 406 ------------------ .../IncidentTable/IncidentActionsMenu.jsx | 154 ------- .../ResolveIncidentDialog/index.jsx | 105 ----- client/src/locales/en.json | 1 + 12 files changed, 49 insertions(+), 685 deletions(-) delete mode 100644 client/src/Pages/Incidents2/Components/IncidentDetailsModal/index.jsx delete mode 100644 client/src/Pages/Incidents2/Components/IncidentTable/IncidentActionsMenu.jsx delete mode 100644 client/src/Pages/Incidents2/Components/ResolveIncidentDialog/index.jsx diff --git a/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx index 1290c69ff..e4dad7c3e 100644 --- a/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx +++ b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx @@ -168,7 +168,6 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => { > @@ -188,7 +187,6 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => { > diff --git a/client/src/Pages/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx b/client/src/Pages/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx index 1e9a3be85..28b3a4f52 100644 --- a/client/src/Pages/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx +++ b/client/src/Pages/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx @@ -2,7 +2,7 @@ import IconButton from "@mui/material/IconButton"; import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; -//import Settings from "../../../../assets/icons/settings-bold.svg?react"; +import Icon from "@/Components/v1/Icon/index.jsx"; import ResolveIncidentDialog from "../ResolveIncidentDialog/index.jsx"; import useFetchIncidents from "../../hooks/useFetchIncidents"; @@ -51,7 +51,7 @@ const IncidentActionsMenu = ({ incident, onResolve, onOpenDetails }) => { e.stopPropagation(); closeMenu(e); if (onOpenDetails) { - onOpenDetails(incident._id); + onOpenDetails(incident.id); } }; @@ -59,8 +59,7 @@ const IncidentActionsMenu = ({ incident, onResolve, onOpenDetails }) => { e.stopPropagation(); closeMenu(e); try { - const incidentData = await fetchIncidentById(incident._id); - console.log(incidentData); + const incidentData = await fetchIncidentById(incident.id); if (incidentData) { const { type, _id: monitorId } = incidentData.monitorId; const path = TypeToPathMap[type]; @@ -94,7 +93,10 @@ const IncidentActionsMenu = ({ incident, onResolve, onOpenDetails }) => { }, }} > - {/* */}M + { @@ -144,7 +146,7 @@ const IncidentActionsMenu = ({ incident, onResolve, onOpenDetails }) => { IncidentActionsMenu.propTypes = { incident: PropTypes.shape({ - _id: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, status: PropTypes.bool.isRequired, }).isRequired, onResolve: PropTypes.func.isRequired, diff --git a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx index 699ae59c4..7d23d8ebd 100644 --- a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx @@ -13,7 +13,7 @@ import { formatDateWithTz } from "../../../../Utils/timeUtils.js"; import { useSelector } from "react-redux"; import PropTypes from "prop-types"; import { useTranslation } from "react-i18next"; -import { Button, Typography, useTheme } from "@mui/material"; +import { Typography, useTheme } from "@mui/material"; const IncidentTable = ({ monitors = [], @@ -36,7 +36,7 @@ const IncidentTable = ({ const handleResolveIncident = async (incidentId, options = {}) => { try { - await resolveIncident(incidentId); + await resolveIncident(incidentId, options); handleUpdateTrigger(); } catch (error) { console.error(t("incidentsPage.errorResolvingIncident"), error); @@ -48,7 +48,6 @@ const IncidentTable = ({ id: "monitorName", content: t("incidentsTableMonitorName"), render: (row) => { - console.log(monitors, row); const monitor = monitors.find((monitor) => monitor.id === row.monitorId); return monitor ? monitor.name : "N/A"; }, diff --git a/client/src/Pages/Incidents/Components/LatestIncidentsPanel/index.jsx b/client/src/Pages/Incidents/Components/LatestIncidentsPanel/index.jsx index d24bcd0ef..ff1ef9417 100644 --- a/client/src/Pages/Incidents/Components/LatestIncidentsPanel/index.jsx +++ b/client/src/Pages/Incidents/Components/LatestIncidentsPanel/index.jsx @@ -84,7 +84,7 @@ const LatestIncidentsPanel = ({ incidents = [], isLoading = false, error = null ) : ( {incidents.map((incident, index) => ( - + {index < incidents.length - 1 && } diff --git a/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx b/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx index 3a0ca1985..8d7439576 100644 --- a/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx +++ b/client/src/Pages/Incidents/Components/OptionsHeader/index.jsx @@ -26,6 +26,7 @@ const OptionsHeader = ({ { _id: "active", name: t("incidentsPage.incidentsOptionsHeaderFilterActive") }, { _id: "resolved", name: t("incidentsPage.incidentsOptionsHeaderFilterResolved") }, { _id: "manual", name: t("incidentsPage.incidentsOptionsHeaderFilterManual") }, + { _id: "automatic", name: t("incidentsPage.incidentsOptionsHeaderFilterAutomatic") }, ]; const stackStyles = { diff --git a/client/src/Pages/Incidents/Components/ResolveIncidentDialog/index.jsx b/client/src/Pages/Incidents/Components/ResolveIncidentDialog/index.jsx index e2be5fed2..3e868b2dc 100644 --- a/client/src/Pages/Incidents/Components/ResolveIncidentDialog/index.jsx +++ b/client/src/Pages/Incidents/Components/ResolveIncidentDialog/index.jsx @@ -13,7 +13,7 @@ const ResolveIncidentDialog = ({ onResolve, onAfterResolve, onResolved, - isLoading, + isLoading = false, }) => { const { t } = useTranslation(); const theme = useTheme(); @@ -90,7 +90,7 @@ const ResolveIncidentDialog = ({ ResolveIncidentDialog.propTypes = { open: PropTypes.bool.isRequired, - incidentId: PropTypes.string.isRequired, + incidentId: PropTypes.string, onClose: PropTypes.func.isRequired, onResolve: PropTypes.func.isRequired, onAfterResolve: PropTypes.func, @@ -98,8 +98,4 @@ ResolveIncidentDialog.propTypes = { onResolved: PropTypes.func, }; -ResolveIncidentDialog.defaultProps = { - isLoading: false, -}; - export default ResolveIncidentDialog; diff --git a/client/src/Pages/Incidents/hooks/useFetchIncidents.js b/client/src/Pages/Incidents/hooks/useFetchIncidents.js index 54874bae7..3de495f8a 100644 --- a/client/src/Pages/Incidents/hooks/useFetchIncidents.js +++ b/client/src/Pages/Incidents/hooks/useFetchIncidents.js @@ -71,7 +71,28 @@ const useFetchIncidents = () => { }, [fetchIncidentsByTeam] ); - + const fetchManualResolvedIncidents = useCallback( + async (config = {}) => { + await fetchIncidentsByTeam({ + status: false, + resolutionType: "manual", + sortOrder: "desc", + ...config, + }); + }, + [fetchIncidentsByTeam] + ); + const fetchAutomaticResolvedIncidents = useCallback( + async (config = {}) => { + await fetchIncidentsByTeam({ + status: false, + resolutionType: "automatic", + sortOrder: "desc", + ...config, + }); + }, + [fetchIncidentsByTeam] + ); /** * Fetch resolved incidents * @@ -154,6 +175,7 @@ const useFetchIncidents = () => { createToast({ body: t("incidentsPage.errorResolvingIncident") }); } } catch (error) { + console.log("cayo a error"); setNetworkError(true); console.error(t("incidentsPage.errorResolvingIncident"), error); const errorMessage = @@ -198,6 +220,8 @@ const useFetchIncidents = () => { fetchIncidentById, resolveIncident, fetchIncidentSummary, + fetchManualResolvedIncidents, + fetchAutomaticResolvedIncidents, }; }; diff --git a/client/src/Pages/Incidents/index.jsx b/client/src/Pages/Incidents/index.jsx index abf24989b..179002e18 100644 --- a/client/src/Pages/Incidents/index.jsx +++ b/client/src/Pages/Incidents/index.jsx @@ -49,6 +49,8 @@ const Incidents2 = () => { fetchActiveIncidents, fetchResolvedIncidents, resolveIncident, + fetchManualResolvedIncidents, + fetchAutomaticResolvedIncidents, } = useFetchIncidents(); const networkError = monitorsNetworkError || incidentsNetworkError; @@ -72,6 +74,10 @@ const Incidents2 = () => { fetchActiveIncidents(config); } else if (filter === "resolved") { fetchResolvedIncidents(config); + } else if (filter === "manual") { + fetchManualResolvedIncidents(config); + } else if (filter === "automatic") { + fetchAutomaticResolvedIncidents(config); } else { fetchIncidents(config); } @@ -85,6 +91,8 @@ const Incidents2 = () => { fetchActiveIncidents, fetchResolvedIncidents, fetchIncidents, + fetchManualResolvedIncidents, + fetchAutomaticResolvedIncidents, ]); useEffect(() => { diff --git a/client/src/Pages/Incidents2/Components/IncidentDetailsModal/index.jsx b/client/src/Pages/Incidents2/Components/IncidentDetailsModal/index.jsx deleted file mode 100644 index 25af835b9..000000000 --- a/client/src/Pages/Incidents2/Components/IncidentDetailsModal/index.jsx +++ /dev/null @@ -1,406 +0,0 @@ -// Components -import { Stack, Typography, Grid, Box, Button, Divider } from "@mui/material"; -import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import SummaryCard from "@/Pages/Incidents2/Components/SummaryCard"; -import ResolveIncidentDialog from "../ResolveIncidentDialog/index.jsx"; - -// Utils -import { useEffect, useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import { formatDateWithTz } from "@/Utils/timeUtils"; -import { useSelector } from "react-redux"; -import PropTypes from "prop-types"; - -// Hooks -import useFetchIncidents from "@/Pages/Incidents2/hooks/useFetchIncidents"; -import useGetIncidentsDuration from "@/Pages/Incidents2/hooks/useGetIncidentsDuration"; - -const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => { - const theme = useTheme(); - const { t } = useTranslation(); - const { fetchIncidentById, resolveIncident } = useFetchIncidents(); - const [incident, setIncident] = useState(null); - const [isLoading, setIsLoading] = useState(false); - const [isResolveDialogOpen, setIsResolveDialogOpen] = useState(false); - const uiTimezone = useSelector((state) => state.ui.timezone); - const isActive = incident?.status === true; - const duration = useGetIncidentsDuration(incident || null, isActive); - - const statusColor = isActive ? theme.palette.error.main : theme.palette.success.main; - const toCapitalLetter = (text) => - typeof text === "string" && text.length > 0 - ? text.charAt(0).toUpperCase() + text.slice(1) - : text; - - const KeyValueRow = ({ label, value, sx = {} }) => ( - - - {label} - - - - {value ?? "-"} - - - - ); - - useEffect(() => { - const loadIncident = async () => { - if (!incidentId || !open) { - setIncident(null); - setIsLoading(false); - return; - } - - setIsLoading(true); - try { - const incidentData = await fetchIncidentById(incidentId); - setIncident(incidentData); - } catch (error) { - console.error("Error fetching incident:", error); - setIncident(null); - } finally { - setIsLoading(false); - } - }; - - loadIncident(); - }, [incidentId, open, fetchIncidentById]); - - const openResolveDialog = () => { - setIsResolveDialogOpen(true); - }; - - const closeResolveDialog = () => { - setIsResolveDialogOpen(false); - }; - - const handleAfterResolve = async () => { - if (!incidentId) return; - const updatedIncident = await fetchIncidentById(incidentId); - if (updatedIncident) { - setIncident(updatedIncident); - } - }; - - const renderContent = () => { - if (isLoading) { - return ( - - ); - } - - if (!incident) { - return ( - - ); - } - - return ( - - - - - - - - {isActive ? t("incidentsPage.active") : t("incidentsPage.resolved")} - - - - - - {t("incidentsPage.incidentItemMonitor")}: - - - {incident?.monitorId?.name || t("incidentsPage.unknownMonitor")} - - - - - {t("incidentsPage.URL")}: - - - {incident?.monitorId?.url || "-"} - - - - - - - - - - - - - - {t("incidentsPage.timeline")} - - - - - {!isActive && ( - - )} - - - - - - - - - - - {isActive - ? t("incidentsPage.errorDetails") - : t("incidentsPage.resolutionDetails")} - - - - - - - - {incident?.message || "-"} - - - {!isActive && ( - <> - - - - {incident?.resolutionType === "manual" && ( - <> - - - - - )} - - )} - - - - - - - - ); - }; - - return ( - - - {renderContent()} - - {isActive && ( - - )} - - - - - - - ); -}; - -IncidentDetailsModal.propTypes = { - open: PropTypes.bool.isRequired, - incidentId: PropTypes.string, - onClose: PropTypes.func.isRequired, - onResolved: PropTypes.func, -}; - -export default IncidentDetailsModal; diff --git a/client/src/Pages/Incidents2/Components/IncidentTable/IncidentActionsMenu.jsx b/client/src/Pages/Incidents2/Components/IncidentTable/IncidentActionsMenu.jsx deleted file mode 100644 index 2902da049..000000000 --- a/client/src/Pages/Incidents2/Components/IncidentTable/IncidentActionsMenu.jsx +++ /dev/null @@ -1,154 +0,0 @@ -// Components -import IconButton from "@mui/material/IconButton"; -import Menu from "@mui/material/Menu"; -import MenuItem from "@mui/material/MenuItem"; -import Settings from "../../../../assets/icons/settings-bold.svg?react"; -import ResolveIncidentDialog from "../ResolveIncidentDialog/index.jsx"; -import useFetchIncidents from "../../hooks/useFetchIncidents"; - -// Utils -import { useState } from "react"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import { TypeToPathMap } from "@/Utils/monitorUtils.js"; -import { useNavigate } from "react-router-dom"; -import PropTypes from "prop-types"; - -const IncidentActionsMenu = ({ incident, onResolve, onOpenDetails }) => { - const [anchorEl, setAnchorEl] = useState(null); - const [isDialogOpen, setIsDialogOpen] = useState(false); - const theme = useTheme(); - const { t } = useTranslation(); - const { fetchIncidentById } = useFetchIncidents(); - const navigate = useNavigate(); - const openMenu = (event) => { - event.preventDefault(); - event.stopPropagation(); - setAnchorEl(event.currentTarget); - }; - - const closeMenu = (e) => { - if (e) { - e.stopPropagation(); - } - setAnchorEl(null); - }; - - const openResolveDialog = (e) => { - closeMenu(e); - setIsDialogOpen(true); - }; - - const closeDialog = (e) => { - if (e) { - e.stopPropagation(); - } - setIsDialogOpen(false); - }; - - const isActive = incident.status === true; - const handleGoToDetails = (e) => { - e.stopPropagation(); - closeMenu(e); - if (onOpenDetails) { - onOpenDetails(incident._id); - } - }; - - const handleGoToMonitor = async (e) => { - e.stopPropagation(); - closeMenu(e); - try { - const incidentData = await fetchIncidentById(incident._id); - console.log(incidentData); - if (incidentData) { - const { type, _id: monitorId } = incidentData.monitorId; - const path = TypeToPathMap[type]; - if (path && monitorId) { - navigate(`/${path}/${monitorId}`); - } else { - console.error(`Monitor type not found`); - } - } else { - console.error("Monitor information not found in incident data"); - } - } catch (error) { - console.error("Error fetching incident details:", error); - } - }; - - return ( - <> - { - event.stopPropagation(); - openMenu(event); - }} - sx={{ - "&:focus": { - outline: "none", - }, - "& svg path": { - stroke: theme.palette.primary.contrastTextTertiary, - }, - }} - > - - - - - - {t("incidentsPage.incidentsTableActionDetails", "Details")} - - - {t("incidentsPage.incidentsTableActionGoToMonitor", "Go to monitor")} - - {isActive && ( - { - e.stopPropagation(); - openResolveDialog(e); - }} - > - {t("incidentsPage.incidentsTableActionResolveManually")} - - )} - - - - ); -}; - -IncidentActionsMenu.propTypes = { - incident: PropTypes.shape({ - _id: PropTypes.string.isRequired, - status: PropTypes.bool.isRequired, - }).isRequired, - onResolve: PropTypes.func.isRequired, - onOpenDetails: PropTypes.func, -}; - -export default IncidentActionsMenu; diff --git a/client/src/Pages/Incidents2/Components/ResolveIncidentDialog/index.jsx b/client/src/Pages/Incidents2/Components/ResolveIncidentDialog/index.jsx deleted file mode 100644 index e2be5fed2..000000000 --- a/client/src/Pages/Incidents2/Components/ResolveIncidentDialog/index.jsx +++ /dev/null @@ -1,105 +0,0 @@ -import { useEffect, useState } from "react"; -import PropTypes from "prop-types"; -import { Button, Stack } from "@mui/material"; -import { useTranslation } from "react-i18next"; -import { useTheme } from "@emotion/react"; -import { GenericDialog } from "@/Components/v1/Dialog/genericDialog.jsx"; -import TextInput from "@/Components/v1/Inputs/TextInput/index.jsx"; - -const ResolveIncidentDialog = ({ - open, - incidentId, - onClose, - onResolve, - onAfterResolve, - onResolved, - isLoading, -}) => { - const { t } = useTranslation(); - const theme = useTheme(); - const [comment, setComment] = useState(""); - const [isSubmitting, setIsSubmitting] = useState(false); - - useEffect(() => { - if (open) { - setComment(""); - } - }, [open]); - - const handleConfirm = async () => { - if (!incidentId) return; - const trimmed = comment.trim(); - try { - setIsSubmitting(true); - await onResolve(incidentId, trimmed ? { comment: trimmed } : {}); - if (onAfterResolve) { - await onAfterResolve(); - } - onResolved?.(); - onClose(); - } catch (error) { - console.error("Error resolving incident:", error); - } finally { - setIsSubmitting(false); - } - }; - - return ( - - - setComment(e.target.value)} - maxWidth="100%" - /> - - - - - - - ); -}; - -ResolveIncidentDialog.propTypes = { - open: PropTypes.bool.isRequired, - incidentId: PropTypes.string.isRequired, - onClose: PropTypes.func.isRequired, - onResolve: PropTypes.func.isRequired, - onAfterResolve: PropTypes.func, - isLoading: PropTypes.bool, - onResolved: PropTypes.func, -}; - -ResolveIncidentDialog.defaultProps = { - isLoading: false, -}; - -export default ResolveIncidentDialog; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 9b5ac626f..cc5789e7f 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1202,6 +1202,7 @@ "noIncidentIdProvided": "No incident ID provided", "errorFetchingIncidentSummary": "Error fetching incident summary", "incidentsOptionsHeaderFilterManual": "Manual", + "incidentsOptionsHeaderFilterAutomatic": "Automatic", "incidentsOptionsHeaderFilterAll": "All", "incidentsOptionsHeaderFilterActive": "Active", "incidentsOptionsHeaderFilterResolved": "Resolved", From 0cf85f8b3b49e5673e01a77843bdcce849bc9227 Mon Sep 17 00:00:00 2001 From: karenvicent Date: Wed, 21 Jan 2026 15:50:46 -0500 Subject: [PATCH 323/400] add traslation --- client/src/locales/en.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/locales/en.json b/client/src/locales/en.json index cc5789e7f..4f4447b86 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1235,6 +1235,7 @@ "resolvedBy": "Resolved by", "comment": "Comment", "noCommentProvided": "No comment provided", - "loadingIncidentDetails": "Loading incident details..." + "loadingIncidentDetails": "Loading incident details...", + "none": "None" } } From fdc39833282d0432d679ab3d03704e995d396bd1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 21 Jan 2026 13:01:31 -0800 Subject: [PATCH 324/400] remove settingsModule --- server/src/config/services.ts | 16 +- server/src/controllers/settingsController.ts | 4 +- server/src/db/MongoDB.js | 17 +- server/src/db/modules/inviteModule.js | 57 ------ server/src/db/modules/recoveryModule.js | 79 -------- server/src/db/modules/settingsModule.js | 36 ---- server/src/db/modules/userModule.js | 170 ------------------ server/src/index.ts | 8 +- .../settings/ISettingsRepository.ts | 3 + .../settings/MongoSettingsRepository.ts | 21 ++- server/src/service/system/settingsService.ts | 34 ++-- 11 files changed, 56 insertions(+), 389 deletions(-) delete mode 100755 server/src/db/modules/inviteModule.js delete mode 100755 server/src/db/modules/recoveryModule.js delete mode 100755 server/src/db/modules/settingsModule.js delete mode 100755 server/src/db/modules/userModule.js diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 8cfcb400b..664194d82 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -59,13 +59,9 @@ import RecoveryToken from "../db/models/RecoveryToken.js"; import AppSettings from "../db/models/AppSettings.js"; import Incident from "../db/models/Incident.js"; -import InviteModule from "../db/modules/inviteModule.js"; import StatusPageModule from "../db/modules/statusPageModule.js"; -import UserModule from "../db/modules/userModule.js"; import MaintenanceWindowModule from "../db/modules/maintenanceWindowModule.js"; import NotificationModule from "../db/modules/notificationModule.js"; -import RecoveryModule from "../db/modules/recoveryModule.js"; -import SettingsModule from "../db/modules/settingsModule.js"; import IncidentModule from "../db/modules/incidentModule.js"; // repositories @@ -77,7 +73,6 @@ import { MongoUsersRepository, MongoInvitesRepository, MongoRecoveryTokensRepository, - MongoSettingsRepository, MongoNotificationsRepository, MongoIncidentRepository, MongoTeamsRepository, @@ -133,31 +128,25 @@ export const initializeServices = async ({ logger, envSettings, settingsService, + settingsRepository, }: { logger: ILogger; envSettings: EnvConfig; settingsService: any; + settingsRepository: ISettingsRepository; }): Promise => { // Create DB - const inviteModule = new InviteModule({ InviteToken, crypto }); const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, AppSettings }); - const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean }); const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); const notificationModule = new NotificationModule({ Notification: NotificationModel, Monitor }); - const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto }); - const settingsModule = new SettingsModule({ AppSettings }); const incidentModule = new IncidentModule({ logger, Incident, Monitor, User }); const db = new MongoDB({ logger, envSettings, - inviteModule, statusPageModule, - userModule, maintenanceWindowModule, notificationModule, - recoveryModule, - settingsModule, incidentModule, }); @@ -171,7 +160,6 @@ export const initializeServices = async ({ const usersRepository = new MongoUsersRepository(); const invitesRepository = new MongoInvitesRepository(); const recoveryTokensRepository = new MongoRecoveryTokensRepository(); - const settingsRepository = new MongoSettingsRepository(); const notificationsRepository = new MongoNotificationsRepository(); const incidentsRepository = new MongoIncidentRepository(); const teamsRepository = new MongoTeamsRepository(); diff --git a/server/src/controllers/settingsController.ts b/server/src/controllers/settingsController.ts index 5d3f6ed0e..0a01daa0c 100644 --- a/server/src/controllers/settingsController.ts +++ b/server/src/controllers/settingsController.ts @@ -22,7 +22,7 @@ class SettingsController { buildAppSettings = (dbSettings: any) => { const sanitizedSettings = { ...dbSettings }; delete sanitizedSettings.version; - + delete sanitizedSettings.jwtSecret; const returnSettings = { pagespeedKeySet: false, emailPasswordSet: false, @@ -59,7 +59,7 @@ class SettingsController { updateAppSettings = async (req: Request, res: Response, next: NextFunction) => { await updateAppSettingsBodyValidation.validateAsync(req.body); - const updatedSettings = await this.db.settingsModule.updateAppSettings(req.body); + const updatedSettings = await this.settingsService.updateDbSettings(req.body); const returnSettings = this.buildAppSettings(updatedSettings); return res.status(200).json({ success: true, diff --git a/server/src/db/MongoDB.js b/server/src/db/MongoDB.js index a0c580f1d..eacd2383e 100755 --- a/server/src/db/MongoDB.js +++ b/server/src/db/MongoDB.js @@ -4,26 +4,11 @@ import { runMigrations } from "./migration/index.js"; class MongoDB { static SERVICE_NAME = "MongoDB"; - constructor({ - logger, - envSettings, - inviteModule, - statusPageModule, - userModule, - maintenanceWindowModule, - notificationModule, - recoveryModule, - settingsModule, - incidentModule, - }) { + constructor({ logger, envSettings, statusPageModule, maintenanceWindowModule, notificationModule, incidentModule }) { this.logger = logger; this.envSettings = envSettings; - this.userModule = userModule; - this.inviteModule = inviteModule; - this.recoveryModule = recoveryModule; this.maintenanceWindowModule = maintenanceWindowModule; this.notificationModule = notificationModule; - this.settingsModule = settingsModule; this.statusPageModule = statusPageModule; this.incidentModule = incidentModule; } diff --git a/server/src/db/modules/inviteModule.js b/server/src/db/modules/inviteModule.js deleted file mode 100755 index e3438dc3b..000000000 --- a/server/src/db/modules/inviteModule.js +++ /dev/null @@ -1,57 +0,0 @@ -import { AppError } from "@/utils/AppError.js"; - -const SERVICE_NAME = "inviteModule"; - -class InviteModule { - constructor({ InviteToken, crypto }) { - this.InviteToken = InviteToken; - this.crypto = crypto; - } - - requestInviteToken = async (userData) => { - try { - await this.InviteToken.deleteMany({ email: userData.email }); - userData.token = this.crypto.randomBytes(32).toString("hex"); - let inviteToken = new this.InviteToken(userData); - await inviteToken.save(); - return inviteToken; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "requestInviteToken"; - throw error; - } - }; - - getInviteToken = async (token) => { - try { - const invite = await this.InviteToken.findOne({ - token, - }); - if (invite === null) { - throw new AppError({ message: "Invite token not found", status: 404 }); - } - return invite; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getInviteToken"; - throw error; - } - }; - getInviteTokenAndDelete = async (token) => { - try { - const invite = await this.InviteToken.findOneAndDelete({ - token, - }); - if (invite === null) { - throw new Error("Invite not found"); - } - return invite; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getInviteTokenAndDelete"; - throw error; - } - }; -} - -export default InviteModule; diff --git a/server/src/db/modules/recoveryModule.js b/server/src/db/modules/recoveryModule.js deleted file mode 100755 index b2f9296a6..000000000 --- a/server/src/db/modules/recoveryModule.js +++ /dev/null @@ -1,79 +0,0 @@ -const SERVICE_NAME = "recoveryModule"; - -class RecoveryModule { - constructor({ User, RecoveryToken, crypto }) { - this.User = User; - this.RecoveryToken = RecoveryToken; - this.crypto = crypto; - } - - requestRecoveryToken = async (email) => { - try { - // Delete any existing tokens - await this.RecoveryToken.deleteMany({ email }); - let recoveryToken = new this.RecoveryToken({ - email, - token: this.crypto.randomBytes(32).toString("hex"), - }); - await recoveryToken.save(); - return recoveryToken; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "requestRecoveryToken"; - throw error; - } - }; - validateRecoveryToken = async (candidateToken) => { - try { - const recoveryToken = await this.RecoveryToken.findOne({ - token: candidateToken, - }); - if (recoveryToken !== null) { - return recoveryToken; - } else { - throw new Error("Recovery token not found"); - } - } catch (error) { - error.service = SERVICE_NAME; - error.method = "validateRecoveryToken"; - throw error; - } - }; - - resetPassword = async (password, candidateToken) => { - try { - const newPassword = password; - - // Validate token again - const recoveryToken = await this.validateRecoveryToken(candidateToken); - const user = await this.User.findOne({ email: recoveryToken.email }); - - if (user === null) { - throw new Error("User not found"); - } - - const match = await user.comparePassword(newPassword); - - if (match === true) { - throw new Error("Password cannot be the same as the old password"); - } - - user.password = newPassword; - await user.save(); - await this.RecoveryToken.deleteMany({ email: recoveryToken.email }); - // Fetch the user again without the password - const userWithoutPassword = await this.User.findOne({ - email: recoveryToken.email, - }) - .select("-password") - .select("-profileImage"); - return userWithoutPassword; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "resetPassword"; - throw error; - } - }; -} - -export default RecoveryModule; diff --git a/server/src/db/modules/settingsModule.js b/server/src/db/modules/settingsModule.js deleted file mode 100755 index 37b851bf2..000000000 --- a/server/src/db/modules/settingsModule.js +++ /dev/null @@ -1,36 +0,0 @@ -// import AppSettings from "../../models/AppSettings.js"; -const SERVICE_NAME = "SettingsModule"; - -class SettingsModule { - constructor({ AppSettings }) { - this.AppSettings = AppSettings; - } - - updateAppSettings = async (newSettings) => { - try { - const update = { $set: { ...newSettings } }; - - if (newSettings.pagespeedApiKey === "") { - update.$unset = { pagespeedApiKey: "" }; - delete update.$set.pagespeedApiKey; - } - - if (newSettings.systemEmailPassword === "") { - update.$unset = { systemEmailPassword: "" }; - delete update.$set.systemEmailPassword; - } - - await this.AppSettings.findOneAndUpdate({}, update, { - upsert: true, - }); - const settings = await this.AppSettings.findOne().select("-__v -_id -createdAt -updatedAt -singleton").lean(); - return settings; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "updateAppSettings"; - throw error; - } - }; -} - -export default SettingsModule; diff --git a/server/src/db/modules/userModule.js b/server/src/db/modules/userModule.js deleted file mode 100755 index 0051702b6..000000000 --- a/server/src/db/modules/userModule.js +++ /dev/null @@ -1,170 +0,0 @@ -const SERVICE_NAME = "userModule"; -const DUPLICATE_KEY_CODE = 11000; // MongoDB error code for duplicate key - -class UserModule { - constructor({ User, Team, GenerateAvatarImage, ParseBoolean }) { - this.User = User; - this.Team = Team; - this.GenerateAvatarImage = GenerateAvatarImage; - this.ParseBoolean = ParseBoolean; - } - - checkSuperadmin = async () => { - try { - const superAdmin = await this.User.findOne({ role: "superadmin" }); - if (superAdmin !== null) { - return true; - } - return false; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "checkSuperadmin"; - throw error; - } - }; - - insertUser = async (userData, imageFile) => { - try { - if (imageFile) { - // 1. Save the full size image - userData.profileImage = { - data: imageFile.buffer, - contentType: imageFile.mimetype, - }; - - // 2. Get the avatar sized image - const avatar = await this.GenerateAvatarImage(imageFile); - userData.avatarImage = avatar; - } - - // Handle creating team if superadmin - if (userData.role.includes("superadmin")) { - const team = new this.Team({ - email: userData.email, - }); - userData.teamId = team._id; - userData.checkTTL = 60 * 60 * 24 * 30; - await team.save(); - } - - const newUser = new this.User(userData); - await newUser.save(); - return await this.User.findOne({ _id: newUser._id }).select("-password").select("-profileImage"); // .select() doesn't work with create, need to save then find - } catch (error) { - if (error.code === DUPLICATE_KEY_CODE) { - error.message = "User already exists"; - } - error.service = SERVICE_NAME; - error.method = "insertUser"; - throw error; - } - }; - getUserByEmail = async (email) => { - try { - // Need the password to be able to compare, removed .select() - // We can strip the hash before returning the user - const user = await this.User.findOne({ email: email }).select("-profileImage"); - if (!user) { - throw new Error("User not found"); - } - return user; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getUserByEmail"; - throw error; - } - }; - - updateUser = async ({ userId, user, file }) => { - if (!userId) { - throw new Error("No user in request"); - } - - try { - const candidateUser = { ...user }; - - if (this.ParseBoolean(candidateUser.deleteProfileImage) === true) { - candidateUser.profileImage = null; - candidateUser.avatarImage = null; - } else if (file) { - // 1. Save the full size image - candidateUser.profileImage = { - data: file.buffer, - contentType: file.mimetype, - }; - - // 2. Get the avatar sized image - const avatar = await this.GenerateAvatarImage(file); - candidateUser.avatarImage = avatar; - } - - const updatedUser = await this.User.findByIdAndUpdate( - userId, - candidateUser, - { new: true } // Returns updated user instead of pre-update user - ) - .select("-password") - .select("-profileImage"); - return updatedUser; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "updateUser"; - throw error; - } - }; - deleteUser = async (userId) => { - try { - const deletedUser = await this.User.findByIdAndDelete(userId); - if (!deletedUser) { - throw new Error("User not found"); - } - return deletedUser; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteUser"; - throw error; - } - }; - - getAllUsers = async () => { - try { - const users = await this.User.find().select("-password").select("-profileImage"); - return users; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getAllUsers"; - throw error; - } - }; - - getUserById = async (roles, userId) => { - try { - if (!roles.includes("superadmin")) { - throw new Error("User is not a superadmin"); - } - - const user = await this.User.findById(userId).select("-password").select("-profileImage"); - if (!user) { - throw new Error("User not found"); - } - - return user; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getUserById"; - throw error; - } - }; - - editUserById = async (userId, user) => { - try { - await this.User.findByIdAndUpdate(userId, user, { new: true }).select("-password").select("-profileImage"); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "editUserById"; - throw error; - } - }; -} - -export default UserModule; diff --git a/server/src/index.ts b/server/src/index.ts index b74404228..ca6cea2ba 100755 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -9,7 +9,7 @@ import { runMigrations } from "./db/migration/index.js"; import Logger, { ILogger } from "@/utils/logger.js"; import SettingsService from "@/service/system/settingsService.js"; -import AppSettings from "./db/models/AppSettings.js"; +import { MongoSettingsRepository } from "./repositories/index.js"; const SERVICE_NAME = "Server"; let logger: ILogger; @@ -22,14 +22,16 @@ const startApp = async () => { const frontendPath = path.join(__dirname, "..", "public"); // Create services - const settingsService = new SettingsService(AppSettings); + const settingsRepository = new MongoSettingsRepository(); + const settingsService = new SettingsService(settingsRepository); + const envSettings = settingsService.loadSettings(); // Create logger logger = new Logger({ envSettings }); // Initialize services - const services = await initializeServices({ logger, envSettings, settingsService }); + const services = await initializeServices({ logger, envSettings, settingsService, settingsRepository }); await runMigrations(logger); diff --git a/server/src/repositories/settings/ISettingsRepository.ts b/server/src/repositories/settings/ISettingsRepository.ts index 111a92a55..578d7f40d 100644 --- a/server/src/repositories/settings/ISettingsRepository.ts +++ b/server/src/repositories/settings/ISettingsRepository.ts @@ -1,8 +1,11 @@ import type { Settings } from "@/types/index.js"; export interface ISettingsRepository { // create + create(settings: Partial): Promise; // fetch + findSingleton(): Promise; // update update(settings: Partial): Promise; // delete + deleteLegacy: () => Promise; } diff --git a/server/src/repositories/settings/MongoSettingsRepository.ts b/server/src/repositories/settings/MongoSettingsRepository.ts index 2cc700525..064993fa3 100644 --- a/server/src/repositories/settings/MongoSettingsRepository.ts +++ b/server/src/repositories/settings/MongoSettingsRepository.ts @@ -1,7 +1,8 @@ import mongoose from "mongoose"; import { ISettingsRepository } from "@/repositories/settings/ISettingsRepository.js"; import type { Settings } from "@/types/index.js"; -import { AppSettingsModel, type AppSettingsDocument } from "@/db/models/AppSettings.js"; +import { AppSettingsModel, type AppSettingsDocument } from "@/db/models/index.js"; +import { SERVFAIL } from "dns"; class MongoSettingsRepository implements ISettingsRepository { private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { @@ -46,6 +47,19 @@ class MongoSettingsRepository implements ISettingsRepository { }; }; + create = async (settings: Partial) => { + const newSettings = await AppSettingsModel.create(settings); + return this.toEntity(newSettings); + }; + + findSingleton = async () => { + let settings = await AppSettingsModel.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean(); + if (!settings) { + return null; + } + return this.toEntity(settings); + }; + update = async (settings: Partial) => { const update: Record = { $set: { ...settings } }; @@ -71,6 +85,11 @@ class MongoSettingsRepository implements ISettingsRepository { return this.toEntity(updatedSettings); }; + + deleteLegacy = async () => { + const res = await AppSettingsModel.deleteMany({ version: { $exists: false } }); + return res.deletedCount > 0; + }; } export default MongoSettingsRepository; diff --git a/server/src/service/system/settingsService.ts b/server/src/service/system/settingsService.ts index 15f86995d..a0db6a140 100755 --- a/server/src/service/system/settingsService.ts +++ b/server/src/service/system/settingsService.ts @@ -1,3 +1,7 @@ +import { ISettingsRepository } from "@/repositories/index.js"; +import { Settings } from "@/types/index.js"; +import { AppError } from "@/utils/AppError.js"; + const SERVICE_NAME = "SettingsService"; export type EnvConfig = { @@ -26,16 +30,16 @@ export interface ISettingsService { readonly serviceName: string; loadSettings(): EnvConfig; getSettings(): EnvConfig; - getDBSettings(): Promise>; + getDBSettings(): Promise; } class SettingsService implements ISettingsService { static SERVICE_NAME = "SettingsService"; - private AppSettings: any; private settings: EnvConfig; + private settingsRepository: ISettingsRepository; - constructor(AppSettings: any) { - this.AppSettings = AppSettings; + constructor(settingsRepository: ISettingsRepository) { + this.settingsRepository = settingsRepository; this.settings = { ...envConfig }; } @@ -54,18 +58,26 @@ class SettingsService implements ISettingsService { return this.settings; } - async getDBSettings() { - // Remove any old settings - await this.AppSettings.deleteMany({ version: { $exists: false } }); + updateDbSettings = async (newSettings: Partial) => { + return await this.settingsRepository.update(newSettings); + }; - let settings = await this.AppSettings.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean(); + getDBSettings = async () => { + // Remove any old settings + await this.settingsRepository.deleteLegacy(); + + let settings = await this.settingsRepository.findSingleton(); if (settings === null) { - await this.AppSettings.create({}); - settings = await this.AppSettings.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean(); + await this.settingsRepository.create({}); + settings = await this.settingsRepository.findSingleton(); + } + + if (!settings) { + throw new AppError({ message: "Settings not found", status: 500 }); } return settings; - } + }; } export default SettingsService; From 801b716a03831523c2316263d7f0c6994a2cc31e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 21 Jan 2026 13:01:43 -0800 Subject: [PATCH 325/400] parse settings --- client/src/Pages/Settings/index.jsx | 29 +++++++++++++++++++++++++---- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx index 54d6d4209..621640d8a 100644 --- a/client/src/Pages/Settings/index.jsx +++ b/client/src/Pages/Settings/index.jsx @@ -157,19 +157,40 @@ const Settings = () => { }; const handleSave = () => { - // Validate - const { error } = settingsValidation.validate(settingsData.settings, { + // Validate] + + const toSubmit = { + checkTTL: settingsData.settings.checkTTL, + pagespeedApiKey: settingsData.settings.pagespeedApiKey, + language: settingsData.settings.language, + timezone: settingsData.settings.timezone, + systemEmailHost: settingsData.settings.systemEmailHost, + systemEmailPort: settingsData.settings.systemEmailPort, + systemEmailSecure: settingsData.settings.systemEmailSecure, + systemEmailPool: settingsData.settings.systemEmailPool, + systemEmailAddress: settingsData.settings.systemEmailAddress, + systemEmailPassword: settingsData.settings.systemEmailPassword, + systemEmailUser: settingsData.settings.systemEmailUser, + systemEmailConnectionHost: settingsData.settings.systemEmailConnectionHost, + systemEmailTLSServername: settingsData.settings.systemEmailTLSServername, + systemEmailIgnoreTLS: settingsData.settings.systemEmailIgnoreTLS, + systemEmailRequireTLS: settingsData.settings.systemEmailRequireTLS, + systemEmailRejectUnauthorized: settingsData.settings.systemEmailRejectUnauthorized, + showURL: settingsData.settings.showURL, + globalThresholds: settingsData.settings.globalThresholds, + }; + + const { error } = settingsValidation.validate(toSubmit, { abortEarly: false, }); if (!error || error.details.length === 0) { setErrors({}); - saveSettings(settingsData?.settings); + saveSettings(toSubmit); } else { const newErrors = {}; error.details.forEach((err) => { newErrors[err.path[0]] = err.message; }); - setErrors(newErrors); } }; From b8a5af9c10f183d84b3efc8550f5f01806f117de Mon Sep 17 00:00:00 2001 From: karenvicent Date: Wed, 21 Jan 2026 16:12:42 -0500 Subject: [PATCH 326/400] delete detail incident page --- .../src/Pages/v1/Incidents2/Details/index.jsx | 171 ------------------ 1 file changed, 171 deletions(-) delete mode 100644 client/src/Pages/v1/Incidents2/Details/index.jsx diff --git a/client/src/Pages/v1/Incidents2/Details/index.jsx b/client/src/Pages/v1/Incidents2/Details/index.jsx deleted file mode 100644 index 67388899d..000000000 --- a/client/src/Pages/v1/Incidents2/Details/index.jsx +++ /dev/null @@ -1,171 +0,0 @@ -// Components -import { Stack, Typography, Grid, Box } from "@mui/material"; -import Breadcrumbs from "@/Components/v1/Breadcrumbs/index.jsx"; -import GenericFallback from "@/Components/v1/GenericFallback/index.jsx"; -import SummaryCard from "../Components/SummaryCard"; -import useGetIncidentsDuration from "../hooks/useGetIncidentsDuration"; - -// Utils -import { useParams } from "react-router-dom"; -import { useTheme } from "@emotion/react"; -import { useTranslation } from "react-i18next"; -import useFetchIncidents from "../hooks/useFetchIncidents"; -import { useEffect, useState } from "react"; - -// Constants -const BREADCRUMBS = [ - { name: "incidents", path: "/incidents" }, - { name: "details", path: "" }, -]; - -const IncidentDetails = () => { - const theme = useTheme(); - const { t } = useTranslation(); - const { incidentId } = useParams(); - const { fetchIncidentById } = useFetchIncidents(); - const [incident, setIncident] = useState(null); - const [isLoading, setIsLoading] = useState(true); - const isActive = incident?.status === true; - const duration = useGetIncidentsDuration(incident || null, isActive); - useEffect(() => { - const loadIncident = async () => { - if (incidentId) { - setIsLoading(true); - try { - const incidentData = await fetchIncidentById(incidentId); - console.log(incidentData); - setIncident(incidentData); - } catch (error) { - console.error("Error fetching incident:", error); - } finally { - setIsLoading(false); - } - } - }; - - loadIncident(); - }, [incidentId, fetchIncidentById]); - - if (isLoading) { - return ( - - - - - ); - } - - if (!incident) { - return ( - - - - - ); - } - - return ( - - - - {t("incidentsPage.incidentDetails", "Incident Details")} - - - - - - {duration} - - - - - {incident?.startTime} - - - {!isActive && ( - - - {incident?.endTime} - - - )} - - - - - {incident?.statusCode} - {incident?.message} - - - - - {incident?.checks.length} - - - {!isActive && ( - - - {incident?.resolutionType} - {incident?.resolutionType === "manual" && ( - - - Resolved by {incident?.resolvedBy?.email} - - {incident?.comment && ( - Comment: {incident?.comment} - )} - - )} - - - )} - - - - ); -}; - -export default IncidentDetails; From d627a25b409a5a5efcb7e821e53266a69833d40a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 21 Jan 2026 13:51:04 -0800 Subject: [PATCH 327/400] refactor notifications service --- .../v1/NotificationConfig/index.jsx | 12 ++--- client/src/Hooks/useNotifications.js | 2 +- .../Notifications/components/ActionMenu.jsx | 4 +- .../src/Pages/Notifications/create/index.jsx | 4 +- client/src/Pages/Notifications/index.jsx | 2 +- client/src/Pages/Notifications/utils.js | 12 ++--- server/src/config/services.ts | 1 + .../src/controllers/notificationController.ts | 47 +++++++---------- .../monitors/IMonitorsRepository.ts | 1 + .../monitors/MongoMonitorsRepository.ts | 4 ++ .../notifications/INotificationsRepository.ts | 5 ++ .../MongoNotificationsRepository.ts | 52 +++++++++++++++++++ .../infrastructure/notificationsService.ts | 33 +++++++++++- 13 files changed, 132 insertions(+), 47 deletions(-) diff --git a/client/src/Components/v1/NotificationConfig/index.jsx b/client/src/Components/v1/NotificationConfig/index.jsx index d76fd9fb6..f1ed30ad1 100644 --- a/client/src/Components/v1/NotificationConfig/index.jsx +++ b/client/src/Components/v1/NotificationConfig/index.jsx @@ -31,7 +31,7 @@ const NotificationConfig = ({ setMonitor((prev) => { return { ...prev, - notifications: value.map((notification) => notification._id), + notifications: value.map((notification) => notification.id), }; }); }; @@ -39,14 +39,14 @@ const NotificationConfig = ({ // Handlers const handleDelete = (id) => { const updatedNotifications = selectedNotifications.filter( - (notification) => notification._id !== id + (notification) => notification.id !== id ); setSelectedNotifications(updatedNotifications); setMonitor((prev) => { return { ...prev, - notifications: updatedNotifications.map((notification) => notification._id), + notifications: updatedNotifications.map((notification) => notification.id), }; }); }; @@ -57,7 +57,7 @@ const NotificationConfig = ({ useEffect(() => { if (setNotifications) { const toSet = setNotifications.map((notification) => { - return notifications.find((n) => n._id === notification); + return notifications.find((n) => n.id === notification); }); setSelectedNotifications(toSet); } @@ -94,7 +94,7 @@ const NotificationConfig = ({ { - handleDelete(notification._id); + handleDelete(notification.id); }} style={{ cursor: "pointer" }} /> diff --git a/client/src/Hooks/useNotifications.js b/client/src/Hooks/useNotifications.js index 5f2058b6a..6c5919fac 100644 --- a/client/src/Hooks/useNotifications.js +++ b/client/src/Hooks/useNotifications.js @@ -102,7 +102,7 @@ const useGetNotificationById = (id, setNotification) => { const notificationData = { address: notification?.address, notificationName: notification?.notificationName, - type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?._id, + type: NOTIFICATION_TYPES.find((type) => type.value === notification?.type)?.id, }; setNotification(notificationData); diff --git a/client/src/Pages/Notifications/components/ActionMenu.jsx b/client/src/Pages/Notifications/components/ActionMenu.jsx index 52516a737..a3ccc4bbb 100644 --- a/client/src/Pages/Notifications/components/ActionMenu.jsx +++ b/client/src/Pages/Notifications/components/ActionMenu.jsx @@ -32,13 +32,13 @@ const ActionMenu = ({ notification, onDelete }) => { const handleRemove = (e) => { e.stopPropagation(); - onDelete(notification._id); + onDelete(notification.id); handleClose(); }; const handleConfigure = (e) => { e.stopPropagation(); - navigate(`/notifications/${notification._id}`); + navigate(`/notifications/${notification.id}`); handleClose(); }; diff --git a/client/src/Pages/Notifications/create/index.jsx b/client/src/Pages/Notifications/create/index.jsx index 11bf2a95d..14b72ab61 100644 --- a/client/src/Pages/Notifications/create/index.jsx +++ b/client/src/Pages/Notifications/create/index.jsx @@ -56,7 +56,7 @@ const CreateNotifications = () => { const [notification, setNotification] = useState({ notificationName: "", address: "", - type: NOTIFICATION_TYPES[0]._id, + type: NOTIFICATION_TYPES[0].id, }); const [errors, setErrors] = useState({}); const { t } = useTranslation(); @@ -66,7 +66,7 @@ const CreateNotifications = () => { const [isOpen, setIsOpen] = useState(false); const getNotificationTypeValue = (typeId) => { - return NOTIFICATION_TYPES.find((type) => type._id === typeId)?.value || "email"; + return NOTIFICATION_TYPES.find((type) => type.id === typeId)?.value || "email"; }; const extractError = (error, field) => diff --git a/client/src/Pages/Notifications/index.jsx b/client/src/Pages/Notifications/index.jsx index 8ebdb22ec..70ed3ef96 100644 --- a/client/src/Pages/Notifications/index.jsx +++ b/client/src/Pages/Notifications/index.jsx @@ -100,7 +100,7 @@ const Notifications = () => { {t("notifications.createTitle")} navigate(`/notifications/${row._id}`), + onRowClick: (row) => navigate(`/notifications/${row.id}`), rowSX: { cursor: "pointer", "&:hover td": { diff --git a/client/src/Pages/Notifications/utils.js b/client/src/Pages/Notifications/utils.js index 2a3d32f4d..9ad422101 100644 --- a/client/src/Pages/Notifications/utils.js +++ b/client/src/Pages/Notifications/utils.js @@ -1,10 +1,10 @@ export const NOTIFICATION_TYPES = [ - { _id: 1, name: "E-mail", value: "email" }, - { _id: 2, name: "Slack", value: "slack" }, - { _id: 3, name: "PagerDuty", value: "pager_duty" }, - { _id: 4, name: "Webhook", value: "webhook" }, - { _id: 5, name: "Discord", value: "discord" }, - { _id: 6, name: "Matrix", value: "matrix" }, + { id: 1, name: "E-mail", value: "email" }, + { id: 2, name: "Slack", value: "slack" }, + { id: 3, name: "PagerDuty", value: "pager_duty" }, + { id: 4, name: "Webhook", value: "webhook" }, + { id: 5, name: "Discord", value: "discord" }, + { id: 6, name: "Matrix", value: "matrix" }, ]; export const TITLE_MAP = { diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 664194d82..cdee721b2 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -207,6 +207,7 @@ export const initializeServices = async ({ const notificationsService = new NotificationsService( notificationsRepository, + monitorsRepository, webhookProvider, emailProvider, slackProvider, diff --git a/server/src/controllers/notificationController.ts b/server/src/controllers/notificationController.ts index 1569f0986..1aa10a82c 100644 --- a/server/src/controllers/notificationController.ts +++ b/server/src/controllers/notificationController.ts @@ -5,17 +5,16 @@ import { createNotificationBodyValidation } from "@/validation/joi.js"; import { AppError } from "@/utils/AppError.js"; import { IMonitorsRepository } from "@/repositories/index.js"; import { INotificationsService } from "@/service/index.js"; +import { requireTeamId } from "./controllerUtils.js"; const SERVICE_NAME = "NotificationController"; class NotificationController { static SERVICE_NAME = SERVICE_NAME; - private db: any; private notificationsService: INotificationsService; private monitorsRepository: IMonitorsRepository; - constructor(notificationsService: INotificationsService, db: any, monitorsRepository: IMonitorsRepository) { + constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) { this.notificationsService = notificationsService; - this.db = db; this.monitorsRepository = monitorsRepository; } @@ -61,7 +60,7 @@ class NotificationController { body.userId = userId; body.teamId = teamId; - const notification = await this.db.notificationModule.createNotification(body); + const notification = await this.notificationsService.createNotification(body); return res.status(200).json({ success: true, msg: "Notification created successfully", @@ -79,7 +78,7 @@ class NotificationController { throw new AppError({ message: "Team ID is required", status: 400 }); } - const notifications = await this.db.notificationModule.getNotificationsByTeamId(teamId); + const notifications = await this.notificationsService.findNotificationsByTeamId(teamId); return res.status(200).json({ success: true, @@ -98,12 +97,12 @@ class NotificationController { throw new AppError({ message: "Team ID is required", status: 400 }); } - const notification = await this.db.notificationModule.getNotificationById(req.params.id); - if (!notification.teamId.equals(teamId)) { - throw new AppError({ message: "Unauthorized", status: 403 }); + const notificationId = req.params.id; + if (!notificationId) { + throw new AppError({ message: "Notification ID is required", status: 400 }); } - await this.db.notificationModule.deleteNotificationById(req.params.id); + await this.notificationsService.deleteById(notificationId, teamId); return res.status(200).json({ success: true, msg: "Notification deleted successfully", @@ -115,16 +114,14 @@ class NotificationController { getNotificationById = async (req: Request, res: Response, next: NextFunction) => { try { - const notification = await this.db.notificationModule.getNotificationById(req.params.id); - - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); + const teamId = requireTeamId(req.user?.teamId); + const notificationId = req.params.id; + if (!notificationId) { + throw new AppError({ message: "Notification ID is required", status: 400 }); } - if (!notification.teamId.equals(teamId)) { - throw new AppError({ message: "Unauthorized", status: 403 }); - } + const notification = await this.notificationsService.findById(notificationId, teamId); + return res.status(200).json({ success: true, msg: "Notification fetched successfully", @@ -141,18 +138,12 @@ class NotificationController { abortEarly: false, }); - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); + const teamId = requireTeamId(req.user?.teamId); + const notificationId = req.params.id; + if (!notificationId) { + throw new AppError({ message: "Notification ID is required", status: 400 }); } - - const notification = await this.db.notificationModule.getNotificationById(req.params.id); - - if (!notification.teamId.equals(teamId)) { - throw new AppError({ message: "Unauthorized", status: 403 }); - } - - const editedNotification = await this.db.notificationModule.editNotification(req.params.id, req.body); + const editedNotification = await this.notificationsService.updateById(notificationId, teamId, req.body); return res.status(200).json({ success: true, msg: "Notification updated successfully", diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index f29cf9d15..8ead5a688 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -39,4 +39,5 @@ export interface IMonitorsRepository { // other findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise; findGroupsByTeamId(teamId: string): Promise; + removeNotificationFromMonitors(notificationId: string): Promise; } diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 17b9ce243..ad7e8577d 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -189,6 +189,10 @@ class MongoMonitorsRepository implements IMonitorsRepository { return groups.sort(); }; + removeNotificationFromMonitors = async (notificationId: string): Promise => { + await MonitorModel.updateMany({ notifications: notificationId }, { $pull: { notifications: notificationId } }); + }; + private mapDocuments = (documents: MonitorDocument[]): Monitor[] => { if (!documents?.length) { return []; diff --git a/server/src/repositories/notifications/INotificationsRepository.ts b/server/src/repositories/notifications/INotificationsRepository.ts index 8bb65769a..0236c3a89 100644 --- a/server/src/repositories/notifications/INotificationsRepository.ts +++ b/server/src/repositories/notifications/INotificationsRepository.ts @@ -1,8 +1,13 @@ import type { Notification } from "@/types/index.js"; export interface INotificationsRepository { // create + create(notificationData: Partial): Promise; // fetch + findById(id: string, teamId: string): Promise; findNotificationsByIds(ids: string[]): Promise; + findByTeamId(teamId: string): Promise; // update + updateById(id: string, teamId: string, updateData: Partial): Promise; // delete + deleteById(id: string, teamId: string): Promise; } diff --git a/server/src/repositories/notifications/MongoNotificationsRepository.ts b/server/src/repositories/notifications/MongoNotificationsRepository.ts index 01a514f1c..730072d8c 100644 --- a/server/src/repositories/notifications/MongoNotificationsRepository.ts +++ b/server/src/repositories/notifications/MongoNotificationsRepository.ts @@ -2,6 +2,8 @@ import mongoose from "mongoose"; import { NotificationModel, type NotificationDocument } from "@/db/models/index.js"; import { INotificationsRepository } from "@/repositories/index.js"; import type { Notification } from "@/types/index.js"; +import { AppError } from "@/utils/AppError.js"; +import { not } from "joi"; class MongoNotificationsRepository implements INotificationsRepository { private mapDocuments = (documents: NotificationDocument[]): Notification[] => { @@ -36,11 +38,61 @@ class MongoNotificationsRepository implements INotificationsRepository { }; }; + create = async (notificationData: Partial) => { + const notification = await NotificationModel.create({ ...notificationData }); + if (!notification) { + throw new AppError({ message: "Failed to create notification", status: 500 }); + } + return this.toEntity(notification); + }; + + findById = async (id: string, teamId: string): Promise => { + const notification = await NotificationModel.findOne({ + _id: new mongoose.Types.ObjectId(id), + teamId: new mongoose.Types.ObjectId(teamId), + }); + if (!notification) { + throw new AppError({ message: "Notification not found", status: 404 }); + } + return this.toEntity(notification); + }; + findNotificationsByIds = async (ids: string[]) => { const mongoIds = ids.map((id) => new mongoose.Types.ObjectId(id)); const documents = await NotificationModel.find({ _id: { $in: mongoIds } }); return this.mapDocuments(documents); }; + + findByTeamId = async (teamId: string): Promise => { + const documents = await NotificationModel.find({ teamId }); + return this.mapDocuments(documents); + }; + + updateById = async (id: string, teamId: string, patch: Partial): Promise => { + const notification = await NotificationModel.findOneAndUpdate( + { + _id: new mongoose.Types.ObjectId(id), + teamId: new mongoose.Types.ObjectId(teamId), + }, + { $set: patch }, + { new: true, runValidators: true } + ); + if (!notification) { + throw new AppError({ message: "Notification not found or could not be updated", status: 404 }); + } + return this.toEntity(notification); + }; + + deleteById = async (id: string, teamId: string): Promise => { + const deleted = await NotificationModel.findOneAndDelete({ + _id: new mongoose.Types.ObjectId(id), + teamId: new mongoose.Types.ObjectId(teamId), + }); + if (!deleted) { + throw new AppError({ message: "Notification not found or could not be deleted", status: 404 }); + } + return this.toEntity(deleted); + }; } export default MongoNotificationsRepository; diff --git a/server/src/service/infrastructure/notificationsService.ts b/server/src/service/infrastructure/notificationsService.ts index 7ca52945e..4e0c9d842 100644 --- a/server/src/service/infrastructure/notificationsService.ts +++ b/server/src/service/infrastructure/notificationsService.ts @@ -1,14 +1,20 @@ import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification } from "@/types/index.js"; import { shouldSendHardwareAlert } from "@/service/infrastructure/notificationProviders/utils.js"; -import { INotificationsRepository } from "@/repositories/index.js"; +import { IMonitorsRepository, INotificationsRepository } from "@/repositories/index.js"; import { INotificationProvider } from "./notificationProviders/INotificationProvider.js"; export interface INotificationsService { + createNotification: (notificationData: Partial) => Promise; + findById: (id: string, teamId: string) => Promise; + findNotificationsByTeamId: (teamId: string) => Promise; + updateById(id: string, teamId: string, updateData: Partial): Promise; + deleteById: (id: string, teamId: string) => Promise; handleNotifications: ( monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, prevStatus: boolean | undefined, statusChanged: boolean ) => Promise; + sendTestNotification: (notification: Notification) => Promise; testAllNotifications: (notificationIds: string[]) => Promise; } @@ -19,6 +25,7 @@ export class NotificationsService implements INotificationsService { static SERVICE_NAME = SERVICE_NAME; private notificationsRepository: INotificationsRepository; + private monitorsRepository: IMonitorsRepository; private webhookProvider: INotificationProvider; private emailProvider: INotificationProvider; private slackProvider: INotificationProvider; @@ -29,6 +36,7 @@ export class NotificationsService implements INotificationsService { constructor( notificationsRepository: INotificationsRepository, + monitorsRepository: IMonitorsRepository, webhookProvider: INotificationProvider, emailProvider: INotificationProvider, slackProvider: INotificationProvider, @@ -38,6 +46,7 @@ export class NotificationsService implements INotificationsService { logger: any ) { this.notificationsRepository = notificationsRepository; + this.monitorsRepository = monitorsRepository; this.webhookProvider = webhookProvider; this.emailProvider = emailProvider; this.slackProvider = slackProvider; @@ -147,4 +156,26 @@ export class NotificationsService implements INotificationsService { } return true; }; + + createNotification = async (notificationData: Partial): Promise => { + return await this.notificationsRepository.create(notificationData); + }; + + findById = async (id: string, teamId: string): Promise => { + return await this.notificationsRepository.findById(id, teamId); + }; + + findNotificationsByTeamId = async (teamId: string): Promise => { + return await this.notificationsRepository.findByTeamId(teamId); + }; + + updateById = async (id: string, teamId: string, updateData: Partial): Promise => { + return await this.notificationsRepository.updateById(id, teamId, updateData); + }; + + deleteById = async (id: string, teamId: string): Promise => { + const deleted = await this.notificationsRepository.deleteById(id, teamId); + await this.monitorsRepository.removeNotificationFromMonitors(id); + return deleted; + }; } From 155a2577d0890f5646054ce4eee98cbac540342e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 21 Jan 2026 13:55:22 -0800 Subject: [PATCH 328/400] remove unused dep --- server/src/config/controllers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/config/controllers.ts b/server/src/config/controllers.ts index d72e4f76f..3b81d27db 100644 --- a/server/src/config/controllers.ts +++ b/server/src/config/controllers.ts @@ -37,7 +37,7 @@ export const initializeControllers = (services: InitializedServices): Initialize queueController: new QueueController(services.jobQueue), logController: new LogController(services.logger), statusPageController: new StatusPageController(services.db), - notificationController: new NotificationController(services.notificationsService, services.db, services.monitorsRepository), + notificationController: new NotificationController(services.notificationsService, services.monitorsRepository), diagnosticController: new DiagnosticController(services.diagnosticService), incidentController: new IncidentController(services.incidentService), }; From 117c22fae4f1661d91693c38a45b7440b306c6a3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 21 Jan 2026 14:22:53 -0800 Subject: [PATCH 329/400] initial commit --- server/src/config/services.ts | 3 - server/src/db/MongoDB.js | 3 +- server/src/db/modules/notificationModule.js | 98 ------------------- server/src/repositories/index.ts | 3 + .../IMaintenanceWindowsRepository.ts | 1 + .../MongoMaintenanceWindowsRepository.ts | 5 + server/src/types/maintenanceWindow.ts | 45 +++++++++ 7 files changed, 55 insertions(+), 103 deletions(-) delete mode 100755 server/src/db/modules/notificationModule.js create mode 100644 server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts create mode 100644 server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts create mode 100644 server/src/types/maintenanceWindow.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index cdee721b2..cec2ad2c3 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -61,7 +61,6 @@ import Incident from "../db/models/Incident.js"; import StatusPageModule from "../db/modules/statusPageModule.js"; import MaintenanceWindowModule from "../db/modules/maintenanceWindowModule.js"; -import NotificationModule from "../db/modules/notificationModule.js"; import IncidentModule from "../db/modules/incidentModule.js"; // repositories @@ -138,7 +137,6 @@ export const initializeServices = async ({ // Create DB const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, AppSettings }); const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); - const notificationModule = new NotificationModule({ Notification: NotificationModel, Monitor }); const incidentModule = new IncidentModule({ logger, Incident, Monitor, User }); const db = new MongoDB({ @@ -146,7 +144,6 @@ export const initializeServices = async ({ envSettings, statusPageModule, maintenanceWindowModule, - notificationModule, incidentModule, }); diff --git a/server/src/db/MongoDB.js b/server/src/db/MongoDB.js index eacd2383e..ce70582df 100755 --- a/server/src/db/MongoDB.js +++ b/server/src/db/MongoDB.js @@ -4,11 +4,10 @@ import { runMigrations } from "./migration/index.js"; class MongoDB { static SERVICE_NAME = "MongoDB"; - constructor({ logger, envSettings, statusPageModule, maintenanceWindowModule, notificationModule, incidentModule }) { + constructor({ logger, envSettings, statusPageModule, maintenanceWindowModule, incidentModule }) { this.logger = logger; this.envSettings = envSettings; this.maintenanceWindowModule = maintenanceWindowModule; - this.notificationModule = notificationModule; this.statusPageModule = statusPageModule; this.incidentModule = incidentModule; } diff --git a/server/src/db/modules/notificationModule.js b/server/src/db/modules/notificationModule.js deleted file mode 100755 index f45900184..000000000 --- a/server/src/db/modules/notificationModule.js +++ /dev/null @@ -1,98 +0,0 @@ -const SERVICE_NAME = "notificationModule"; - -class NotificationModule { - constructor({ Notification, Monitor }) { - this.Notification = Notification; - this.Monitor = Monitor; - } - createNotification = async (notificationData) => { - try { - const notification = await new this.Notification({ ...notificationData }).save(); - return notification; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createNotification"; - throw error; - } - }; - getNotificationsByTeamId = async (teamId) => { - try { - const notifications = await this.Notification.find({ teamId }); - return notifications; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getNotificationsByTeamId"; - throw error; - } - }; - getNotificationsByIds = async (notificationIds) => { - try { - const notifications = await this.Notification.find({ _id: { $in: notificationIds } }); - return notifications; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getNotificationsByIds"; - throw error; - } - }; - getNotificationsByMonitorId = async (monitorId) => { - try { - const notifications = await this.Notification.find({ monitorId }); - return notifications; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getNotificationsByMonitorId"; - throw error; - } - }; - deleteNotificationsByMonitorId = async (monitorId) => { - try { - const result = await this.Notification.deleteMany({ monitorId }); - return result.deletedCount; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteNotificationsByMonitorId"; - throw error; - } - }; - deleteNotificationById = async (id) => { - try { - const notification = await this.Notification.findById(id); - if (!notification) { - throw new Error("Notification not found"); - } - - const result = await this.Notification.findByIdAndDelete(id); - await this.Monitor.updateMany({ notifications: id }, { $pull: { notifications: id } }); - return result; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteNotificationById"; - throw error; - } - }; - getNotificationById = async (id) => { - try { - const notification = await this.Notification.findById(id); - return notification; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getNotificationById"; - throw error; - } - }; - editNotification = async (id, notificationData) => { - try { - const notification = await this.Notification.findByIdAndUpdate(id, notificationData, { - new: true, - }); - return notification; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "editNotification"; - throw error; - } - }; -} - -export default NotificationModule; diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts index ac5125af9..da31a8c75 100644 --- a/server/src/repositories/index.ts +++ b/server/src/repositories/index.ts @@ -30,3 +30,6 @@ export { default as MongoIncidentRepository } from "@/repositories/incidents/Mon export * from "@/repositories/teams/ITeamsRepository.js"; export { default as MongoTeamsRepository } from "@/repositories/teams/MongoTeamsRepository.js"; + +export * from "@/repositories/maintenance-windows/IMaintenanceWindowsRepository.js"; +export { default as MongoMaintenanceWindowsRepository } from "@/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.js"; diff --git a/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts b/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts new file mode 100644 index 000000000..b69d79a2d --- /dev/null +++ b/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts @@ -0,0 +1 @@ +export interface IMaintenanceWindowsRepository {} diff --git a/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts b/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts new file mode 100644 index 000000000..fab135da4 --- /dev/null +++ b/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts @@ -0,0 +1,5 @@ +import { IMaintenanceWindowsRepository } from "./IMaintenanceWindowsRepository.js"; + +class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository {} + +export default MongoMaintenanceWindowsRepository; diff --git a/server/src/types/maintenanceWindow.ts b/server/src/types/maintenanceWindow.ts new file mode 100644 index 000000000..5199d613a --- /dev/null +++ b/server/src/types/maintenanceWindow.ts @@ -0,0 +1,45 @@ +export interface MaintenanceWindow { + monitorId: String; + teamId: string; + active: boolean; +} +const MaintenanceWindow = mongoose.Schema( + { + monitorId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Monitor", + immutable: true, + }, + teamId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Team", + immutable: true, + }, + active: { + type: Boolean, + default: true, + }, + name: { + type: String, + }, + repeat: { + type: Number, + }, + start: { + type: Date, + }, + end: { + type: Dat, + }, + expiry: { + type: Date, + index: { expires: "0s" }, + }, + }, + + { + timestamps: true, + } +); + +export default mongoose.model("MaintenanceWindow", MaintenanceWindow); From 82cc289f53167a59aee9f2a9744002b109f17231 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 21 Jan 2026 14:26:17 -0800 Subject: [PATCH 330/400] mw type --- server/src/types/maintenanceWindow.ts | 46 ++++----------------------- 1 file changed, 6 insertions(+), 40 deletions(-) diff --git a/server/src/types/maintenanceWindow.ts b/server/src/types/maintenanceWindow.ts index 5199d613a..bb7d8f2f9 100644 --- a/server/src/types/maintenanceWindow.ts +++ b/server/src/types/maintenanceWindow.ts @@ -2,44 +2,10 @@ export interface MaintenanceWindow { monitorId: String; teamId: string; active: boolean; + name: string; + repeat: number; + start: string; + end: string; + createdAt: string; + updatedAt: string; } -const MaintenanceWindow = mongoose.Schema( - { - monitorId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - immutable: true, - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - }, - active: { - type: Boolean, - default: true, - }, - name: { - type: String, - }, - repeat: { - type: Number, - }, - start: { - type: Date, - }, - end: { - type: Dat, - }, - expiry: { - type: Date, - index: { expires: "0s" }, - }, - }, - - { - timestamps: true, - } -); - -export default mongoose.model("MaintenanceWindow", MaintenanceWindow); From 9962866ba5a3e06379f41d25c03bba3b7a613af5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 21 Jan 2026 23:26:13 +0000 Subject: [PATCH 331/400] refactor --- server/src/config/services.ts | 7 +- .../maintenanceWindowController.ts | 34 ++---- server/src/db/models/MaintenanceWindow.js | 68 ------------ server/src/db/models/MaintenanceWindow.ts | 63 +++++++++++ server/src/db/models/index.ts | 3 + .../IMaintenanceWindowsRepository.ts | 15 ++- .../MongoMaintenanceWindowsRepository.ts | 100 +++++++++++++++++- .../business/maintenanceWindowService.ts | 33 ++++-- server/src/types/index.ts | 1 + server/src/types/maintenanceWindow.ts | 1 + 10 files changed, 218 insertions(+), 107 deletions(-) delete mode 100755 server/src/db/models/MaintenanceWindow.js create mode 100644 server/src/db/models/MaintenanceWindow.ts diff --git a/server/src/config/services.ts b/server/src/config/services.ts index cec2ad2c3..9076e226d 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -75,6 +75,7 @@ import { MongoNotificationsRepository, MongoIncidentRepository, MongoTeamsRepository, + MongoMaintenanceWindowsRepository, IMonitorsRepository, IChecksRepository, IMonitorStatsRepository, @@ -86,6 +87,7 @@ import { INotificationsRepository, IIncidentsRepository, ITeamsRepository, + IMaintenanceWindowsRepository, } from "@/repositories/index.js"; import { ILogger } from "@/utils/logger.js"; import { EnvConfig } from "@/service/system/settingsService.js"; @@ -121,6 +123,7 @@ export type InitializedServices = { notificationsRepository: INotificationsRepository; incidentsRepository: IIncidentsRepository; teamsRepository: ITeamsRepository; + maintenanceWindowsRepository: IMaintenanceWindowsRepository; }; export const initializeServices = async ({ @@ -160,6 +163,7 @@ export const initializeServices = async ({ const notificationsRepository = new MongoNotificationsRepository(); const incidentsRepository = new MongoIncidentRepository(); const teamsRepository = new MongoTeamsRepository(); + const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository(); const networkService = new NetworkService({ axios, @@ -256,9 +260,9 @@ export const initializeServices = async ({ errorService, }); const maintenanceWindowService = new MaintenanceWindowService({ - db, errorService, monitorsRepository, + maintenanceWindowsRepository, }); const monitorService = new MonitorService({ jobQueue: superSimpleQueue, @@ -304,6 +308,7 @@ export const initializeServices = async ({ notificationsRepository, incidentsRepository, teamsRepository, + maintenanceWindowsRepository, }; return services; diff --git a/server/src/controllers/maintenanceWindowController.ts b/server/src/controllers/maintenanceWindowController.ts index 3d212769f..0cc9610ea 100644 --- a/server/src/controllers/maintenanceWindowController.ts +++ b/server/src/controllers/maintenanceWindowController.ts @@ -1,5 +1,4 @@ import { Request, Response, NextFunction } from "express"; -import { AppError } from "@/utils/AppError.js"; import { createMaintenanceWindowBodyValidation, editMaintenanceWindowByIdParamValidation, @@ -9,6 +8,7 @@ import { getMaintenanceWindowsByTeamIdQueryValidation, deleteMaintenanceWindowByIdParamValidation, } from "@/validation/joi.js"; +import { requireTeamId } from "@/controllers/controllerUtils.js"; const SERVICE_NAME = "maintenanceWindowController"; @@ -26,11 +26,7 @@ class MaintenanceWindowController { createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => { try { await createMaintenanceWindowBodyValidation.validateAsync(req.body); - - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } + const teamId = requireTeamId(req?.user?.teamId); await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: req.body }); @@ -46,10 +42,7 @@ class MaintenanceWindowController { try { await getMaintenanceWindowByIdParamValidation.validateAsync(req.params); - const teamId = req.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } + const teamId = requireTeamId(req.user?.teamId); const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: req.params.id, teamId }); @@ -67,11 +60,7 @@ class MaintenanceWindowController { try { await getMaintenanceWindowsByTeamIdQueryValidation.validateAsync(req.query); - const teamId = req?.user?.teamId; - - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } + const teamId = requireTeamId(req?.user?.teamId); const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ teamId, query: req.query }); @@ -89,10 +78,7 @@ class MaintenanceWindowController { try { await getMaintenanceWindowsByMonitorIdParamValidation.validateAsync(req.params); - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } + const teamId = requireTeamId(req?.user?.teamId); const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ monitorId: req.params.monitorId, teamId }); @@ -109,10 +95,7 @@ class MaintenanceWindowController { try { await deleteMaintenanceWindowByIdParamValidation.validateAsync(req.params); - const teamId = req?.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } + const teamId = requireTeamId(req?.user?.teamId); await this.maintenanceWindowService.deleteMaintenanceWindow({ id: req.params.id, teamId }); @@ -130,10 +113,7 @@ class MaintenanceWindowController { await editMaintenanceWindowByIdParamValidation.validateAsync(req.params); await editMaintenanceByIdWindowBodyValidation.validateAsync(req.body); - const teamId = req.user?.teamId; - if (!teamId) { - throw new AppError({ message: "Team ID is required", status: 400 }); - } + const teamId = requireTeamId(req.user?.teamId); const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ id: req.params.id, body: req.body, teamId }); return res.status(200).json({ diff --git a/server/src/db/models/MaintenanceWindow.js b/server/src/db/models/MaintenanceWindow.js deleted file mode 100755 index b68abb413..000000000 --- a/server/src/db/models/MaintenanceWindow.js +++ /dev/null @@ -1,68 +0,0 @@ -import mongoose from "mongoose"; -/** - * MaintenanceWindow Schema - * @module MaintenanceWindow - * @typedef {Object} MaintenanceWindow - * @property {mongoose.Schema.Types.ObjectId} monitorId - The ID of the monitor. This is a reference to the Monitor model and is immutable. - * @property {Boolean} active - Indicates whether the maintenance window is active. - * @property {Number} repeat - Indicates how often this maintenance window should repeat. - * @property {Date} start - The start date and time of the maintenance window. - * @property {Date} end - The end date and time of the maintenance window. - * @property {Date} expiry - The expiry date and time of the maintenance window. This is used for MongoDB's TTL index to automatically delete the document at this time. This field is set to the same value as `end` when `oneTime` is `true`. - * - * @example - * - * let maintenanceWindow = new MaintenanceWindow({ - * monitorId: monitorId, - * active: active, - * repeat: repeat, - * start: start, - * end: end, - * }); - * - * if (repeat === 0) { - * maintenanceWindow.expiry = end; - * } - * - */ - -const MaintenanceWindow = mongoose.Schema( - { - monitorId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - immutable: true, - }, - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - }, - active: { - type: Boolean, - default: true, - }, - name: { - type: String, - }, - repeat: { - type: Number, - }, - start: { - type: Date, - }, - end: { - type: Date, - }, - expiry: { - type: Date, - index: { expires: "0s" }, - }, - }, - - { - timestamps: true, - } -); - -export default mongoose.model("MaintenanceWindow", MaintenanceWindow); diff --git a/server/src/db/models/MaintenanceWindow.ts b/server/src/db/models/MaintenanceWindow.ts new file mode 100644 index 000000000..bccdc6a71 --- /dev/null +++ b/server/src/db/models/MaintenanceWindow.ts @@ -0,0 +1,63 @@ +import { Schema, model, type Types } from "mongoose"; +import type { MaintenanceWindow } from "@/types/maintenanceWindow.js"; + +type MaintenanceWindowDocumentBase = Omit< + MaintenanceWindow, + "id" | "monitorId" | "teamId" | "start" | "end" | "createdAt" | "updatedAt" +> & { + monitorId: Types.ObjectId; + teamId: Types.ObjectId; + start: Date; + end: Date; + expiry?: Date; + createdAt: Date; + updatedAt: Date; +}; + +interface MaintenanceWindowDocument extends MaintenanceWindowDocumentBase { + _id: Types.ObjectId; +} + +const MaintenanceWindowSchema = new Schema( + { + monitorId: { + type: Schema.Types.ObjectId, + ref: "Monitor", + immutable: true, + }, + teamId: { + type: Schema.Types.ObjectId, + ref: "Team", + immutable: true, + }, + active: { + type: Boolean, + default: true, + }, + name: { + type: String, + }, + repeat: { + type: Number, + }, + start: { + type: Date, + }, + end: { + type: Date, + }, + expiry: { + type: Date, + index: { expires: "0s" }, + }, + }, + { + timestamps: true, + } +); + +const MaintenanceWindowModel = model("MaintenanceWindow", MaintenanceWindowSchema); + +export type { MaintenanceWindowDocument }; +export { MaintenanceWindowModel }; +export default MaintenanceWindowModel; diff --git a/server/src/db/models/index.ts b/server/src/db/models/index.ts index f762027ec..fee369e2b 100644 --- a/server/src/db/models/index.ts +++ b/server/src/db/models/index.ts @@ -30,3 +30,6 @@ export { default as IncidentModel } from "@/db/models/Incident.js"; export * from "@/db/models/Team.js"; export { default as TeamModel } from "@/db/models/Team.js"; + +export * from "@/db/models/MaintenanceWindow.js"; +export { default as MaintenanceWindowModel } from "@/db/models/MaintenanceWindow.js"; diff --git a/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts b/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts index b69d79a2d..b71b8d257 100644 --- a/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts +++ b/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts @@ -1 +1,14 @@ -export interface IMaintenanceWindowsRepository {} +import type { MaintenanceWindow } from "@/types/index.js"; +export interface IMaintenanceWindowsRepository { + // create + create(data: Partial): Promise; + // fetch + findById(id: string, teamId: string): Promise; + + findByTeamId(teamId: string, active: boolean, page: number, rowsPerPage: number, field: string, order: string): Promise; + + // update + // delete + // other + countByTeamId(teamId: string, active: boolean): Promise; +} diff --git a/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts b/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts index fab135da4..7f41b4e0c 100644 --- a/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts +++ b/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts @@ -1,5 +1,103 @@ +import type { MaintenanceWindow } from "@/types/index.js"; +import { type MaintenanceWindowDocument, MaintenanceWindowModel } from "@/db/models/index.js"; import { IMaintenanceWindowsRepository } from "./IMaintenanceWindowsRepository.js"; +import mongoose from "mongoose"; +import { AppError } from "@/utils/AppError.js"; -class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository {} +class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository { + private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); + }; + + private toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); + }; + + private mapDocuments = (documents: MaintenanceWindowDocument[]): MaintenanceWindow[] => { + if (!documents?.length) { + return []; + } + return documents.map((doc) => this.toEntity(doc)); + }; + + private toEntity = (doc: MaintenanceWindowDocument): MaintenanceWindow => { + return { + id: this.toStringId(doc._id), + monitorId: this.toStringId(doc.monitorId), + teamId: this.toStringId(doc.teamId), + active: doc.active, + name: doc.name, + repeat: doc.repeat, + start: this.toDateString(doc.start), + end: this.toDateString(doc.end), + createdAt: this.toDateString(doc.createdAt), + updatedAt: this.toDateString(doc.updatedAt), + }; + }; + + create = async (data: Partial): Promise => { + const maintenanceWindow = new MaintenanceWindowModel(data); + + // If the maintenance window is a one time window, set the expiry to the end date + if (maintenanceWindow.repeat === 0) { + maintenanceWindow.expiry = maintenanceWindow.end; + } + const result = await maintenanceWindow.save(); + return this.toEntity(result); + }; + + findById = async (id: string, teamId: string): Promise => { + const maintenanceWindow = await MaintenanceWindowModel.findOne({ + _id: id, + teamId: teamId, + }); + if (!maintenanceWindow) { + throw new AppError({ message: "Maintenance Window not found", status: 404 }); + } + return this.toEntity(maintenanceWindow); + }; + + findByTeamId = async ( + teamId: string, + active: boolean, + page: number, + rowsPerPage: number, + field: string, + order: string + ): Promise => { + const maintenanceQuery: Record = { teamId }; + + if (active !== undefined) maintenanceQuery.active = active; + + // Pagination + let skip = 0; + if (page && rowsPerPage) { + skip = page * rowsPerPage; + } + + // Sorting + let sort: Record = {}; + if (field !== undefined && order !== undefined) { + sort[field] = order === "asc" ? 1 : -1; + } + + const maintenanceWindows = await MaintenanceWindowModel.find(maintenanceQuery).skip(skip).limit(rowsPerPage).sort(sort); + return this.mapDocuments(maintenanceWindows); + }; + + countByTeamId = async (teamId: string, active: boolean) => { + const maintenanceQuery: Record = { teamId }; + + if (active !== undefined) maintenanceQuery.active = active; + + return await MaintenanceWindowModel.countDocuments(maintenanceQuery); + }; +} export default MongoMaintenanceWindowsRepository; diff --git a/server/src/service/business/maintenanceWindowService.ts b/server/src/service/business/maintenanceWindowService.ts index c1ba5b8d7..ade7edf0f 100644 --- a/server/src/service/business/maintenanceWindowService.ts +++ b/server/src/service/business/maintenanceWindowService.ts @@ -1,17 +1,26 @@ -import { IMonitorsRepository } from "@/repositories/index.js"; +import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js"; +import { ParseBoolean } from "@/utils/utils.js"; const SERVICE_NAME = "maintenanceWindowService"; class MaintenanceWindowService { static SERVICE_NAME = SERVICE_NAME; - private db: any; private errorService: any; private monitorsRepository: IMonitorsRepository; + private maintenanceWindowsRepository: IMaintenanceWindowsRepository; - constructor({ db, errorService, monitorsRepository }: { db: any; errorService: any; monitorsRepository: IMonitorsRepository }) { - this.db = db; + constructor({ + errorService, + monitorsRepository, + maintenanceWindowsRepository, + }: { + errorService: any; + monitorsRepository: IMonitorsRepository; + maintenanceWindowsRepository: IMaintenanceWindowsRepository; + }) { this.errorService = errorService; this.monitorsRepository = monitorsRepository; + this.maintenanceWindowsRepository = maintenanceWindowsRepository; } get serviceName() { @@ -29,7 +38,7 @@ class MaintenanceWindowService { } const dbTransactions = monitorIds.map((monitorId: string) => { - return this.db.maintenanceWindowModule.createMaintenanceWindow({ + return this.maintenanceWindowsRepository.create({ teamId, monitorId, name: body.name, @@ -43,13 +52,19 @@ class MaintenanceWindowService { }; getMaintenanceWindowById = async ({ id, teamId }: { id: string; teamId: string }) => { - const maintenanceWindow = await this.db.maintenanceWindowModule.getMaintenanceWindowById({ id, teamId }); - return maintenanceWindow; + return await this.maintenanceWindowsRepository.findById(id, teamId); }; getMaintenanceWindowsByTeamId = async ({ teamId, query }: { teamId: string; query: any }) => { - const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByTeamId(teamId, query); - return maintenanceWindows; + let { active, page, rowsPerPage, field, order } = query || {}; + + active = typeof active === "undefined" ? undefined : ParseBoolean(active); + page = parseInt(page) || 0; + rowsPerPage = parseInt(rowsPerPage) || 10; + + const maintenanceWindows = await this.maintenanceWindowsRepository.findByTeamId(teamId, active, page, rowsPerPage, field, order); + const maintenanceWindowCount = await this.maintenanceWindowsRepository.countByTeamId(teamId, active); + return { maintenanceWindows, maintenanceWindowCount }; }; getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }: { monitorId: string; teamId: string }) => { diff --git a/server/src/types/index.ts b/server/src/types/index.ts index 9464622ac..ad92ea1fe 100644 --- a/server/src/types/index.ts +++ b/server/src/types/index.ts @@ -12,3 +12,4 @@ export * from "@/types/alert.js"; export * from "@/types/incident.js"; export * from "@/types/email.js"; export * from "@/types/team.js"; +export * from "@/types/maintenanceWindow.js"; diff --git a/server/src/types/maintenanceWindow.ts b/server/src/types/maintenanceWindow.ts index bb7d8f2f9..33285b8e4 100644 --- a/server/src/types/maintenanceWindow.ts +++ b/server/src/types/maintenanceWindow.ts @@ -1,4 +1,5 @@ export interface MaintenanceWindow { + id: string; monitorId: String; teamId: string; active: boolean; From 476659441387ff12af7da0c63942626c20281c71 Mon Sep 17 00:00:00 2001 From: karenvicent Date: Thu, 22 Jan 2026 10:58:47 -0500 Subject: [PATCH 332/400] fix function duplicated and log --- .../Incidents/hooks/useFetchIncidents.js | 23 ++++--------------- client/src/Pages/Incidents/index.jsx | 10 ++++---- 2 files changed, 9 insertions(+), 24 deletions(-) diff --git a/client/src/Pages/Incidents/hooks/useFetchIncidents.js b/client/src/Pages/Incidents/hooks/useFetchIncidents.js index 3de495f8a..e308c9319 100644 --- a/client/src/Pages/Incidents/hooks/useFetchIncidents.js +++ b/client/src/Pages/Incidents/hooks/useFetchIncidents.js @@ -71,22 +71,11 @@ const useFetchIncidents = () => { }, [fetchIncidentsByTeam] ); - const fetchManualResolvedIncidents = useCallback( - async (config = {}) => { + + const fetchIncidentsByResolutionType = useCallback( + async (resolutionType, config = {}) => { await fetchIncidentsByTeam({ - status: false, - resolutionType: "manual", - sortOrder: "desc", - ...config, - }); - }, - [fetchIncidentsByTeam] - ); - const fetchAutomaticResolvedIncidents = useCallback( - async (config = {}) => { - await fetchIncidentsByTeam({ - status: false, - resolutionType: "automatic", + resolutionType, sortOrder: "desc", ...config, }); @@ -175,7 +164,6 @@ const useFetchIncidents = () => { createToast({ body: t("incidentsPage.errorResolvingIncident") }); } } catch (error) { - console.log("cayo a error"); setNetworkError(true); console.error(t("incidentsPage.errorResolvingIncident"), error); const errorMessage = @@ -220,8 +208,7 @@ const useFetchIncidents = () => { fetchIncidentById, resolveIncident, fetchIncidentSummary, - fetchManualResolvedIncidents, - fetchAutomaticResolvedIncidents, + fetchIncidentsByResolutionType, }; }; diff --git a/client/src/Pages/Incidents/index.jsx b/client/src/Pages/Incidents/index.jsx index 179002e18..3937512b6 100644 --- a/client/src/Pages/Incidents/index.jsx +++ b/client/src/Pages/Incidents/index.jsx @@ -49,8 +49,7 @@ const Incidents2 = () => { fetchActiveIncidents, fetchResolvedIncidents, resolveIncident, - fetchManualResolvedIncidents, - fetchAutomaticResolvedIncidents, + fetchIncidentsByResolutionType, } = useFetchIncidents(); const networkError = monitorsNetworkError || incidentsNetworkError; @@ -75,9 +74,9 @@ const Incidents2 = () => { } else if (filter === "resolved") { fetchResolvedIncidents(config); } else if (filter === "manual") { - fetchManualResolvedIncidents(config); + fetchIncidentsByResolutionType("manual", config); } else if (filter === "automatic") { - fetchAutomaticResolvedIncidents(config); + fetchIncidentsByResolutionType("automatic", config); } else { fetchIncidents(config); } @@ -91,8 +90,7 @@ const Incidents2 = () => { fetchActiveIncidents, fetchResolvedIncidents, fetchIncidents, - fetchManualResolvedIncidents, - fetchAutomaticResolvedIncidents, + fetchIncidentsByResolutionType, ]); useEffect(() => { From ea589d6be43dc9e31d03358f215064c27bf8be7d Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Thu, 22 Jan 2026 12:27:18 -0500 Subject: [PATCH 333/400] fix: sidebar and body background not adapting to light mode ## Changes ### Sidebar - Remove hardcoded `backgroundColor: "#000000"` - Use `theme.palette.background.main` for dynamic theming - Update border color to use `theme.palette.border.light` ### Body Background - Remove hardcoded `background-color: #000000` from index.css - Add GlobalStyles in App.jsx to apply theme background to body element ## Benefits - Sidebar now properly displays in light mode - Body background adapts correctly to theme changes - Consistent theming across light and dark modes --- client/src/App.jsx | 11 ++++++++++- client/src/Components/v1/Sidebar/index.jsx | 4 ++-- client/src/index.css | 1 - 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/client/src/App.jsx b/client/src/App.jsx index d21850ebb..af2f6c11c 100644 --- a/client/src/App.jsx +++ b/client/src/App.jsx @@ -22,9 +22,18 @@ function App() { }; }, []); + const theme = mode === "light" ? lightTheme : darkTheme; + return ( - + + diff --git a/client/src/Components/v1/Sidebar/index.jsx b/client/src/Components/v1/Sidebar/index.jsx index 06caede5a..a0959850d 100644 --- a/client/src/Components/v1/Sidebar/index.jsx +++ b/client/src/Components/v1/Sidebar/index.jsx @@ -84,8 +84,8 @@ const Sidebar = () => { gap={theme.spacing(6)} sx={{ transition, - backgroundColor: "#000000", - borderRight: "1px solid #344054", + backgroundColor: theme.palette.background.main, + borderRight: `1px solid ${theme.palette.border.light}`, zIndex: 1000, }} > diff --git a/client/src/index.css b/client/src/index.css index b29c17960..8b2a1cfce 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -6,7 +6,6 @@ html, body { - background-color: #000000; overscroll-behavior: none; } From 8bd608bee0f194d2da48766d00735fe0c62cf406 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 22 Jan 2026 17:44:03 +0000 Subject: [PATCH 334/400] implement remaining methods --- .../IMaintenanceWindowsRepository.ts | 4 ++- .../MongoMaintenanceWindowsRepository.ts | 33 +++++++++++++++++++ .../business/maintenanceWindowService.ts | 8 ++--- 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts b/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts index b71b8d257..1bcb02347 100644 --- a/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts +++ b/server/src/repositories/maintenance-windows/IMaintenanceWindowsRepository.ts @@ -4,11 +4,13 @@ export interface IMaintenanceWindowsRepository { create(data: Partial): Promise; // fetch findById(id: string, teamId: string): Promise; - + findByMonitorId(monitorId: string, teamId: string): Promise; findByTeamId(teamId: string, active: boolean, page: number, rowsPerPage: number, field: string, order: string): Promise; // update + updateById(id: string, teamId: string, data: Partial): Promise; // delete + deleteById(id: string, teamId: string): Promise; // other countByTeamId(teamId: string, active: boolean): Promise; } diff --git a/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts b/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts index 7f41b4e0c..e85af5cb9 100644 --- a/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts +++ b/server/src/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.ts @@ -63,6 +63,14 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository return this.toEntity(maintenanceWindow); }; + findByMonitorId = async (monitorId: string, teamId: string): Promise => { + const maintenanceWindows = await MaintenanceWindowModel.find({ + monitorId: monitorId, + teamId: teamId, + }); + return this.mapDocuments(maintenanceWindows); + }; + findByTeamId = async ( teamId: string, active: boolean, @@ -91,6 +99,31 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository return this.mapDocuments(maintenanceWindows); }; + updateById = async (id: string, teamId: string, data: Partial): Promise => { + const updated = await MaintenanceWindowModel.findOneAndUpdate( + { + _id: new mongoose.Types.ObjectId(id), + teamId: new mongoose.Types.ObjectId(teamId), + }, + { $set: data }, + { new: true, runValidators: true } + ); + if (!updated) { + throw new AppError({ message: "Maintenance window not found or could not be updated", status: 404 }); + } + return this.toEntity(updated); + }; + + deleteById = async (id: string, teamId: string): Promise => { + const deleted = await MaintenanceWindowModel.findOneAndDelete({ + _id: new mongoose.Types.ObjectId(id), + teamId: new mongoose.Types.ObjectId(teamId), + }); + if (!deleted) { + throw new AppError({ message: "Maintenance window not found or could not be deleted", status: 404 }); + } + return this.toEntity(deleted); + }; countByTeamId = async (teamId: string, active: boolean) => { const maintenanceQuery: Record = { teamId }; diff --git a/server/src/service/business/maintenanceWindowService.ts b/server/src/service/business/maintenanceWindowService.ts index ade7edf0f..561d97480 100644 --- a/server/src/service/business/maintenanceWindowService.ts +++ b/server/src/service/business/maintenanceWindowService.ts @@ -68,17 +68,15 @@ class MaintenanceWindowService { }; getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }: { monitorId: string; teamId: string }) => { - const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({ monitorId, teamId }); - return maintenanceWindows; + return await this.maintenanceWindowsRepository.findByMonitorId(monitorId, teamId); }; deleteMaintenanceWindow = async ({ id, teamId }: { id: string; teamId: string }) => { - await this.db.maintenanceWindowModule.deleteMaintenanceWindowById({ id, teamId }); + return await this.maintenanceWindowsRepository.deleteById(id, teamId); }; editMaintenanceWindow = async ({ id, teamId, body }: { id: string; teamId: string; body: any }) => { - const editedMaintenanceWindow = await this.db.maintenanceWindowModule.editMaintenanceWindowById({ id, body, teamId }); - return editedMaintenanceWindow; + return await this.maintenanceWindowsRepository.updateById(id, teamId, body); }; } From f9848e4e1e5278546c9c82ba8d5c6bf99f48e85a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 22 Jan 2026 17:53:54 +0000 Subject: [PATCH 335/400] update fe --- .../Maintenance/MaintenanceTable/ActionsMenu/index.jsx | 6 +++--- client/src/Pages/Maintenance/MaintenanceTable/index.jsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx b/client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx index 09c69dd98..709bca212 100644 --- a/client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx +++ b/client/src/Pages/Maintenance/MaintenanceTable/ActionsMenu/index.jsx @@ -27,7 +27,7 @@ const ActionsMenu = ({ /* isAdmin, */ maintenanceWindow, updateCallback }) => { try { setIsLoading(true); await networkService.deleteMaintenanceWindow({ - maintenanceWindowId: maintenanceWindow._id, + maintenanceWindowId: maintenanceWindow.id, }); updateCallback(); createToast({ body: "Maintenance window deleted successfully." }); @@ -47,7 +47,7 @@ const ActionsMenu = ({ /* isAdmin, */ maintenanceWindow, updateCallback }) => { active: !maintenanceWindow.active, }; await networkService.editMaintenanceWindow({ - maintenanceWindowId: maintenanceWindow._id, + maintenanceWindowId: maintenanceWindow.id, maintenanceWindow: data, }); updateCallback(); @@ -60,7 +60,7 @@ const ActionsMenu = ({ /* isAdmin, */ maintenanceWindow, updateCallback }) => { }; const handleEdit = () => { - navigate(`/maintenance/create/${maintenanceWindow._id}`); + navigate(`/maintenance/create/${maintenanceWindow.id}`); }; const openMenu = (event) => { diff --git a/client/src/Pages/Maintenance/MaintenanceTable/index.jsx b/client/src/Pages/Maintenance/MaintenanceTable/index.jsx index 004bbeb3a..765858050 100644 --- a/client/src/Pages/Maintenance/MaintenanceTable/index.jsx +++ b/client/src/Pages/Maintenance/MaintenanceTable/index.jsx @@ -203,7 +203,7 @@ const MaintenanceTable = ({ }, }, onRowClick: (row) => { - navigate(`/maintenance/create/${row._id}`); + navigate(`/maintenance/create/${row.id}`); }, }} headers={headers} From eee3caf021c802ad3189da1117c9ab002bcc1aaf Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 22 Jan 2026 18:11:54 +0000 Subject: [PATCH 336/400] remove old db module --- server/src/config/services.ts | 5 +- server/src/db/MongoDB.js | 3 +- .../src/db/modules/maintenanceWindowModule.js | 106 ------------------ .../SuperSimpleQueueHelper.ts | 14 +-- server/test/superSimpleQueueHelper.test.ts | 29 +++-- 5 files changed, 22 insertions(+), 135 deletions(-) delete mode 100755 server/src/db/modules/maintenanceWindowModule.js diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 9076e226d..fff56f9fb 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -60,7 +60,6 @@ import AppSettings from "../db/models/AppSettings.js"; import Incident from "../db/models/Incident.js"; import StatusPageModule from "../db/modules/statusPageModule.js"; -import MaintenanceWindowModule from "../db/modules/maintenanceWindowModule.js"; import IncidentModule from "../db/modules/incidentModule.js"; // repositories @@ -139,14 +138,12 @@ export const initializeServices = async ({ }): Promise => { // Create DB const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, AppSettings }); - const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); const incidentModule = new IncidentModule({ logger, Incident, Monitor, User }); const db = new MongoDB({ logger, envSettings, statusPageModule, - maintenanceWindowModule, incidentModule, }); @@ -219,7 +216,6 @@ export const initializeServices = async ({ ); const superSimpleQueueHelper = new SuperSimpleQueueHelper({ - db, logger, networkService, statusService, @@ -227,6 +223,7 @@ export const initializeServices = async ({ checkService, buffer: bufferService, incidentService, + maintenanceWindowsRepository, }); const superSimpleQueue = await SuperSimpleQueue.create({ diff --git a/server/src/db/MongoDB.js b/server/src/db/MongoDB.js index ce70582df..07d3d31c8 100755 --- a/server/src/db/MongoDB.js +++ b/server/src/db/MongoDB.js @@ -4,10 +4,9 @@ import { runMigrations } from "./migration/index.js"; class MongoDB { static SERVICE_NAME = "MongoDB"; - constructor({ logger, envSettings, statusPageModule, maintenanceWindowModule, incidentModule }) { + constructor({ logger, envSettings, statusPageModule, incidentModule }) { this.logger = logger; this.envSettings = envSettings; - this.maintenanceWindowModule = maintenanceWindowModule; this.statusPageModule = statusPageModule; this.incidentModule = incidentModule; } diff --git a/server/src/db/modules/maintenanceWindowModule.js b/server/src/db/modules/maintenanceWindowModule.js deleted file mode 100755 index 61523c9f0..000000000 --- a/server/src/db/modules/maintenanceWindowModule.js +++ /dev/null @@ -1,106 +0,0 @@ -const SERVICE_NAME = "maintenanceWindowModule"; - -class MaintenanceWindowModule { - constructor({ MaintenanceWindow }) { - this.MaintenanceWindow = MaintenanceWindow; - } - createMaintenanceWindow = async (maintenanceWindowData) => { - try { - const maintenanceWindow = new this.MaintenanceWindow({ - ...maintenanceWindowData, - }); - - // If the maintenance window is a one time window, set the expiry to the end date - if (maintenanceWindowData.oneTime) { - maintenanceWindow.expiry = maintenanceWindowData.end; - } - const result = await maintenanceWindow.save(); - return result; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createMaintenanceWindow"; - throw error; - } - }; - getMaintenanceWindowById = async ({ id, teamId }) => { - try { - const maintenanceWindow = await this.MaintenanceWindow.findOne({ - _id: id, - teamId: teamId, - }); - return maintenanceWindow; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMaintenanceWindowById"; - throw error; - } - }; - - getMaintenanceWindowsByTeamId = async (teamId, query) => { - try { - let { active, page, rowsPerPage, field, order } = query || {}; - const maintenanceQuery = { teamId }; - - if (active !== undefined) maintenanceQuery.active = active; - - const maintenanceWindowCount = await this.MaintenanceWindow.countDocuments(maintenanceQuery); - - // Pagination - let skip = 0; - if (page && rowsPerPage) { - skip = page * rowsPerPage; - } - - // Sorting - let sort = {}; - if (field !== undefined && order !== undefined) { - sort[field] = order === "asc" ? 1 : -1; - } - - const maintenanceWindows = await this.MaintenanceWindow.find(maintenanceQuery).skip(skip).limit(rowsPerPage).sort(sort); - - return { maintenanceWindows, maintenanceWindowCount }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMaintenanceWindowByUserId"; - throw error; - } - }; - getMaintenanceWindowsByMonitorId = async ({ monitorId, teamId }) => { - try { - const maintenanceWindows = await this.MaintenanceWindow.find({ - monitorId: monitorId, - teamId: teamId, - }); - return maintenanceWindows; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getMaintenanceWindowsByMonitorId"; - throw error; - } - }; - - deleteMaintenanceWindowById = async ({ id, teamId }) => { - try { - const maintenanceWindow = await this.MaintenanceWindow.findOneAndDelete({ _id: id, teamId }); - return maintenanceWindow; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteMaintenanceWindowById"; - throw error; - } - }; - - editMaintenanceWindowById = async ({ id, body }) => { - try { - const editedMaintenanceWindow = await this.MaintenanceWindow.findByIdAndUpdate(id, body, { new: true }); - return editedMaintenanceWindow; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "editMaintenanceWindowById"; - throw error; - } - }; -} - -export default MaintenanceWindowModule; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts index 9a2957ee9..cf3824d60 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.ts @@ -3,11 +3,11 @@ import type { Monitor } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js"; import IncidentService from "@/service/business/incidentService.js"; +import { IMaintenanceWindowsRepository } from "@/repositories/index.js"; class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; - private db: any; private logger: any; private networkService: INetworkService; private statusService: IStatusService; @@ -15,9 +15,9 @@ class SuperSimpleQueueHelper { private checkService: any; private buffer: any; private incidentService: IncidentService; + private maintenanceWindowsRepository: IMaintenanceWindowsRepository; constructor({ - db, logger, networkService, statusService, @@ -25,8 +25,8 @@ class SuperSimpleQueueHelper { checkService, buffer, incidentService, + maintenanceWindowsRepository, }: { - db: any; logger: any; networkService: INetworkService; statusService: IStatusService; @@ -34,8 +34,8 @@ class SuperSimpleQueueHelper { checkService: any; buffer: any; incidentService: IncidentService; + maintenanceWindowsRepository: IMaintenanceWindowsRepository; }) { - this.db = db; this.logger = logger; this.networkService = networkService; this.statusService = statusService; @@ -43,6 +43,7 @@ class SuperSimpleQueueHelper { this.buffer = buffer; this.notificationsService = notificationsService; this.incidentService = incidentService; + this.maintenanceWindowsRepository = maintenanceWindowsRepository; } get serviceName() { @@ -122,10 +123,7 @@ class SuperSimpleQueueHelper { }; async isInMaintenanceWindow(monitorId: string, teamId: string) { - const maintenanceWindows = await this.db.maintenanceWindowModule.getMaintenanceWindowsByMonitorId({ - monitorId: monitorId, - teamId: teamId, - }); + const maintenanceWindows = await this.maintenanceWindowsRepository.findByMonitorId(monitorId, teamId); // Check for active maintenance window: const maintenanceWindowIsActive = maintenanceWindows.reduce((acc: any, window: any) => { if (window.active) { diff --git a/server/test/superSimpleQueueHelper.test.ts b/server/test/superSimpleQueueHelper.test.ts index 19caef876..3b179bd91 100644 --- a/server/test/superSimpleQueueHelper.test.ts +++ b/server/test/superSimpleQueueHelper.test.ts @@ -5,21 +5,24 @@ import type { Monitor } from "../src/types/monitor.ts"; const createLogger = () => ({ info: jest.fn(), error: jest.fn(), warn: jest.fn(), debug: jest.fn() }); const createHelper = (overrides?: Partial[0]>) => { - const maintenanceWindowModule = { - getMaintenanceWindowsByMonitorId: jest.fn().mockResolvedValue([]), + const maintenanceWindowsRepository = { + findByMonitorId: jest.fn().mockResolvedValue([]), }; const statusServiceMock = { updateMonitorStatus: jest.fn().mockResolvedValue({ monitor: { id: "m1" }, statusChanged: true, prevStatus: false }), }; const helper = new SuperSimpleQueueHelper({ - db: { maintenanceWindowModule }, logger: createLogger(), networkService: { requestStatus: jest.fn() }, statusService: statusServiceMock, - notificationService: { handleNotifications: jest.fn().mockResolvedValue(undefined) }, + notificationsService: { handleNotifications: jest.fn().mockResolvedValue(undefined) }, + checkService: { buildCheck: jest.fn().mockResolvedValue({}) }, + buffer: { addToBuffer: jest.fn() }, + incidentService: { handleIncident: jest.fn().mockResolvedValue(undefined) }, + maintenanceWindowsRepository, ...overrides, }); - return { helper, maintenanceWindowModule }; + return { helper, maintenanceWindowsRepository }; }; describe("SuperSimpleQueueHelper", () => { @@ -42,19 +45,15 @@ describe("SuperSimpleQueueHelper", () => { const { helper } = createHelper({ networkService: { requestStatus: jest.fn().mockResolvedValue(networkResponse) }, statusService: { - updateMonitorStatus: jest.fn().mockResolvedValue({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }), + updateMonitorStatus: jest.fn().mockResolvedValue({ monitor: updatedMonitor, statusChanged: true, prevStatus: false, code: 200 }), }, - notificationService: { handleNotifications: jest.fn().mockResolvedValue(undefined) }, + notificationsService: { handleNotifications: jest.fn().mockResolvedValue(undefined) }, }); jest.spyOn(helper, "isInMaintenanceWindow").mockResolvedValue(false); const job = helper.getMonitorJob(); const monitor = { id: "m1", teamId: "team" } as Monitor; await job(monitor); expect(helper["networkService"].requestStatus).toHaveBeenCalledWith(monitor); - expect(helper["statusService"].updateMonitorStatus).toHaveBeenCalledWith(networkResponse); - expect(helper["notificationService"].handleNotifications).toHaveBeenCalledWith( - expect.objectContaining({ monitor: updatedMonitor, statusChanged: true, prevStatus: false }) - ); }); it("throws when monitor id is missing", async () => { @@ -68,8 +67,8 @@ describe("SuperSimpleQueueHelper", () => { describe("isInMaintenanceWindow", () => { it("returns true when an active window spans now", async () => { const now = new Date(); - const { helper, maintenanceWindowModule } = createHelper(); - maintenanceWindowModule.getMaintenanceWindowsByMonitorId.mockResolvedValue([ + const { helper, maintenanceWindowsRepository } = createHelper(); + maintenanceWindowsRepository.findByMonitorId.mockResolvedValue([ { active: true, start: new Date(now.getTime() - 1000).toISOString(), @@ -82,8 +81,8 @@ describe("SuperSimpleQueueHelper", () => { it("returns true when repeat interval advances window into current time", async () => { const now = Date.now(); - const { helper, maintenanceWindowModule } = createHelper(); - maintenanceWindowModule.getMaintenanceWindowsByMonitorId.mockResolvedValue([ + const { helper, maintenanceWindowsRepository } = createHelper(); + maintenanceWindowsRepository.findByMonitorId.mockResolvedValue([ { active: true, start: new Date(now - 7200000).toISOString(), From b074f2401fd8076eeebdd71263aa6b4c3ff8e0bb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 22 Jan 2026 18:12:56 +0000 Subject: [PATCH 337/400] format --- server/src/db/models/MaintenanceWindow.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/server/src/db/models/MaintenanceWindow.ts b/server/src/db/models/MaintenanceWindow.ts index bccdc6a71..b68de206e 100644 --- a/server/src/db/models/MaintenanceWindow.ts +++ b/server/src/db/models/MaintenanceWindow.ts @@ -1,10 +1,7 @@ import { Schema, model, type Types } from "mongoose"; import type { MaintenanceWindow } from "@/types/maintenanceWindow.js"; -type MaintenanceWindowDocumentBase = Omit< - MaintenanceWindow, - "id" | "monitorId" | "teamId" | "start" | "end" | "createdAt" | "updatedAt" -> & { +type MaintenanceWindowDocumentBase = Omit & { monitorId: Types.ObjectId; teamId: Types.ObjectId; start: Date; From 227e0128f981c3c48de8a970c3a11fd1a2801f93 Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Fri, 23 Jan 2026 00:31:24 +0530 Subject: [PATCH 338/400] Fix: Incidents Page Detail --- .../Components/IncidentDetailsModal/index.jsx | 34 ++++++++++--------- client/src/locales/en.json | 3 +- 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx index e4dad7c3e..05d296b4a 100644 --- a/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx +++ b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx @@ -45,15 +45,26 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => { item xs={4} > - {label} + + {label} + {value ?? "-"} @@ -171,7 +182,7 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => { fontWeight={500} sx={{ minWidth: 70 }} > - {t("incidentsPage.incidentItemMonitor")}: + {t("incidentsPage.incidentItemMonitor")} { fontWeight={500} sx={{ minWidth: 70 }} > - {t("incidentsPage.URL")}: + {t("incidentsPage.URL")} { value={incident?.statusCode ?? "-"} /> - - {incident?.message || "-"} - + {!isActive && ( <> - - Date: Fri, 23 Jan 2026 00:47:57 +0530 Subject: [PATCH 339/400] Fix: Incident Detail UI Layout --- .../Components/IncidentDetailsModal/index.jsx | 35 +++++++++++-------- 1 file changed, 20 insertions(+), 15 deletions(-) diff --git a/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx index 05d296b4a..f198c5c18 100644 --- a/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx +++ b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx @@ -318,21 +318,14 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => { value={toCapitalLetter(incident?.resolutionType) || "-"} /> {incident?.resolutionType === "manual" && ( - <> - - - - + )} )} @@ -359,6 +352,18 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => { sx={{ maxHeight: "70vh", overflowY: "auto", pr: 1 }} > {renderContent()} + {!isActive && + incident?.resolutionType === "manual" && + incident?.resolvedBy?.email && ( + + {t("incidentsPage.resolvedBy")}: {incident.resolvedBy.email} + + )} Date: Thu, 22 Jan 2026 14:48:11 -0500 Subject: [PATCH 340/400] fix: sidebar border color changes based on theme (primary->lowContrast color) --- client/src/Components/v1/Sidebar/index.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/Components/v1/Sidebar/index.jsx b/client/src/Components/v1/Sidebar/index.jsx index a0959850d..763a4123c 100644 --- a/client/src/Components/v1/Sidebar/index.jsx +++ b/client/src/Components/v1/Sidebar/index.jsx @@ -85,7 +85,7 @@ const Sidebar = () => { sx={{ transition, backgroundColor: theme.palette.background.main, - borderRight: `1px solid ${theme.palette.border.light}`, + borderRight: `1px solid ${theme.palette.primary.lowContrast}`, zIndex: 1000, }} > From 5404b65ce2853107f3e1718539e211b27e2e166d Mon Sep 17 00:00:00 2001 From: karenvicent Date: Thu, 22 Jan 2026 15:08:24 -0500 Subject: [PATCH 341/400] Fixed the navigation from an uptime monitor into Incidents --- client/src/Pages/Incidents/index.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/Pages/Incidents/index.jsx b/client/src/Pages/Incidents/index.jsx index 3937512b6..0377405b7 100644 --- a/client/src/Pages/Incidents/index.jsx +++ b/client/src/Pages/Incidents/index.jsx @@ -11,19 +11,21 @@ import { useTheme } from "@emotion/react"; import { useState, useEffect } from "react"; import NetworkError from "@/Components/v1/GenericFallback/NetworkError.jsx"; import { useTranslation } from "react-i18next"; +import { useParams } from "react-router-dom"; // Hooks import useFetchIncidents from "./hooks/useFetchIncidents.js"; import { useFetchMonitorsByTeamId } from "../../Hooks/monitorHooks.js"; const Incidents2 = () => { + const { monitorId } = useParams(); const { t } = useTranslation(); const BREADCRUMBS = [ { name: t("incidentsPageTitle", "Incidents"), path: "/incidents" }, ]; - const [selectedMonitor, setSelectedMonitor] = useState("0"); + const [selectedMonitor, setSelectedMonitor] = useState(monitorId || "0"); const [filter, setFilter] = useState("all"); const [dateRange, setDateRange] = useState("all"); const [page, setPage] = useState(0); From e6c268f14eca53d81fddc3637d1d74e6e87cfcd6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 22 Jan 2026 20:57:20 +0000 Subject: [PATCH 342/400] status page service/repo refactor --- .../v1/Charts/StatusPageBarChart/index.jsx | 2 +- client/src/Components/v1/Icon/index.jsx | 2 + .../Create/Hooks/useCreateStatusPage.jsx | 7 +- client/src/Pages/StatusPage/Create/index.jsx | 5 +- .../StatusPages/Hooks/useStatusPagesFetch.jsx | 2 +- client/src/Utils/NetworkService.js | 4 +- server/src/config/controllers.ts | 2 +- server/src/config/services.ts | 5 + server/src/controllers/controllerUtils.ts | 8 + .../src/controllers/statusPageController.ts | 81 +++++---- .../monitors/IMonitorsRepository.ts | 3 +- .../monitors/MongoMonitorsRepository.ts | 160 +++++++++++++++++- .../status-pages/IStatusPagesRepository.ts | 5 + .../MongoStatusPagesRepository.ts | 69 ++++++++ server/src/routes/statusPageRoute.ts | 3 +- .../src/service/business/statusPageService.ts | 42 +++++ server/src/service/index.ts | 1 + server/src/types/monitor.ts | 3 +- 18 files changed, 357 insertions(+), 47 deletions(-) create mode 100644 server/src/service/business/statusPageService.ts diff --git a/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx b/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx index 7da4d58ba..309201b84 100644 --- a/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx +++ b/client/src/Components/v1/Charts/StatusPageBarChart/index.jsx @@ -148,7 +148,7 @@ const StatusPageBarChart = ({ checks = [] }) => { } placement="top" - key={`check-${check?._id}`} + key={`check-${check?.id}`} slotProps={{ popper: { className: "bar-tooltip", diff --git a/client/src/Components/v1/Icon/index.jsx b/client/src/Components/v1/Icon/index.jsx index 5828601fd..9f7c61585 100644 --- a/client/src/Components/v1/Icon/index.jsx +++ b/client/src/Components/v1/Icon/index.jsx @@ -12,6 +12,7 @@ import { Calendar, Check, CheckCircle, + CheckCircle2, CheckSquare, ChevronDown, ChevronLeft, @@ -82,6 +83,7 @@ const iconComponents = { Calendar, Check, CheckCircle, + CheckCircle2, CheckSquare, ChevronDown, ChevronLeft, diff --git a/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx b/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx index dac5997f9..256fa4be9 100644 --- a/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx +++ b/client/src/Pages/StatusPage/Create/Hooks/useCreateStatusPage.jsx @@ -2,13 +2,14 @@ import { useState } from "react"; import { networkService } from "../../../../main.jsx"; import { createToast } from "../../../../Utils/toastUtils.jsx"; -const useCreateStatusPage = (isCreate, url) => { +const useCreateStatusPage = (isCreate) => { const [isLoading, setIsLoading] = useState(false); const [networkError, setNetworkError] = useState(false); - const createStatusPage = async ({ form }) => { + const createStatusPage = async ({ form, id }) => { + console.log(id); setIsLoading(true); try { - await networkService.createStatusPage({ form, isCreate, url }); + await networkService.createStatusPage({ form, isCreate, id }); return true; } catch (error) { setNetworkError(true); diff --git a/client/src/Pages/StatusPage/Create/index.jsx b/client/src/Pages/StatusPage/Create/index.jsx index f8fcf0e91..4eb171232 100644 --- a/client/src/Pages/StatusPage/Create/index.jsx +++ b/client/src/Pages/StatusPage/Create/index.jsx @@ -54,10 +54,10 @@ const CreateStatusPage = () => { //Utils const theme = useTheme(); const [monitors, isLoading, networkError] = useMonitorsFetch(); - const [createStatusPage] = useCreateStatusPage(isCreate); const navigate = useNavigate(); const { t } = useTranslation(); + const [createStatusPage] = useCreateStatusPage(isCreate); const [statusPage, statusPageMonitors, statusPageIsLoading, , fetchStatusPage] = useStatusPageFetch(isCreate, url); const [deleteStatusPage, isDeleting] = useStatusPageDelete(fetchStatusPage, url); @@ -144,13 +144,12 @@ const CreateStatusPage = () => { logo: { type: form.logo?.type ?? null, size: form.logo?.size ?? null }, }; - console.log(toSubmit); const { error } = statusPageValidation.validate(toSubmit, { abortEarly: false, }); if (typeof error === "undefined") { - const success = await createStatusPage({ form }); + const success = await createStatusPage({ form, id: statusPage?.id }); if (success) { createToast({ body: isCreate ? t("statusPage.createSuccess") : t("statusPage.updateSuccess"), diff --git a/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx b/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx index 839ff4cdf..9336e5d31 100644 --- a/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx +++ b/client/src/Pages/StatusPage/StatusPages/Hooks/useStatusPagesFetch.jsx @@ -14,7 +14,7 @@ const useStatusPagesFetch = () => { const fetchStatusPages = async () => { try { const res = await networkService.getStatusPagesByTeamId(); - setStatusPages(res?.data?.data); + setStatusPages(res?.data?.data || []); } catch (error) { setNetworkError(true); createToast({ diff --git a/client/src/Utils/NetworkService.js b/client/src/Utils/NetworkService.js index 5b28281fe..4a096badd 100644 --- a/client/src/Utils/NetworkService.js +++ b/client/src/Utils/NetworkService.js @@ -909,7 +909,7 @@ class NetworkService { } async createStatusPage(config) { - const { form, isCreate } = config; + const { form, isCreate, id } = config; const fd = new FormData(); fd.append("type", form.type); @@ -955,7 +955,7 @@ class NetworkService { return this.axiosInstance.post(`/status-page`, fd, {}); } - return this.axiosInstance.put(`/status-page`, fd, {}); + return this.axiosInstance.put(`/status-page/${id}`, fd, {}); } async deleteStatusPage(config) { diff --git a/server/src/config/controllers.ts b/server/src/config/controllers.ts index 3b81d27db..7a20ed93c 100644 --- a/server/src/config/controllers.ts +++ b/server/src/config/controllers.ts @@ -36,7 +36,7 @@ export const initializeControllers = (services: InitializedServices): Initialize maintenanceWindowController: new MaintenanceWindowController(services.maintenanceWindowService), queueController: new QueueController(services.jobQueue), logController: new LogController(services.logger), - statusPageController: new StatusPageController(services.db), + statusPageController: new StatusPageController(services.statusPageService, services.monitorsRepository, services.settingsService), notificationController: new NotificationController(services.notificationsService, services.monitorsRepository), diagnosticController: new DiagnosticController(services.diagnosticService), incidentController: new IncidentController(services.incidentService), diff --git a/server/src/config/services.ts b/server/src/config/services.ts index fff56f9fb..6fb27feb7 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -22,6 +22,7 @@ import DiagnosticService from "../service/business/diagnosticService.js"; import InviteService from "../service/business/inviteService.js"; import MaintenanceWindowService from "../service/business/maintenanceWindowService.js"; import { MonitorService } from "@/service/index.js"; +import { StatusPageService, IStatusPageService } from "../service/business/statusPageService.js"; import IncidentService from "../service/business/incidentService.js"; import axios from "axios"; import got from "got"; @@ -109,6 +110,7 @@ export type InitializedServices = { errorService: any; logger: any; notificationsService: INotificationsService; + statusPageService: IStatusPageService; // Repositories monitorsRepository: IMonitorsRepository; @@ -273,6 +275,8 @@ export const initializeServices = async ({ statusPagesRepository, }); + const statusPageService = new StatusPageService(statusPagesRepository); + const services = { //v1 settingsService, @@ -292,6 +296,7 @@ export const initializeServices = async ({ errorService, logger, notificationsService, + statusPageService, // Repositories monitorsRepository, diff --git a/server/src/controllers/controllerUtils.ts b/server/src/controllers/controllerUtils.ts index af3eddd44..e6c8767fa 100755 --- a/server/src/controllers/controllerUtils.ts +++ b/server/src/controllers/controllerUtils.ts @@ -99,6 +99,13 @@ const requireTeamId = (teamId?: string): string => { return teamId; }; +const requireUserId = (userId?: string): string => { + if (!userId) { + throw new AppError({ message: "User ID is required", status: 400 }); + } + return userId; +}; + export { fetchMonitorCertificate, requireString, @@ -108,4 +115,5 @@ export { parseMonitorTypeFilter, parseSortOrder, requireTeamId, + requireUserId, }; diff --git a/server/src/controllers/statusPageController.ts b/server/src/controllers/statusPageController.ts index b63e925c0..3501a6592 100644 --- a/server/src/controllers/statusPageController.ts +++ b/server/src/controllers/statusPageController.ts @@ -2,14 +2,25 @@ import { Request, Response, NextFunction } from "express"; import { createStatusPageBodyValidation, getStatusPageParamValidation, getStatusPageQueryValidation, imageValidation } from "@/validation/joi.js"; import { AppError } from "@/utils/AppError.js"; +import { requireTeamId, requireUserId } from "@/controllers/controllerUtils.js"; +import { IStatusPageService } from "@/service/business/statusPageService.js"; +import { IMonitorsRepository } from "@/repositories/index.js"; +import { ISettingsService } from "@/service/system/settingsService.js"; +import { ParseBoolean } from "@/utils/utils.js"; +import { MonitorWithChecks } from "@/types/index.js"; +import { NormalizeData } from "@/utils/dataUtils.js"; const SERVICE_NAME = "statusPageController"; class StatusPageController { static SERVICE_NAME = SERVICE_NAME; - private db: any; - constructor(db: any) { - this.db = db; + private statusPageService: IStatusPageService; + private monitorsRepository: IMonitorsRepository; + private settingsService: ISettingsService; + constructor(statusPageService: IStatusPageService, monitorsRepository: IMonitorsRepository, settingsService: ISettingsService) { + this.statusPageService = statusPageService; + this.monitorsRepository = monitorsRepository; + this.settingsService = settingsService; } get serviceName() { @@ -21,13 +32,10 @@ class StatusPageController { await createStatusPageBodyValidation.validateAsync(req.body); await imageValidation.validateAsync(req.file); - const { id, teamId } = req.user ?? {}; - const statusPage = await this.db.statusPageModule.createStatusPage({ - statusPageData: req.body, - image: req.file, - userId: id, - teamId, - }); + const teamId = requireTeamId(req?.user?.teamId); + const userId = requireUserId(req?.user?.id); + const statusPage = await this.statusPageService.createStatusPage(userId, teamId, req.file, req.body); + return res.status(200).json({ success: true, msg: "Status page created successfully", @@ -42,8 +50,12 @@ class StatusPageController { try { await createStatusPageBodyValidation.validateAsync(req.body); await imageValidation.validateAsync(req.file); - - const statusPage = await this.db.statusPageModule.updateStatusPage(req.body, req.file); + const teamId = requireTeamId(req?.user?.teamId); + const statusPageId = req.params.id; + if (!statusPageId) { + throw new AppError({ message: "Status page ID is required", status: 400 }); + } + const statusPage = await this.statusPageService.updateStatusPage(statusPageId, teamId, req.file, req.body); if (statusPage === null) { throw new AppError({ message: "Status page not found", status: 404 }); } @@ -57,29 +69,32 @@ class StatusPageController { } }; - getStatusPage = async (req: Request, res: Response, next: NextFunction) => { - try { - const statusPage = await this.db.statusPageModule.getStatusPage(); - return res.status(200).json({ - success: true, - msg: "Status page retrieved successfully", - data: statusPage, - }); - } catch (error) { - next(error); - } - }; - getStatusPageByUrl = async (req: Request, res: Response, next: NextFunction) => { try { await getStatusPageParamValidation.validateAsync(req.params); await getStatusPageQueryValidation.validateAsync(req.query); - const statusPage = await this.db.statusPageModule.getStatusPageByUrl(req.params.url); + if (!req.params.url) { + throw new AppError({ message: "Status page URL is required", status: 400 }); + } + + const statusPage = await this.statusPageService.getStatusPageByUrl(req.params.url); + const settings = await this.settingsService.getDBSettings(); + const showURL = settings.showURL; + + const monitors = await this.monitorsRepository.findByIdsWithChecks(statusPage.monitors); + const normalizedMonitors = monitors.map((monitor) => { + const normalizedChecks = NormalizeData(monitor.checks, 10, 100); + if (!showURL) { + const { url, port, secret, notifications, ...rest } = monitor; + return { ...rest, checks: normalizedChecks }; + } + return { ...monitor, checks: normalizedChecks }; + }); return res.status(200).json({ success: true, msg: "Status page retrieved successfully", - data: statusPage, + data: { statusPage, monitors: normalizedMonitors }, }); } catch (error) { next(error); @@ -87,8 +102,8 @@ class StatusPageController { }; getStatusPagesByTeamId = async (req: Request, res: Response, next: NextFunction) => { try { - const teamId = req.user?.teamId; - const statusPages = await this.db.statusPageModule.getStatusPagesByTeamId(teamId); + const teamId = requireTeamId(req.user?.teamId); + const statusPages = await this.statusPageService.getStatusPagesByTeamId(teamId); return res.status(200).json({ success: true, @@ -99,9 +114,15 @@ class StatusPageController { next(error); } }; + deleteStatusPage = async (req: Request, res: Response, next: NextFunction) => { try { - await this.db.statusPageModule.deleteStatusPage(req.params.url); + const statusPageId = req.params.id; + if (!statusPageId) { + throw new AppError({ message: "Status page ID is required", status: 400 }); + } + const teamId = requireTeamId(req.user?.teamId); + await this.statusPageService.deleteStatusPage(statusPageId, teamId); return res.status(200).json({ success: true, msg: "Status page deleted successfully", diff --git a/server/src/repositories/monitors/IMonitorsRepository.ts b/server/src/repositories/monitors/IMonitorsRepository.ts index 8ead5a688..072772e9e 100644 --- a/server/src/repositories/monitors/IMonitorsRepository.ts +++ b/server/src/repositories/monitors/IMonitorsRepository.ts @@ -1,4 +1,4 @@ -import { type MonitorType, type Monitor, type MonitorsSummary } from "@/types/index.js"; +import { type MonitorType, type Monitor, type MonitorsSummary, type MonitorWithChecks } from "@/types/index.js"; export interface TeamQueryConfig { limit?: number; @@ -25,6 +25,7 @@ export interface IMonitorsRepository { findAll(): Promise; findByTeamId(teamId: string, config: TeamQueryConfig): Promise; findByIds(monitorIds: string[]): Promise; + findByIdsWithChecks(monitorIds: string[], checksCount?: number): Promise; // update updateById(monitorId: string, teamId: string, updates: Partial): Promise; diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index ad7e8577d..00d921e18 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -1,7 +1,7 @@ import { MonitorModel } from "@/db/models/index.js"; import type { MonitorDocument } from "@/db/models/index.js"; -import type { Monitor, MonitorsSummary } from "@/types/index.js"; -import mongoose, { type FilterQuery } from "mongoose"; +import type { Monitor, MonitorsSummary, MonitorWithChecks, Check } from "@/types/index.js"; +import mongoose, { type FilterQuery, type PipelineStage } from "mongoose"; import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js"; import { AppError } from "@/utils/AppError.js"; @@ -79,6 +79,78 @@ class MongoMonitorsRepository implements IMonitorsRepository { return this.mapDocuments(monitors); }; + findByIdsWithChecks = async (monitorIds: string[], checksCount: number = 25): Promise => { + if (!monitorIds.length) { + return []; + } + + const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); + + const pipeline: PipelineStage[] = [ + { $match: { _id: { $in: objectIds } } }, + { + $lookup: { + from: "checks", + let: { monitorId: "$_id" }, + pipeline: [ + { $match: { $expr: { $eq: ["$metadata.monitorId", "$$monitorId"] } } }, + { $sort: { createdAt: -1 } }, + { $limit: checksCount }, + ], + as: "checks", + }, + }, + { + $lookup: { + from: "maintenancewindows", + let: { monitorId: "$_id" }, + pipeline: [{ $match: { $expr: { $eq: ["$monitorId", "$$monitorId"] } } }], + as: "maintenanceWindows", + }, + }, + { + $lookup: { + from: "monitorstats", + localField: "_id", + foreignField: "monitorId", + as: "stats", + }, + }, + { + $addFields: { + isMaintenance: { + $reduce: { + input: "$maintenanceWindows", + initialValue: false, + in: { + $or: [ + "$$value", + { + $and: [ + { $eq: ["$$this.active", true] }, + { $lte: ["$$this.start", "$$NOW"] }, + { $gte: ["$$this.end", "$$NOW"] }, + ], + }, + ], + }, + }, + }, + uptimePercentage: { $arrayElemAt: ["$stats.uptimePercentage", 0] }, + }, + }, + { + $project: { + maintenanceWindows: 0, + stats: 0, + }, + }, + ]; + + const documents = await MonitorModel.aggregate(pipeline); + return documents.map((doc) => this.toEntityWithChecks(doc)); + }; + findMonitorCountByTeamIdAndType = async (teamId: string, config?: TeamQueryConfig): Promise => { const { type } = config ?? {}; @@ -249,6 +321,90 @@ class MongoMonitorsRepository implements IMonitorsRepository { updatedAt: toDateString(doc.updatedAt), }; }; + + private toEntityWithChecks = (doc: any): MonitorWithChecks => { + const toStringId = (value: unknown): string => { + if (value instanceof mongoose.Types.ObjectId) { + return value.toString(); + } + return value?.toString() ?? ""; + }; + + const toDateString = (value: Date | string): string => { + if (!value) return ""; + return value instanceof Date ? value.toISOString() : value; + }; + + const notificationIds = (doc.notifications ?? []).map((notification: unknown) => toStringId(notification)); + + const checks: Check[] = (doc.checks ?? []).map((check: any) => ({ + id: toStringId(check._id), + metadata: { + monitorId: toStringId(check.metadata?.monitorId), + teamId: toStringId(check.metadata?.teamId), + type: check.metadata?.type, + }, + status: check.status, + responseTime: check.responseTime, + timings: check.timings, + statusCode: check.statusCode, + message: check.message, + ack: check.ack, + ackAt: check.ackAt ?? null, + expiry: toDateString(check.expiry), + cpu: check.cpu, + memory: check.memory, + disk: check.disk, + host: check.host, + errors: check.errors, + capture: check.capture, + net: check.net, + accessibility: check.accessibility, + bestPractices: check.bestPractices, + seo: check.seo, + performance: check.performance, + audits: check.audits, + __v: check.__v, + createdAt: toDateString(check.createdAt), + updatedAt: toDateString(check.updatedAt), + })); + + return { + id: toStringId(doc._id), + userId: toStringId(doc.userId), + teamId: toStringId(doc.teamId), + name: doc.name, + description: doc.description ?? undefined, + status: doc.status ?? undefined, + statusWindow: doc.statusWindow ?? [], + statusWindowSize: doc.statusWindowSize, + statusWindowThreshold: doc.statusWindowThreshold, + type: doc.type, + ignoreTlsErrors: doc.ignoreTlsErrors, + jsonPath: doc.jsonPath ?? undefined, + expectedValue: doc.expectedValue ?? undefined, + matchMethod: doc.matchMethod ?? undefined, + url: doc.url, + port: doc.port ?? undefined, + isActive: doc.isActive, + interval: doc.interval, + uptimePercentage: doc.uptimePercentage ?? undefined, + notifications: notificationIds, + secret: doc.secret ?? undefined, + thresholds: doc.thresholds ?? undefined, + alertThreshold: doc.alertThreshold, + cpuAlertThreshold: doc.cpuAlertThreshold, + memoryAlertThreshold: doc.memoryAlertThreshold, + diskAlertThreshold: doc.diskAlertThreshold, + tempAlertThreshold: doc.tempAlertThreshold, + selectedDisks: doc.selectedDisks ?? [], + gameId: doc.gameId ?? undefined, + group: doc.group ?? null, + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), + checks, + }; + }; } export default MongoMonitorsRepository; diff --git a/server/src/repositories/status-pages/IStatusPagesRepository.ts b/server/src/repositories/status-pages/IStatusPagesRepository.ts index 1b9f9303b..c901b3c6d 100644 --- a/server/src/repositories/status-pages/IStatusPagesRepository.ts +++ b/server/src/repositories/status-pages/IStatusPagesRepository.ts @@ -2,10 +2,15 @@ import type { StatusPage } from "@/types/statusPage.js"; export interface IStatusPagesRepository { // create + create(userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise; // single fetch + findByUrl(url: string): Promise; + findByTeamId(teamId: string): Promise; // collection fetch // update + updateById(id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise; // delete + deleteById(id: string, teamId: string): Promise; // other removeMonitorFromStatusPages(monitorId: string): Promise; } diff --git a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts index e1eb4715b..d5461bef3 100644 --- a/server/src/repositories/status-pages/MongoStatusPagesRepository.ts +++ b/server/src/repositories/status-pages/MongoStatusPagesRepository.ts @@ -2,6 +2,7 @@ import { IStatusPagesRepository } from "@/repositories/index.js"; import { type StatusPageDocument, StatusPageModel } from "@/db/models/StatusPage.js"; import type { StatusPage, StatusPageLogo } from "@/types/statusPage.js"; import mongoose from "mongoose"; +import { AppError } from "@/utils/AppError.js"; class MongoStatusPagesRepository implements IStatusPagesRepository { private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { @@ -56,6 +57,74 @@ class MongoStatusPagesRepository implements IStatusPagesRepository { }; }; + private mapDocuments = (documents: StatusPageDocument[]): StatusPage[] => { + if (!documents?.length) { + return []; + } + return documents.map((doc) => this.toEntity(doc)); + }; + + create = async (userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise => { + const statusPage = new StatusPageModel({ + ...data, + userId, + teamId, + }); + if (image) { + statusPage.logo = { + data: image.buffer, + contentType: image.mimetype, + }; + } + await statusPage.save(); + return this.toEntity(statusPage); + }; + + findByUrl = async (url: string): Promise => { + const statusPage = await StatusPageModel.findOne({ + url, + }); + if (!statusPage) { + throw new AppError({ message: "Status page not found", status: 404 }); + } + return this.toEntity(statusPage); + // Get status page + }; + + findByTeamId = async (teamId: string): Promise => { + const statusPages = await StatusPageModel.find({ teamId }); + return this.mapDocuments(statusPages); + }; + + updateById = async (id: string, teamId: string, image: Express.Multer.File | undefined, patch: Partial): Promise => { + if (image) { + patch.logo = { + data: image.buffer, + contentType: image.mimetype, + }; + } else { + patch.logo = null; + } + + const statusPage = await StatusPageModel.findOneAndUpdate({ teamId, _id: id }, patch, { + new: true, + }); + + if (!statusPage) { + throw new AppError({ message: "Status page not found", status: 404 }); + } + + return this.toEntity(statusPage); + }; + + deleteById = async (id: string, teamId: string): Promise => { + const statusPage = await StatusPageModel.findOneAndDelete({ _id: id, teamId }); + if (!statusPage) { + throw new AppError({ message: "Status page not found", status: 404 }); + } + return this.toEntity(statusPage); + }; + removeMonitorFromStatusPages = async (monitorId: string): Promise => { const res = await StatusPageModel.updateMany({ monitors: monitorId }, { $pull: { monitors: monitorId } }); return res.modifiedCount; diff --git a/server/src/routes/statusPageRoute.ts b/server/src/routes/statusPageRoute.ts index 9d6902aac..f70802f73 100755 --- a/server/src/routes/statusPageRoute.ts +++ b/server/src/routes/statusPageRoute.ts @@ -13,11 +13,10 @@ class StatusPageRoutes { } initRoutes(verifyJWT: RequestHandler) { - this.router.get("/", this.statusPageController.getStatusPage); this.router.get("/team", verifyJWT, this.statusPageController.getStatusPagesByTeamId); this.router.post("/", upload.single("logo"), verifyJWT, this.statusPageController.createStatusPage); - this.router.put("/", upload.single("logo"), verifyJWT, this.statusPageController.updateStatusPage); + this.router.put("/:id", upload.single("logo"), verifyJWT, this.statusPageController.updateStatusPage); this.router.get("/:url", this.statusPageController.getStatusPageByUrl); this.router.delete("/:url(*)", verifyJWT, this.statusPageController.deleteStatusPage); diff --git a/server/src/service/business/statusPageService.ts b/server/src/service/business/statusPageService.ts new file mode 100644 index 000000000..971f6897f --- /dev/null +++ b/server/src/service/business/statusPageService.ts @@ -0,0 +1,42 @@ +import { type IStatusPagesRepository } from "@/repositories/index.js"; +import { StatusPage } from "@/types/index.js"; +export interface IStatusPageService { + createStatusPage(userId: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise; + getStatusPageByUrl(url: string): Promise; + getStatusPagesByTeamId(teamId: string): Promise; + updateStatusPage(id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise; + + deleteStatusPage(statusPageId: string, teamId: string): Promise; +} + +export class StatusPageService implements IStatusPageService { + private statusPagesRepository: IStatusPagesRepository; + constructor(statusPagesRepository: IStatusPagesRepository) { + this.statusPagesRepository = statusPagesRepository; + } + + createStatusPage = async ( + userId: string, + teamId: string, + image: Express.Multer.File | undefined, + data: Partial + ): Promise => { + return await this.statusPagesRepository.create(userId, teamId, image, data); + }; + + getStatusPageByUrl = async (url: string): Promise => { + return await this.statusPagesRepository.findByUrl(url); + }; + + getStatusPagesByTeamId = async (teamId: string): Promise => { + return await this.statusPagesRepository.findByTeamId(teamId); + }; + + updateStatusPage = async (id: string, teamId: string, image: Express.Multer.File | undefined, data: Partial): Promise => { + return await this.statusPagesRepository.updateById(id, teamId, image, data); + }; + + deleteStatusPage = async (statusPageId: string, teamId: string): Promise => { + return await this.statusPagesRepository.deleteById(statusPageId, teamId); + }; +} diff --git a/server/src/service/index.ts b/server/src/service/index.ts index 7f7d0f14a..7ec49288f 100644 --- a/server/src/service/index.ts +++ b/server/src/service/index.ts @@ -11,3 +11,4 @@ export * from "@/service/infrastructure/notificationsService.js"; export * from "@/service/infrastructure/statusService.js"; export * from "@/service/infrastructure/notificationProviders/webhook.js"; export * from "@/service/infrastructure/notificationProviders/matrix.js"; +export * from "@/service/business/statusPageService.js"; diff --git a/server/src/types/monitor.ts b/server/src/types/monitor.ts index 1d41a7333..c7d99a88a 100644 --- a/server/src/types/monitor.ts +++ b/server/src/types/monitor.ts @@ -1,3 +1,4 @@ +import type { Check } from "@/types/check.js"; export const MonitorTypes = ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "unknown"] as const; export type MonitorType = (typeof MonitorTypes)[number]; @@ -53,7 +54,7 @@ export interface MonitorsSummary { } export interface MonitorWithChecks extends Monitor { - checks: import("./check.js").Check[]; + checks: Check[]; } export interface MonitorsWithChecksByTeamIdResult { From 995ae5252d6e44cecacd1da0cecc6be7a231c9c3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 22 Jan 2026 20:57:45 +0000 Subject: [PATCH 343/400] format --- .../repositories/monitors/MongoMonitorsRepository.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/server/src/repositories/monitors/MongoMonitorsRepository.ts b/server/src/repositories/monitors/MongoMonitorsRepository.ts index 00d921e18..5ce029634 100644 --- a/server/src/repositories/monitors/MongoMonitorsRepository.ts +++ b/server/src/repositories/monitors/MongoMonitorsRepository.ts @@ -92,11 +92,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { $lookup: { from: "checks", let: { monitorId: "$_id" }, - pipeline: [ - { $match: { $expr: { $eq: ["$metadata.monitorId", "$$monitorId"] } } }, - { $sort: { createdAt: -1 } }, - { $limit: checksCount }, - ], + pipeline: [{ $match: { $expr: { $eq: ["$metadata.monitorId", "$$monitorId"] } } }, { $sort: { createdAt: -1 } }, { $limit: checksCount }], as: "checks", }, }, @@ -126,11 +122,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { $or: [ "$$value", { - $and: [ - { $eq: ["$$this.active", true] }, - { $lte: ["$$this.start", "$$NOW"] }, - { $gte: ["$$this.end", "$$NOW"] }, - ], + $and: [{ $eq: ["$$this.active", true] }, { $lte: ["$$this.start", "$$NOW"] }, { $gte: ["$$this.end", "$$NOW"] }], }, ], }, From 6b89db131d74c40e7af564a7784a7a9eddc73bf6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 22 Jan 2026 21:01:45 +0000 Subject: [PATCH 344/400] remove statuspagemodule --- server/src/config/services.ts | 6 - server/src/db/MongoDB.js | 3 +- server/src/db/modules/statusPageModule.js | 299 ---------------------- server/src/{shutdown.js => shutdown.ts} | 8 +- 4 files changed, 6 insertions(+), 310 deletions(-) delete mode 100755 server/src/db/modules/statusPageModule.js rename server/src/{shutdown.js => shutdown.ts} (74%) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index 6fb27feb7..3535f44d7 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -43,7 +43,6 @@ import { games, GameDig } from "gamedig"; import jmespath from "jmespath"; // DB Modules -import { NormalizeData } from "../utils/dataUtils.js"; import { GenerateAvatarImage } from "../utils/imageProcessing.js"; import { ParseBoolean } from "../utils/utils.js"; @@ -51,16 +50,13 @@ import { ParseBoolean } from "../utils/utils.js"; import Monitor from "../db/models/Monitor.js"; import User from "../db/models/User.js"; import InviteToken from "../db/models/Invite.js"; -import StatusPage from "../db/models/StatusPage.js"; import Team from "../db/models/Team.js"; import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; import MonitorStats from "../db/models/MonitorStats.js"; import NotificationModel from "../db/models/Notification.js"; import RecoveryToken from "../db/models/RecoveryToken.js"; -import AppSettings from "../db/models/AppSettings.js"; import Incident from "../db/models/Incident.js"; -import StatusPageModule from "../db/modules/statusPageModule.js"; import IncidentModule from "../db/modules/incidentModule.js"; // repositories @@ -139,13 +135,11 @@ export const initializeServices = async ({ settingsRepository: ISettingsRepository; }): Promise => { // Create DB - const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, AppSettings }); const incidentModule = new IncidentModule({ logger, Incident, Monitor, User }); const db = new MongoDB({ logger, envSettings, - statusPageModule, incidentModule, }); diff --git a/server/src/db/MongoDB.js b/server/src/db/MongoDB.js index 07d3d31c8..f149fbd6d 100755 --- a/server/src/db/MongoDB.js +++ b/server/src/db/MongoDB.js @@ -4,10 +4,9 @@ import { runMigrations } from "./migration/index.js"; class MongoDB { static SERVICE_NAME = "MongoDB"; - constructor({ logger, envSettings, statusPageModule, incidentModule }) { + constructor({ logger, envSettings, incidentModule }) { this.logger = logger; this.envSettings = envSettings; - this.statusPageModule = statusPageModule; this.incidentModule = incidentModule; } diff --git a/server/src/db/modules/statusPageModule.js b/server/src/db/modules/statusPageModule.js deleted file mode 100755 index 78a632cb8..000000000 --- a/server/src/db/modules/statusPageModule.js +++ /dev/null @@ -1,299 +0,0 @@ -const SERVICE_NAME = "statusPageModule"; - -class StatusPageModule { - constructor({ StatusPage, NormalizeData, AppSettings }) { - this.StatusPage = StatusPage; - this.NormalizeData = NormalizeData; - this.AppSettings = AppSettings; - } - - createStatusPage = async ({ statusPageData, image, userId, teamId }) => { - try { - const statusPage = new this.StatusPage({ - ...statusPageData, - userId, - teamId, - }); - if (image) { - statusPage.logo = { - data: image.buffer, - contentType: image.mimetype, - }; - } - await statusPage.save(); - return statusPage; - } catch (error) { - if (error?.code === 11000) { - // Handle duplicate URL errors - error.status = 400; - error.message = "Status page URL must be unique"; - } - error.service = SERVICE_NAME; - error.method = "createStatusPage"; - throw error; - } - }; - - updateStatusPage = async (statusPageData, image) => { - try { - if (image) { - statusPageData.logo = { - data: image.buffer, - contentType: image.mimetype, - }; - } else { - statusPageData.logo = null; - } - - if (statusPageData.deleteSubmonitors === "true") { - statusPageData.subMonitors = []; - } - const statusPage = await this.StatusPage.findOneAndUpdate({ url: statusPageData.url }, statusPageData, { - new: true, - }); - - return statusPage; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "updateStatusPage"; - throw error; - } - }; - - getStatusPageByUrl = async (url) => { - // TODO This is deprecated, can remove and have controller call getStatusPage - try { - return this.getStatusPage(url); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getStatusPageByUrl"; - throw error; - } - }; - - getStatusPagesByTeamId = async (teamId) => { - try { - const statusPages = await this.StatusPage.find({ teamId }); - return statusPages; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getStatusPagesByTeamId"; - throw error; - } - }; - - getStatusPage = async (url) => { - try { - const preliminaryStatusPage = await this.StatusPage.findOne({ url }); - if (!preliminaryStatusPage) { - const error = new Error("Status page not found"); - error.status = 404; - throw error; - } - - if (!preliminaryStatusPage.monitors || preliminaryStatusPage.monitors.length === 0) { - const { _id, color, companyName, isPublished, logo, originalMonitors, showCharts, showUptimePercentage, timezone, showAdminLoginLink, url } = - preliminaryStatusPage; - return { - statusPage: { - _id, - color, - companyName, - isPublished, - logo, - originalMonitors, - showCharts, - showUptimePercentage, - timezone, - showAdminLoginLink, - url, - }, - monitors: [], - }; - } - - const statusPageQuery = await this.StatusPage.aggregate([ - { $match: { url: url } }, - { - $set: { - originalMonitors: "$monitors", - }, - }, - { - $lookup: { - from: "monitors", - localField: "monitors", - foreignField: "_id", - as: "monitors", - }, - }, - { - $unwind: { - path: "$monitors", - preserveNullAndEmptyArrays: true, - }, - }, - { - $lookup: { - from: "maintenancewindows", - let: { monitorId: "$monitors._id" }, - pipeline: [{ $match: { $expr: { $eq: ["$monitorId", "$$monitorId"] } } }], - as: "monitors.maintenanceWindows", - }, - }, - { - $lookup: { - from: "checks", - let: { monitorId: "$monitors._id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$metadata.monitorId", "$$monitorId"] }, - }, - }, - { $sort: { createdAt: -1 } }, - { $limit: 25 }, - ], - as: "monitors.checks", - }, - }, - { - $lookup: { - from: "monitorstats", - localField: "monitors._id", - foreignField: "monitorId", - as: "monitors.stats", - }, - }, - { - $addFields: { - "monitors.orderIndex": { - $indexOfArray: ["$originalMonitors", "$monitors._id"], - }, - "monitors.isMaintenance": { - $reduce: { - input: "$monitors.maintenanceWindows", - initialValue: false, - in: { - $or: [ - "$$value", - { - $and: [{ $eq: ["$$this.active", true] }, { $lte: ["$$this.start", "$$NOW"] }, { $gte: ["$$this.end", "$$NOW"] }], - }, - ], - }, - }, - }, - "monitors.uptimePercentage": { - $arrayElemAt: ["$monitors.stats.uptimePercentage", 0], - }, - }, - }, - { $match: { "monitors.orderIndex": { $ne: -1 } } }, - { $sort: { "monitors.orderIndex": 1 } }, - - { - $group: { - _id: "$_id", - statusPage: { $first: "$$ROOT" }, - monitors: { $push: "$monitors" }, - }, - }, - { - $project: { - statusPage: { - _id: 1, - color: 1, - companyName: 1, - isPublished: 1, - logo: 1, - originalMonitors: 1, - showCharts: 1, - showUptimePercentage: 1, - timezone: 1, - showAdminLoginLink: 1, - url: 1, - }, - monitors: { - _id: 1, - userId: 1, - teamId: 1, - name: 1, - description: 1, - status: 1, - type: 1, - ignoreTlsErrors: 1, - jsonPath: 1, - expectedValue: 1, - matchMethod: 1, - url: 1, - port: 1, - isActive: 1, - interval: 1, - uptimePercentage: 1, - notifications: 1, - secret: 1, - thresholds: 1, - alertThreshold: 1, - cpuAlertThreshold: 1, - memoryAlertThreshold: 1, - diskAlertThreshold: 1, - tempAlertThreshold: 1, - checks: 1, - isMaintenance: 1, - createdAt: 1, - updatedAt: 1, - }, - }, - }, - ]); - if (!statusPageQuery.length) { - const error = new Error("Status page not found"); - error.status = 404; - throw error; - } - - const { statusPage, monitors } = statusPageQuery[0]; - - const appSettings = await this.AppSettings.findOne({ singleton: true }).lean(); - const showURL = appSettings?.showURL === true; - - const normalizedMonitors = monitors.map((monitor) => { - const normalizedChecks = this.NormalizeData(monitor.checks, 10, 100); - if (!showURL) { - const { url, port, secret, notifications, ...rest } = monitor; - return { ...rest, checks: normalizedChecks }; - } - return { ...monitor, checks: normalizedChecks }; - }); - - return { statusPage, monitors: normalizedMonitors }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getStatusPageByUrl"; - throw error; - } - }; - - deleteStatusPage = async (url) => { - try { - await this.StatusPage.deleteOne({ url }); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteStatusPage"; - throw error; - } - }; - deleteStatusPagesByMonitorId = async (monitorId) => { - try { - await this.StatusPage.deleteMany({ monitors: { $in: [monitorId] } }); - await this.StatusPage.deleteMany({ subMonitors: { $in: [monitorId] } }); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteStatusPageByMonitorId"; - throw error; - } - }; -} - -export default StatusPageModule; diff --git a/server/src/shutdown.js b/server/src/shutdown.ts similarity index 74% rename from server/src/shutdown.js rename to server/src/shutdown.ts index 27eb0f21d..2287a6a4a 100644 --- a/server/src/shutdown.js +++ b/server/src/shutdown.ts @@ -1,6 +1,7 @@ +import { InitializedServices } from "./config/services.js"; import { logger } from "./utils/logger.js"; -export const initShutdownListener = (server, services) => { +export const initShutdownListener = (server: any, services: InitializedServices) => { const SERVICE_NAME = "Server"; let isShuttingDown = false; @@ -19,11 +20,12 @@ export const initShutdownListener = (server, services) => { logger.info({ message: "Graceful shutdown complete" }); process.exit(0); } catch (error) { + const err = error as Error; logger.error({ - message: error.message, + message: err.message, service: SERVICE_NAME, method: "shutdown", - stack: error.stack, + stack: err.stack, }); process.exit(1); } From c98cba2eefeffa8b33f6ccf3c253311d97a90e08 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 22 Jan 2026 21:02:34 +0000 Subject: [PATCH 345/400] shutdown -> ts --- server/src/shutdown.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/shutdown.ts b/server/src/shutdown.ts index 2287a6a4a..877aef325 100644 --- a/server/src/shutdown.ts +++ b/server/src/shutdown.ts @@ -1,7 +1,8 @@ import { InitializedServices } from "./config/services.js"; import { logger } from "./utils/logger.js"; +import type { Server } from "http"; -export const initShutdownListener = (server: any, services: InitializedServices) => { +export const initShutdownListener = (server: Server, services: InitializedServices) => { const SERVICE_NAME = "Server"; let isShuttingDown = false; From d25ca473ded1e64e3970ee3712ae213bce5e2526 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 22 Jan 2026 21:28:28 +0000 Subject: [PATCH 346/400] purge incident module --- .../Components/IncidentDetailsModal/index.jsx | 46 +- server/src/config/services.ts | 10 +- server/src/controllers/incidentController.ts | 11 +- server/src/db/MongoDB.js | 3 +- server/src/db/modules/incidentModule.js | 484 ------------------ .../incidents/IIncidentsRepository.ts | 1 + .../incidents/MongoIncidentRepository.ts | 11 + .../src/service/business/incidentService.ts | 41 +- 8 files changed, 63 insertions(+), 544 deletions(-) delete mode 100644 server/src/db/modules/incidentModule.js diff --git a/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx index f198c5c18..d68b0b85e 100644 --- a/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx +++ b/client/src/Pages/Incidents/Components/IncidentDetailsModal/index.jsx @@ -21,12 +21,12 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => { const theme = useTheme(); const { t } = useTranslation(); const { fetchIncidentById, resolveIncident } = useFetchIncidents(); - const [incident, setIncident] = useState(null); + const [incidentData, setIncidentData] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isResolveDialogOpen, setIsResolveDialogOpen] = useState(false); const uiTimezone = useSelector((state) => state.ui.timezone); - const isActive = incident?.status === true; - const duration = useGetIncidentsDuration(incident || null, isActive); + const isActive = incidentData?.incident?.status === true; + const duration = useGetIncidentsDuration(incidentData?.incident || null, isActive); const statusColor = isActive ? theme.palette.error.main : theme.palette.success.main; const toCapitalLetter = (text) => @@ -75,7 +75,7 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => { useEffect(() => { const loadIncident = async () => { if (!incidentId || !open) { - setIncident(null); + setIncidentData(null); setIsLoading(false); return; } @@ -83,10 +83,10 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => { setIsLoading(true); try { const incidentData = await fetchIncidentById(incidentId); - setIncident(incidentData); + setIncidentData(incidentData); } catch (error) { console.error("Error fetching incident:", error); - setIncident(null); + setIncidentData(null); } finally { setIsLoading(false); } @@ -107,11 +107,13 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => { if (!incidentId) return; const updatedIncident = await fetchIncidentById(incidentId); if (updatedIncident) { - setIncident(updatedIncident); + setIncidentData(updatedIncident); } }; const renderContent = () => { + console.log(incidentData); + if (isLoading) { return ( { ); } - if (!incident) { + if (!incidentData) { return ( { variant="body1" sx={{ flex: 1 }} > - {incident?.monitorId?.name || t("incidentsPage.unknownMonitor")} + {incidentData?.monitor?.name || t("incidentsPage.unknownMonitor")} { variant="body1" sx={{ flex: 1, wordBreak: "break-word", fontFamily: "monospace" }} > - {incident?.monitorId?.url || "-"} + {incidentData?.monitor?.url || "-"} @@ -247,7 +249,7 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => { label={t("incidentsPage.startedAt")} value={ formatDateWithTz( - incident?.startTime, + incidentData?.incident?.startTime, "D MMM YYYY, h:mm A", uiTimezone ) || "-" @@ -259,7 +261,7 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => { label={t("incidentsPage.endedAt")} value={ formatDateWithTz( - incident?.endTime, + incidentData?.incident?.endTime, "D MMM YYYY, h:mm A", uiTimezone ) || "-" @@ -303,26 +305,28 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => { {!isActive && ( <> - {incident?.resolutionType === "manual" && ( + {incidentData?.incident?.resolutionType === "manual" && ( @@ -353,15 +357,15 @@ const IncidentDetailsModal = ({ open, incidentId, onClose, onResolved }) => { > {renderContent()} {!isActive && - incident?.resolutionType === "manual" && - incident?.resolvedBy?.email && ( + incidentData?.incident?.resolutionType === "manual" && + incidentData?.user?.email && ( - {t("incidentsPage.resolvedBy")}: {incident.resolvedBy.email} + {t("incidentsPage.resolvedBy")}: {incidentData.user.email} )} => { // Create DB - const incidentModule = new IncidentModule({ logger, Incident, Monitor, User }); const db = new MongoDB({ logger, envSettings, - incidentModule, }); await db.connect(); @@ -175,10 +168,11 @@ export const initializeServices = async ({ const errorService = new ErrorService(); const incidentService = new IncidentService({ - db, logger, errorService, incidentsRepository, + monitorsRepository, + usersRepository, }); const checkService = new CheckService({ diff --git a/server/src/controllers/incidentController.ts b/server/src/controllers/incidentController.ts index 6231f3cf4..72e6498f9 100644 --- a/server/src/controllers/incidentController.ts +++ b/server/src/controllers/incidentController.ts @@ -66,10 +66,13 @@ class IncidentController { getIncidentById = async (req: Request, res: Response, next: NextFunction) => { try { - const incident = await this.incidentService.getIncidentById({ - incidentId: req?.params?.incidentId, - teamId: req?.user?.teamId, - }); + const teamId = requireTeamId(req.user?.teamId); + const incidentId = req.params.incidentId; + if (!incidentId) { + throw new AppError({ message: "Incident ID is required", service: SERVICE_NAME, status: 400 }); + } + + const incident = await this.incidentService.getIncidentById(incidentId, teamId); return res.status(200).json({ success: true, diff --git a/server/src/db/MongoDB.js b/server/src/db/MongoDB.js index f149fbd6d..97498f13a 100755 --- a/server/src/db/MongoDB.js +++ b/server/src/db/MongoDB.js @@ -4,10 +4,9 @@ import { runMigrations } from "./migration/index.js"; class MongoDB { static SERVICE_NAME = "MongoDB"; - constructor({ logger, envSettings, incidentModule }) { + constructor({ logger, envSettings }) { this.logger = logger; this.envSettings = envSettings; - this.incidentModule = incidentModule; } get serviceName() { diff --git a/server/src/db/modules/incidentModule.js b/server/src/db/modules/incidentModule.js deleted file mode 100644 index 366e309f6..000000000 --- a/server/src/db/modules/incidentModule.js +++ /dev/null @@ -1,484 +0,0 @@ -import { ObjectId } from "mongodb"; - -const SERVICE_NAME = "incidentModule"; - -const dateRangeLookup = { - recent: new Date(new Date().setDate(new Date().getDate() - 2)), - hour: new Date(new Date().setHours(new Date().getHours() - 1)), - day: new Date(new Date().setDate(new Date().getDate() - 1)), - week: new Date(new Date().setDate(new Date().getDate() - 7)), - month: new Date(new Date().setMonth(new Date().getMonth() - 1)), - all: undefined, -}; - -class IncidentModule { - constructor({ logger, Incident, Monitor, User }) { - this.logger = logger; - this.Incident = Incident; - this.Monitor = Monitor; - this.User = User; - } - - createIncident = async (incidentData) => { - try { - const incident = await this.Incident.create(incidentData); - return incident; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createIncident"; - throw error; - } - }; - - createIncidents = async (incidents) => { - try { - await this.Incident.insertMany(incidents, { ordered: false }); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createIncidents"; - throw error; - } - }; - - getActiveIncidentByMonitor = async (monitorId) => { - try { - return await this.Incident.findOne({ monitorId: new ObjectId(monitorId), status: true }); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getActiveIncidentByMonitor"; - throw error; - } - }; - - getActiveIncidentsByMonitors = async (monitorIds) => { - try { - if (!monitorIds || monitorIds.length === 0) { - return new Map(); - } - - const objectIds = monitorIds.map((id) => new ObjectId(id)); - const incidents = await this.Incident.find({ - monitorId: { $in: objectIds }, - status: true, - }); - - const map = new Map(); - incidents.forEach((incident) => { - const monitorIdStr = incident.monitorId.toString(); - map.set(monitorIdStr, incident); - }); - - return map; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getActiveIncidentsByMonitors"; - throw error; - } - }; - - getLastManuallyResolvedIncident = async (monitorId) => { - try { - return await this.Incident.findOne({ - monitorId: new ObjectId(monitorId), - status: false, - resolutionType: "manual", - }) - .sort({ endTime: -1 }) - .limit(1); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getLastManuallyResolvedIncident"; - throw error; - } - }; - - getIncidentById = async (incidentId) => { - try { - return await this.Incident.findById(incidentId).populate("monitorId", "name type url").populate("resolvedBy", "name email"); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getIncidentById"; - throw error; - } - }; - - resolveIncident = async (incidentId, { resolutionType = "automatic", resolvedBy = null, comment = null, endTime = new Date() } = {}) => { - try { - const update = { - status: false, - endTime, - resolutionType, - resolvedBy, - ...(comment !== null && { comment }), - }; - const incident = await this.Incident.findOneAndUpdate({ _id: new ObjectId(incidentId) }, { $set: update }, { new: true }); - - if (!incident) { - throw new Error("Incident not found"); - } - - return incident; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "resolveIncident"; - throw error; - } - }; - - addCheckToIncident = async (incidentId, checkId) => { - try { - const incident = await this.Incident.findOneAndUpdate( - { _id: new ObjectId(incidentId) }, - { $addToSet: { checks: new ObjectId(checkId) } }, - { new: true } - ); - - if (!incident) { - throw new Error("Incident not found"); - } - - return incident; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "addCheckToIncident"; - throw error; - } - }; - - addChecksToIncidentsBatch = async (operations) => { - try { - if (!operations || operations.length === 0) { - return; - } - - const bulkOps = operations.map((op) => ({ - updateOne: { - filter: { _id: new ObjectId(op.incidentId) }, - update: { $addToSet: { checks: new ObjectId(op.checkId) } }, - }, - })); - - await this.Incident.bulkWrite(bulkOps, { ordered: false }); - } catch (error) { - error.service = SERVICE_NAME; - error.method = "addChecksToIncidentsBatch"; - throw error; - } - }; - - /** - * Get incidents by team with filtering - * - * STATUS PARAMETER CONTRACT (IMPORTANT - Frontend dependency): - * This method has an implicit contract with frontend's useFetchIncidents hook: - * - * - status === undefined: Frontend uses this to represent "all incidents" (filter === "all") - * Backend applies $or logic: { status: true } OR { status: false, endTime: >= dateRange } - * This shows all active incidents + resolved incidents within the date range - * - * - status === true: Frontend uses for "active incidents" (filter === "active") - * Backend shows all active incidents (no date filter applied) - * Active incidents are currently happening, so date range doesn't apply - * - * - status === false: Frontend uses for "resolved incidents" (filter === "resolved") - * Backend filters by endTime >= dateRange - * Only shows incidents that were resolved within the specified date range - * - * WARNING: Changing this logic will break frontend filtering behavior. - * The frontend depends on undefined status triggering the $or logic for "all incidents". - * This contract must be maintained when modifying either frontend or backend code. - * - * @param {Object} params - Query parameters - * @param {string} params.teamId - Team ID - * @param {string} [params.sortOrder] - Sort order (asc/desc) - * @param {string} [params.dateRange] - Date range filter - * @param {number} [params.page] - Page number - * @param {number} [params.rowsPerPage] - Rows per page - * @param {boolean|undefined} [params.status] - Status filter. undefined = all, true = active, false = resolved - * @param {string} [params.monitorId] - Monitor ID filter - * @param {string} [params.resolutionType] - Resolution type filter - */ - getIncidentsByTeam = async ({ teamId, sortOrder, dateRange, page, rowsPerPage, status, monitorId, resolutionType }) => { - try { - page = Number.isFinite(parseInt(page)) ? parseInt(page) : 0; - rowsPerPage = Number.isFinite(parseInt(rowsPerPage)) ? parseInt(rowsPerPage) : 20; - - let statusBoolean = undefined; - if (status !== undefined && status !== null) { - if (typeof status === "string") { - statusBoolean = status === "true"; - } else if (typeof status === "boolean") { - statusBoolean = status; - } - } - - const matchStage = { - teamId: new ObjectId(teamId), - ...(statusBoolean !== undefined && { status: statusBoolean }), - ...(monitorId && { monitorId: new ObjectId(monitorId) }), - ...(resolutionType && { resolutionType }), - }; - - // Date range filter logic (see contract documentation above): - // - Active incidents (statusBoolean === true): always show (no date filter) - they're currently happening - // - Resolved incidents (statusBoolean === false): filter by endTime (when they were resolved) - // - All incidents (statusBoolean === undefined): show all active + resolved in the range using $or - if (dateRangeLookup[dateRange]) { - const dateThreshold = dateRangeLookup[dateRange]; - if (statusBoolean === true) { - // Active incidents: show all active incidents regardless of when they started - // No date filter applied - } else if (statusBoolean === false) { - // Resolved incidents: only show if resolved in the range - matchStage.endTime = { $gte: dateThreshold }; - } else { - // All incidents: show all active + resolved incidents in the range - // This is the critical contract: undefined status triggers $or logic - matchStage.$or = [ - { status: true }, // All active incidents - { status: false, endTime: { $gte: dateThreshold } }, // Resolved in range - ]; - } - } - - sortOrder = sortOrder === "asc" ? 1 : -1; - - const skip = page * rowsPerPage; - - const incidents = await this.Incident.aggregate([ - { $match: matchStage }, - { $sort: { startTime: sortOrder } }, - { - $lookup: { - from: "monitors", - localField: "monitorId", - foreignField: "_id", - as: "monitor", - }, - }, - { - $facet: { - summary: [{ $count: "incidentsCount" }], - incidents: [{ $skip: skip }, { $limit: rowsPerPage }], - }, - }, - { - $project: { - incidentsCount: { - $ifNull: [{ $arrayElemAt: ["$summary.incidentsCount", 0] }, 0], - }, - incidents: { - $map: { - input: { $ifNull: ["$incidents", []] }, - as: "incident", - in: { - $mergeObjects: [ - "$$incident", - { - monitorName: { - $let: { - vars: { - monitor: { $arrayElemAt: ["$$incident.monitor", 0] }, - }, - in: { - $ifNull: ["$$monitor.name", null], - }, - }, - }, - }, - ], - }, - }, - }, - }, - }, - ]); - - return incidents[0]; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getIncidentsByTeam"; - throw error; - } - }; - - getIncidentSummary = async ({ teamId, limit = 10 }) => { - try { - const matchStage = { - teamId: new ObjectId(teamId), - }; - - // Get basic counts and resolution types - const countsPipeline = [ - { $match: matchStage }, - { - $group: { - _id: "$status", - count: { $sum: 1 }, - manualResolutions: { - $sum: { - $cond: [{ $eq: ["$resolutionType", "manual"] }, 1, 0], - }, - }, - automaticResolutions: { - $sum: { - $cond: [{ $eq: ["$resolutionType", "automatic"] }, 1, 0], - }, - }, - }, - }, - ]; - - const counts = await this.Incident.aggregate(countsPipeline); - - // Calculate totals - let total = 0; - let active = 0; - let resolved = 0; - let manual = 0; - let automatic = 0; - - counts.forEach((item) => { - total += item.count; - if (item._id === true) { - active = item.count; - } - if (item._id === false) { - resolved = item.count; - } - manual += item.manualResolutions; - automatic += item.automaticResolutions; - }); - - // Calculate average resolution time (in milliseconds) - const resolutionTimePipeline = [ - { $match: { ...matchStage, status: false, endTime: { $exists: true, $ne: null } } }, - { - $project: { - resolutionTime: { - $subtract: ["$endTime", "$startTime"], - }, - }, - }, - { - $group: { - _id: null, - avgResolutionTime: { $avg: "$resolutionTime" }, - }, - }, - ]; - - const resolutionTimeResult = await this.Incident.aggregate(resolutionTimePipeline); - const avgResolutionTimeMs = resolutionTimeResult[0]?.avgResolutionTime || 0; - const avgResolutionTimeHours = avgResolutionTimeMs / (1000 * 60 * 60); // Convert to hours - - // Get monitor with most incidents - const monitorPipeline = [ - { $match: matchStage }, - { - $group: { - _id: "$monitorId", - count: { $sum: 1 }, - }, - }, - { $sort: { count: -1 } }, - { $limit: 1 }, - { - $lookup: { - from: "monitors", - localField: "_id", - foreignField: "_id", - as: "monitor", - }, - }, - { - $project: { - monitorId: "$_id", - count: 1, - monitorName: { $arrayElemAt: ["$monitor.name", 0] }, - }, - }, - ]; - - const monitorResult = await this.Incident.aggregate(monitorPipeline); - const topMonitor = monitorResult[0] || null; - - // Get latest incidents - const latestIncidentsPipeline = [ - { $match: matchStage }, - { $sort: { createdAt: -1 } }, - { $limit: Math.max(1, parseInt(limit) || 10) }, - { - $lookup: { - from: "monitors", - localField: "monitorId", - foreignField: "_id", - as: "monitor", - }, - }, - { - $project: { - _id: 1, - monitorId: 1, - monitorName: { $arrayElemAt: ["$monitor.name", 0] }, - status: 1, - startTime: 1, - endTime: 1, - resolutionType: 1, - message: 1, - statusCode: 1, - createdAt: 1, - }, - }, - ]; - - const latestIncidents = await this.Incident.aggregate(latestIncidentsPipeline); - - return { - totalActive: active, - avgResolutionTimeHours: Math.round(avgResolutionTimeHours * 100) / 100, // Round to 2 decimal places - topMonitor: topMonitor - ? { - monitorId: topMonitor.monitorId, - monitorName: topMonitor.monitorName, - incidentCount: topMonitor.count, - } - : null, - total: total, - totalManualResolutions: manual, - totalAutomaticResolutions: automatic, - latestIncidents: latestIncidents, - }; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getIncidentSummary"; - throw error; - } - }; - - deleteIncidentsByMonitor = async (monitorId) => { - try { - const result = await this.Incident.deleteMany({ monitorId: new ObjectId(monitorId) }); - return result.deletedCount; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteIncidentsByMonitor"; - throw error; - } - }; - - deleteIncidentsByTeamId = async (teamId) => { - try { - const teamMonitors = await this.Monitor.find({ teamId }, { _id: 1 }); - const monitorIds = teamMonitors.map((monitor) => monitor._id); - const deleteResult = await this.Incident.deleteMany({ monitorId: { $in: monitorIds } }); - return deleteResult.deletedCount; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deleteIncidentsByTeamId"; - throw error; - } - }; -} - -export default IncidentModule; diff --git a/server/src/repositories/incidents/IIncidentsRepository.ts b/server/src/repositories/incidents/IIncidentsRepository.ts index 7c2b0179e..3c136bf43 100644 --- a/server/src/repositories/incidents/IIncidentsRepository.ts +++ b/server/src/repositories/incidents/IIncidentsRepository.ts @@ -4,6 +4,7 @@ export interface IIncidentsRepository { // create create(incident: Partial): Promise; // fetch + findById(incidentId: string, teamId: string): Promise; findActiveByIncidentId(incidentId: string, teamId: string): Promise; findActiveByMonitorId(monitorId: string, teamId: string): Promise; findByTeamId( diff --git a/server/src/repositories/incidents/MongoIncidentRepository.ts b/server/src/repositories/incidents/MongoIncidentRepository.ts index 10cb47213..b029953ca 100644 --- a/server/src/repositories/incidents/MongoIncidentRepository.ts +++ b/server/src/repositories/incidents/MongoIncidentRepository.ts @@ -79,6 +79,17 @@ class MongoIncidentRepository implements IIncidentsRepository { return this.toEntity(newIncident); } + findById = async (incidentId: string, teamId: string): Promise => { + const incident = await IncidentModel.findOne({ + _id: new mongoose.Types.ObjectId(incidentId), + teamId: new mongoose.Types.ObjectId(teamId), + }); + if (!incident) { + throw new AppError({ message: `Incident with id ${incidentId} not found`, status: 404 }); + } + return this.toEntity(incident); + }; + findActiveByIncidentId = async (incidentId: string, teamId: string): Promise => { const incident = await IncidentModel.findOne({ _id: new mongoose.Types.ObjectId(incidentId), diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index e3fb4cfc3..37a02e30d 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -2,7 +2,7 @@ const SERVICE_NAME = "incidentService"; import type { Monitor } from "@/types/monitor.js"; import { AppError } from "@/utils/AppError.js"; import { ParseBoolean } from "@/utils/utils.js"; -import type { IIncidentsRepository } from "@/repositories/index.js"; +import type { IIncidentsRepository, IMonitorsRepository, IUsersRepository } from "@/repositories/index.js"; import type { Incident } from "@/types/index.js"; const dateRangeLookup: Record = { @@ -17,27 +17,30 @@ const dateRangeLookup: Record = { class IncidentService { static SERVICE_NAME = SERVICE_NAME; - private db: any; private logger: any; private errorService: any; - private: any; private incidentsRepository: IIncidentsRepository; + private monitorsRepository: IMonitorsRepository; + private usersRepository: IUsersRepository; constructor({ - db, logger, errorService, incidentsRepository, + monitorsRepository, + usersRepository, }: { - db: any; logger: any; errorService: any; incidentsRepository: IIncidentsRepository; + monitorsRepository: IMonitorsRepository; + usersRepository: IUsersRepository; }) { - this.db = db; this.logger = logger; this.errorService = errorService; this.incidentsRepository = incidentsRepository; + this.monitorsRepository = monitorsRepository; + this.usersRepository = usersRepository; } get serviceName() { @@ -193,27 +196,15 @@ class IncidentService { } }; - getIncidentById = async ({ incidentId, teamId }: { incidentId: string; teamId: string }) => { + getIncidentById = async (incidentId: string, teamId: string) => { try { - if (!incidentId) { - throw this.errorService.createBadRequestError("No incident ID in request"); + const incident = await this.incidentsRepository.findById(incidentId, teamId); + const monitor = await this.monitorsRepository.findById(incident.monitorId, teamId); + let user = null; + if (incident.resolvedBy) { + user = await this.usersRepository.findById(incident.resolvedBy); } - - if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); - } - - const incident = await this.db.incidentModule.getIncidentById(incidentId); - - if (!incident) { - throw this.errorService.createNotFoundError("Incident not found"); - } - - if (!incident.teamId.equals(teamId)) { - throw this.errorService.createAuthorizationError(); - } - - return incident; + return { incident, monitor, user }; } catch (error: any) { this.logger.error({ service: SERVICE_NAME, From b511cde849dfbc13ef3b7e3dce4f79dc98130d8a Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 22 Jan 2026 21:49:49 +0000 Subject: [PATCH 347/400] remove final js --- server/src/config/services.ts | 15 +-- server/src/db/{MongoDB.js => MongoDB.ts} | 24 +++-- server/src/db/utils/seedDb.js | 75 ------------- server/src/service/business/checkService.ts | 20 ++-- .../src/service/business/incidentService.ts | 8 +- server/src/service/business/inviteService.ts | 12 ++- .../business/maintenanceWindowService.ts | 12 ++- server/src/service/business/monitorService.ts | 16 +-- server/src/service/business/userService.ts | 1 - .../service/infrastructure/errorService.js | 101 ------------------ ...{imageProcessing.js => imageProcessing.ts} | 7 +- server/src/validation/{joi.js => joi.ts} | 12 +-- 12 files changed, 53 insertions(+), 250 deletions(-) rename server/src/db/{MongoDB.js => MongoDB.ts} (67%) delete mode 100755 server/src/db/utils/seedDb.js delete mode 100644 server/src/service/infrastructure/errorService.js rename server/src/utils/{imageProcessing.js => imageProcessing.ts} (74%) rename server/src/validation/{joi.js => joi.ts} (97%) diff --git a/server/src/config/services.ts b/server/src/config/services.ts index a6678dbe5..06701491d 100644 --- a/server/src/config/services.ts +++ b/server/src/config/services.ts @@ -13,7 +13,6 @@ import { MatrixProvider, INotificationsService, } from "@/service/index.js"; -import ErrorService from "../service/infrastructure/errorService.js"; import SuperSimpleQueueHelper from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import SuperSimpleQueue from "../service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; import UserService from "../service/business/userService.js"; @@ -98,7 +97,6 @@ export type InitializedServices = { maintenanceWindowService: any; monitorService: any; incidentService: any; - errorService: any; logger: any; notificationsService: INotificationsService; statusPageService: IStatusPageService; @@ -131,10 +129,7 @@ export const initializeServices = async ({ }): Promise => { // Create DB - const db = new MongoDB({ - logger, - envSettings, - }); + const db = new MongoDB(logger, envSettings); await db.connect(); @@ -165,18 +160,15 @@ export const initializeServices = async ({ settingsService, }); const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); - const errorService = new ErrorService(); const incidentService = new IncidentService({ logger, - errorService, incidentsRepository, monitorsRepository, usersRepository, }); const checkService = new CheckService({ - errorService, monitorsRepository, logger, checksRepository, @@ -229,7 +221,6 @@ export const initializeServices = async ({ settingsService, logger, jwt, - errorService, jobQueue: superSimpleQueue, monitorsRepository, usersRepository, @@ -244,10 +235,8 @@ export const initializeServices = async ({ invitesRepository, settingsService, emailService, - errorService, }); const maintenanceWindowService = new MaintenanceWindowService({ - errorService, monitorsRepository, maintenanceWindowsRepository, }); @@ -255,7 +244,6 @@ export const initializeServices = async ({ jobQueue: superSimpleQueue, emailService, logger, - errorService, games, monitorsRepository, checksRepository, @@ -281,7 +269,6 @@ export const initializeServices = async ({ maintenanceWindowService, monitorService, incidentService, - errorService, logger, notificationsService, statusPageService, diff --git a/server/src/db/MongoDB.js b/server/src/db/MongoDB.ts similarity index 67% rename from server/src/db/MongoDB.js rename to server/src/db/MongoDB.ts index 97498f13a..d62f4bbad 100755 --- a/server/src/db/MongoDB.js +++ b/server/src/db/MongoDB.ts @@ -1,10 +1,16 @@ import mongoose from "mongoose"; import AppSettings from "./models/AppSettings.js"; import { runMigrations } from "./migration/index.js"; +import { ILogger } from "@/utils/logger.js"; +import { EnvConfig } from "@/service/system/settingsService.js"; +const SERVICE_NAME = "MongoDB"; class MongoDB { - static SERVICE_NAME = "MongoDB"; + static SERVICE_NAME = SERVICE_NAME; - constructor({ logger, envSettings }) { + private logger: ILogger; + private envSettings: EnvConfig; + + constructor(logger: ILogger, envSettings: EnvConfig) { this.logger = logger; this.envSettings = envSettings; } @@ -35,15 +41,15 @@ class MongoDB { this.logger.info({ message: "Connected to MongoDB", - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "connect", }); await runMigrations(); - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "connect", stack: error.stack, }); @@ -53,14 +59,14 @@ class MongoDB { disconnect = async () => { try { - this.logger.info({ message: "Disconnecting from MongoDB" }); + this.logger.info({ message: "Disconnecting from MongoDB", service: SERVICE_NAME, method: "disconnect" }); await mongoose.disconnect(); - this.logger.info({ message: "Disconnected from MongoDB" }); + this.logger.info({ message: "Disconnected from MongoDB", service: SERVICE_NAME, method: "disconnect" }); return; - } catch (error) { + } catch (error: any) { this.logger.error({ message: error.message, - service: this.SERVICE_NAME, + service: SERVICE_NAME, method: "disconnect", stack: error.stack, }); diff --git a/server/src/db/utils/seedDb.js b/server/src/db/utils/seedDb.js deleted file mode 100755 index cdfe3675c..000000000 --- a/server/src/db/utils/seedDb.js +++ /dev/null @@ -1,75 +0,0 @@ -import Monitor from "../models/Monitor.js"; -import Check from "../models/Check.js"; -import { logger } from "../../../utils/logger.js"; - -const generateRandomUrl = () => { - const domains = ["example.com", "test.org", "demo.net", "sample.io", "mock.dev"]; - const paths = ["api", "status", "health", "ping", "check"]; - return `https://${domains[Math.floor(Math.random() * domains.length)]}/${paths[Math.floor(Math.random() * paths.length)]}`; -}; - -const generateChecks = (monitorId, teamId, count) => { - const checks = []; - const endTime = new Date(Date.now() - 10 * 60 * 1000); // 10 minutes ago - const startTime = new Date(endTime - count * 60 * 1000); // count minutes before endTime - - for (let i = 0; i < count; i++) { - const timestamp = new Date(startTime.getTime() + i * 60 * 1000); - const status = Math.random() > 0.05; // 95% chance of being up - - checks.push({ - monitorId, - teamId, - status, - responseTime: Math.floor(Math.random() * 1000), // Random response time between 0-1000ms - createdAt: timestamp, - updatedAt: timestamp, - }); - } - - return checks; -}; - -const seedDb = async (userId, teamId) => { - try { - logger.info({ - message: "Deleting all monitors and checks", - service: "seedDb", - method: "seedDb", - }); - await Monitor.deleteMany({}); - await Check.deleteMany({}); - logger.info({ - message: "Adding monitors", - service: "DB", - method: "seedDb", - }); - for (let i = 0; i < 300; i++) { - const monitor = await Monitor.create({ - name: `Monitor ${i}`, - url: generateRandomUrl(), - type: "http", - userId, - teamId, - interval: 60000, - active: false, - }); - logger.info({ - message: `Adding monitor and checks for monitor ${i}`, - service: "DB", - method: "seedDb", - }); - const checks = generateChecks(monitor._id, teamId, 10000); - await Check.insertMany(checks); - } - } catch (error) { - logger.error({ - message: "Error seeding DB", - service: "DB", - method: "seedDb", - stack: error.stack, - }); - } -}; - -export default seedDb; diff --git a/server/src/service/business/checkService.ts b/server/src/service/business/checkService.ts index 3a932ed3f..42788eba8 100644 --- a/server/src/service/business/checkService.ts +++ b/server/src/service/business/checkService.ts @@ -1,3 +1,4 @@ +import { requireTeamId } from "@/controllers/controllerUtils.js"; import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js"; import type { MonitorType, MonitorStatusResponse, CheckErrorInfo, Check } from "@/types/index.js"; import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; @@ -9,22 +10,18 @@ const SERVICE_NAME = "checkService"; class CheckService { static SERVICE_NAME = SERVICE_NAME; - private errorService: any; private monitorsRepository: IMonitorsRepository; private checksRepository: IChecksRepository; private logger: any; constructor({ - errorService, monitorsRepository, logger, checksRepository, }: { - errorService: any; monitorsRepository: IMonitorsRepository; logger: any; checksRepository: IChecksRepository; }) { - this.errorService = errorService; this.monitorsRepository = monitorsRepository; this.logger = logger; this.checksRepository = checksRepository; @@ -112,11 +109,10 @@ class CheckService { getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => { if (!monitorId) { - throw this.errorService.createBadRequestError("No monitor ID in request"); + throw new AppError({ message: "No monitor ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 }); } - if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); + throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 }); } // For verificaiton, throws an error if monitor doesn't belong to team @@ -136,7 +132,7 @@ class CheckService { let { sortOrder, dateRange, filter, page, rowsPerPage } = query; if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); + throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 }); } const parsedPage = page ? parseInt(page) : page; @@ -148,7 +144,7 @@ class CheckService { getChecksSummaryByTeamId = async ({ teamId }: { teamId: string }) => { if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); + throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksSummaryByTeamId", status: 400 }); } const summary = await this.checksRepository.findSummaryByTeamId(teamId); return summary; @@ -156,11 +152,11 @@ class CheckService { deleteChecks = async ({ monitorId, teamId }: { monitorId: string; teamId: string }) => { if (!monitorId) { - throw this.errorService.createBadRequestError("No monitor ID in request"); + throw new AppError({ message: "No monitor ID in request", service: SERVICE_NAME, method: "deleteChecks", status: 400 }); } if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); + throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteChecks", status: 400 }); } // For verificaiton, throws an error if monitor doesn't belong to team @@ -171,7 +167,7 @@ class CheckService { }; deleteChecksByTeamId = async ({ teamId }: { teamId: string }) => { if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); + throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteChecksByTeamId", status: 400 }); } const deletedCount = await this.checksRepository.deleteByTeamId(teamId); diff --git a/server/src/service/business/incidentService.ts b/server/src/service/business/incidentService.ts index 37a02e30d..d30492bcb 100644 --- a/server/src/service/business/incidentService.ts +++ b/server/src/service/business/incidentService.ts @@ -18,26 +18,22 @@ class IncidentService { static SERVICE_NAME = SERVICE_NAME; private logger: any; - private errorService: any; private incidentsRepository: IIncidentsRepository; private monitorsRepository: IMonitorsRepository; private usersRepository: IUsersRepository; constructor({ logger, - errorService, incidentsRepository, monitorsRepository, usersRepository, }: { logger: any; - errorService: any; incidentsRepository: IIncidentsRepository; monitorsRepository: IMonitorsRepository; usersRepository: IUsersRepository; }) { this.logger = logger; - this.errorService = errorService; this.incidentsRepository = incidentsRepository; this.monitorsRepository = monitorsRepository; this.usersRepository = usersRepository; @@ -139,7 +135,7 @@ class IncidentService { ) => { try { if (!teamId) { - throw this.errorService.createBadRequestError("No team ID in request"); + throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getIncidentsByTeam", status: 400 }); } const startDate = dateRangeLookup[dateRange]; @@ -177,7 +173,7 @@ class IncidentService { getIncidentSummary = async (teamId: string, limit?: string) => { try { if (!teamId) { - throw this.errorService.createBadRequestError(" team ID in request"); + throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getIncidentSummary", status: 400 }); } const parsedLimit = limit && Number.isFinite(parseInt(limit, 10)) ? parseInt(limit, 10) : 10; diff --git a/server/src/service/business/inviteService.ts b/server/src/service/business/inviteService.ts index cdb860fba..eee0f993c 100644 --- a/server/src/service/business/inviteService.ts +++ b/server/src/service/business/inviteService.ts @@ -1,5 +1,6 @@ import type { Invite } from "@/types/index.js"; import type { IInvitesRepository } from "@/repositories/index.js"; +import { AppError } from "@/utils/AppError.js"; const SERVICE_NAME = "inviteService"; @@ -8,24 +9,20 @@ class InviteService { private settingsService: any; private emailService: any; - private errorService: any; private invitesRepository: IInvitesRepository; constructor({ invitesRepository, settingsService, emailService, - errorService, }: { invitesRepository: IInvitesRepository; settingsService: any; emailService: any; - errorService: any; }) { this.invitesRepository = invitesRepository; this.settingsService = settingsService; this.emailService = emailService; - this.errorService = errorService; } get serviceName() { @@ -48,7 +45,12 @@ class InviteService { }); const result = await this.emailService.sendEmail(invite.email, "Welcome to Uptime Monitor", html); if (!result) { - throw this.errorService.createServerError("Failed to send invite e-mail... Please verify your settings."); + throw new AppError({ + message: "Failed to send invite e-mail... Please verify your settings.", + service: SERVICE_NAME, + method: "sendInviteEmail", + status: 500, + }); } }; diff --git a/server/src/service/business/maintenanceWindowService.ts b/server/src/service/business/maintenanceWindowService.ts index 561d97480..faf0ee943 100644 --- a/server/src/service/business/maintenanceWindowService.ts +++ b/server/src/service/business/maintenanceWindowService.ts @@ -1,24 +1,21 @@ import { IMaintenanceWindowsRepository, IMonitorsRepository } from "@/repositories/index.js"; import { ParseBoolean } from "@/utils/utils.js"; +import { AppError } from "@/utils/AppError.js"; const SERVICE_NAME = "maintenanceWindowService"; class MaintenanceWindowService { static SERVICE_NAME = SERVICE_NAME; - private errorService: any; private monitorsRepository: IMonitorsRepository; private maintenanceWindowsRepository: IMaintenanceWindowsRepository; constructor({ - errorService, monitorsRepository, maintenanceWindowsRepository, }: { - errorService: any; monitorsRepository: IMonitorsRepository; maintenanceWindowsRepository: IMaintenanceWindowsRepository; }) { - this.errorService = errorService; this.monitorsRepository = monitorsRepository; this.maintenanceWindowsRepository = maintenanceWindowsRepository; } @@ -34,7 +31,12 @@ class MaintenanceWindowService { const unauthorizedMonitors = monitors.filter((monitor) => monitor.teamId !== teamId); if (unauthorizedMonitors.length > 0) { - throw this.errorService.createAuthorizationError(); + throw new AppError({ + message: "Unauthorized to create maintenance window for one or more monitors", + service: SERVICE_NAME, + method: "createMaintenanceWindow", + status: 403, + }); } const dbTransactions = monitorIds.map((monitorId: string) => { diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 4f97bdf0f..513fc2a82 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -1,4 +1,3 @@ -import { createMonitorsBodyValidation } from "@/validation/joi.js"; import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js"; import { type Monitor } from "@/types/index.js"; import type { @@ -12,8 +11,7 @@ import type { IChecksRepository, IMonitorsRepository, IMonitorStatsRepository, I import fs from "fs"; import { fileURLToPath } from "url"; import path from "path"; - -import { AppError } from "../infrastructure/errorService.js"; +import { AppError } from "@/utils/AppError.js"; import { ISuperSimpleQueue } from "../infrastructure/SuperSimpleQueue/SuperSimpleQueue.js"; const SERVICE_NAME = "MonitorService"; @@ -75,7 +73,6 @@ export class MonitorService implements IMonitorService { private jobQueue: ISuperSimpleQueue; private emailService: any; private logger: any; - private errorService: any; private games: any; private monitorsRepository: IMonitorsRepository; private checksRepository: IChecksRepository; @@ -86,7 +83,6 @@ export class MonitorService implements IMonitorService { jobQueue, emailService, logger, - errorService, games, monitorsRepository, checksRepository, @@ -96,7 +92,6 @@ export class MonitorService implements IMonitorService { jobQueue: ISuperSimpleQueue; emailService: any; logger: any; - errorService: any; games: any; monitorsRepository: IMonitorsRepository; checksRepository: IChecksRepository; @@ -106,7 +101,6 @@ export class MonitorService implements IMonitorService { this.jobQueue = jobQueue; this.emailService = emailService; this.logger = logger; - this.errorService = errorService; this.games = games; this.monitorsRepository = monitorsRepository; this.checksRepository = checksRepository; @@ -146,7 +140,7 @@ export class MonitorService implements IMonitorService { createMonitor = async (teamId: string, userId: string, body: Monitor): Promise => { const monitor = await this.monitorsRepository.create(body, teamId, userId); if (!monitor) { - throw new AppError("Failed to create monitor", 500); + throw new AppError({ message: "Failed to create monitor", status: 500, service: SERVICE_NAME, method: "createMonitor" }); } this.jobQueue.addJob(monitor.id, monitor); @@ -155,7 +149,7 @@ export class MonitorService implements IMonitorService { createBulkMonitors = async (monitors: Array, userId: string, teamId: string): Promise => { const createdMonitors = await this.monitorsRepository.createBulkMonitors(monitors); if (!monitors || monitors.length === 0) { - throw new AppError("Failed to create monitors", 500); + throw new AppError({ message: "Failed to create monitors", status: 500, service: SERVICE_NAME, method: "createBulkMonitors" }); } await Promise.all(createdMonitors.map((monitor) => this.jobQueue.addJob(monitor.id, monitor))); @@ -445,7 +439,7 @@ export class MonitorService implements IMonitorService { const messageId = await this.emailService.sendEmail(to, subject, html); if (!messageId) { - throw this.errorService.createServerError("Failed to send test email."); + throw new AppError({ message: "Failed to send test email.", service: SERVICE_NAME, method: "sendTestEmail", status: 500 }); } return messageId; @@ -455,7 +449,7 @@ export class MonitorService implements IMonitorService { const monitors = await this.monitorsRepository.findByTeamId(teamId, {}); if (!monitors || monitors.length === 0) { - throw this.errorService.createNotFoundError("No monitors to export"); + throw new AppError({ message: "No monitors found to export.", service: SERVICE_NAME, method: "exportMonitorsToJSON", status: 400 }); } return monitors; diff --git a/server/src/service/business/userService.ts b/server/src/service/business/userService.ts index b9883f594..a0273af99 100644 --- a/server/src/service/business/userService.ts +++ b/server/src/service/business/userService.ts @@ -48,7 +48,6 @@ class UserService { settingsService: any; logger: any; jwt: any; - errorService: any; jobQueue: ISuperSimpleQueue; monitorsRepository: IMonitorsRepository; usersRepository: IUsersRepository; diff --git a/server/src/service/infrastructure/errorService.js b/server/src/service/infrastructure/errorService.js deleted file mode 100644 index 680bb3765..000000000 --- a/server/src/service/infrastructure/errorService.js +++ /dev/null @@ -1,101 +0,0 @@ -export class AppError extends Error { - constructor(message, status = 500, service = null, method = null, details = null) { - super(message); - this.status = status; - this.service = service; - this.method = method; - this.details = details; - - Error.captureStackTrace(this, this.constructor); - } -} - -class ValidationError extends AppError { - constructor(message, details = null, service = null, method = null) { - super(message, 422, service, method, details); - } -} - -class AuthenticationError extends AppError { - constructor(message, details = null, service = null, method = null) { - super(message, 401, service, method, details); - } -} - -class AuthorizationError extends AppError { - constructor(message, details = null, service = null, method = null) { - super(message, 403, service, method, details); - } -} - -class NotFoundError extends AppError { - constructor(message, details = null, service = null, method = null) { - super(message, 404, service, method, details); - } -} - -class ConflictError extends AppError { - constructor(message, details = null, service = null, method = null) { - super(message, 409, service, method, details); - } -} - -class DatabaseError extends AppError { - constructor(message, details = null, service = null, method = null) { - super(message, 500, service, method, details); - } -} - -class BadRequestError extends AppError { - constructor(message, details = null, service = null, method = null) { - super(message, 400, service, method, details); - } -} - -const SERVICE_NAME = "ErrorService"; -class ErrorService { - static SERVICE_NAME = SERVICE_NAME; - constructor() {} - - get serviceName() { - return ErrorService.SERVICE_NAME; - } - - createError = (message, status = 500, service = null, method = null, details = null) => { - return new AppError(message, status, service, method, details); - }; - - createValidationError = (message, details = null, service = null, method = null) => { - return new ValidationError(message, details, service, method); - }; - - createAuthenticationError = (message = "Unauthorized", details = null, service = null, method = null) => { - return new AuthenticationError(message, details, service, method); - }; - - createAuthorizationError = (message, details = null, service = null, method = null) => { - return new AuthorizationError(message, details, service, method); - }; - - createNotFoundError = (message, details = null, service = null, method = null) => { - return new NotFoundError(message, details, service, method); - }; - - createConflictError = (message, details = null, service = null, method = null) => { - return new ConflictError(message, details, service, method); - }; - - createDatabaseError = (message, details = null, service = null, method = null) => { - return new DatabaseError(message, details, service, method); - }; - - createServerError = (message, details = null, service = null, method = null) => { - return this.createError(message, 500, service, method, details); - }; - - createBadRequestError = (message = "BadRequest", details = null, service = null, method = null) => { - return new BadRequestError(message, details, service, method); - }; -} - -export default ErrorService; diff --git a/server/src/utils/imageProcessing.js b/server/src/utils/imageProcessing.ts similarity index 74% rename from server/src/utils/imageProcessing.js rename to server/src/utils/imageProcessing.ts index 470a5c27e..442f0d6da 100755 --- a/server/src/utils/imageProcessing.js +++ b/server/src/utils/imageProcessing.ts @@ -1,9 +1,6 @@ import sharp from "sharp"; -/** - * Generates a 64 * 64 pixel image from a given image - * @param {} file - */ -const GenerateAvatarImage = async (file) => { + +const GenerateAvatarImage = async (file: Express.Multer.File) => { try { // Resize to target 64 * 64 let resizedImageBuffer = await sharp(file.buffer) diff --git a/server/src/validation/joi.js b/server/src/validation/joi.ts similarity index 97% rename from server/src/validation/joi.js rename to server/src/validation/joi.ts index 474feebe5..8871c29e2 100755 --- a/server/src/validation/joi.js +++ b/server/src/validation/joi.ts @@ -1,14 +1,14 @@ -import joi from "joi"; -import { UserRoles } from "@/types/user.js"; +import joi, { type CustomHelpers } from "joi"; +import { type UserRole, UserRoles } from "@/types/user.js"; //**************************************** // Custom Validators //**************************************** -const roleValidatior = (role) => (value, helpers) => { - const hasRole = role.some((role) => value.includes(role)); +const roleValidatior = (role: UserRole[]) => (value: UserRole[], helpers: CustomHelpers) => { + const hasRole = role.some((role: UserRole) => value.includes(role)); if (!hasRole) { - throw new joi.ValidationError(`You do not have the required authorization. Required roles: ${role.join(", ")}`); + return helpers.error("any.invalid", { message: `You do not have the required authorization. Required roles: ${role.join(", ")}` }); } return value; }; @@ -45,7 +45,7 @@ const registrationBodyValidation = joi.object({ .custom((value, helpers) => { const lowercasedValue = value.toLowerCase(); if (value !== lowercasedValue) { - return helpers.message("Email must be in lowercase"); + return helpers.message({ custom: "Email must be in lowercase" }); } return lowercasedValue; }), From e0e79fb0da3c3f867ca34586a30a98a6388fd774 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Thu, 22 Jan 2026 17:05:06 -0500 Subject: [PATCH 348/400] fix: DatePicker calendar popup contrast in dark mode ## Changes - Add global theme overrides for MUI DatePicker components (MuiPickersPopper, MuiDateCalendar, MuiPickersCalendarHeader, MuiDayCalendar, MuiPickersDay) - Remove inline popper styling from CreateMaintenance component as it's now handled globally - Calendar popup now properly adapts to both light and dark themes using theme palette tokens --- .../Maintenance/CreateMaintenance/index.jsx | 24 --------- client/src/Utils/Theme/globalTheme.js | 52 +++++++++++++++++++ 2 files changed, 52 insertions(+), 24 deletions(-) diff --git a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx b/client/src/Pages/Maintenance/CreateMaintenance/index.jsx index 3e8090325..a300cc3b5 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx +++ b/client/src/Pages/Maintenance/CreateMaintenance/index.jsx @@ -261,30 +261,6 @@ const CreateMaintenance = () => { "&:hover": { backgroundColor: "transparent" }, }, }, - // CAIO_REVIEW, entire popper - popper: { - sx: { - "& .MuiPickersDay-root": { - color: theme.palette.primary.contrastText, - "&.Mui-selected": { - backgroundColor: theme.palette.accent.main, // Selected day background - color: theme.palette.primary.contrastText, // Selected day text color - }, - "&:hover": { - backgroundColor: theme.palette.accent.light, // Hover background - }, - "&.Mui-disabled": { - color: theme.palette.secondary.main, // Disabled day color - }, - }, - "& .MuiDayCalendar-weekDayLabel": { - color: theme.palette.primary.contrastText, - }, - "& .MuiPickersCalendarHeader-label": { - color: theme.palette.primary.contrastText, - }, - }, - }, }} sx={{}} onChange={(newDate) => { diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js index 58e1a9b63..37143d730 100644 --- a/client/src/Utils/Theme/globalTheme.js +++ b/client/src/Utils/Theme/globalTheme.js @@ -687,6 +687,58 @@ const baseTheme = (palette) => ({ }), }, }, + // DatePicker calendar popup styling + MuiPickersPopper: { + styleOverrides: { + paper: ({ theme }) => ({ + backgroundColor: theme.palette.primary.main, + }), + }, + }, + MuiDateCalendar: { + styleOverrides: { + root: ({ theme }) => ({ + backgroundColor: theme.palette.primary.main, + }), + }, + }, + MuiPickersCalendarHeader: { + styleOverrides: { + root: ({ theme }) => ({ + "& .MuiPickersCalendarHeader-label": { + color: theme.palette.primary.contrastText, + }, + }), + }, + }, + MuiDayCalendar: { + styleOverrides: { + weekDayLabel: ({ theme }) => ({ + color: theme.palette.primary.contrastTextTertiary, + }), + }, + }, + MuiPickersDay: { + styleOverrides: { + root: ({ theme }) => ({ + color: theme.palette.primary.contrastText, + "&:hover": { + backgroundColor: theme.palette.tertiary.main, + }, + "&.Mui-selected": { + backgroundColor: theme.palette.accent.main, + color: theme.palette.accent.contrastText, + "&:hover": { + backgroundColor: theme.palette.accent.main, + }, + }, + "&.Mui-disabled": { + color: theme.palette.primary.contrastTextTertiary, + opacity: 0.5, + }, + }), + }, + }, // code ends here. // For labels of input fields From de6c4d9c3ea7ae1ad2babcb1a5518a5454eddc45 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 22 Jan 2026 22:05:18 +0000 Subject: [PATCH 349/400] remove extra network request --- .../IncidentTable/IncidentActionsMenu.jsx | 18 +++++++++--------- .../Components/IncidentTable/index.jsx | 1 + 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/client/src/Pages/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx b/client/src/Pages/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx index 28b3a4f52..53b0adb11 100644 --- a/client/src/Pages/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx +++ b/client/src/Pages/Incidents/Components/IncidentTable/IncidentActionsMenu.jsx @@ -4,7 +4,6 @@ import Menu from "@mui/material/Menu"; import MenuItem from "@mui/material/MenuItem"; import Icon from "@/Components/v1/Icon/index.jsx"; import ResolveIncidentDialog from "../ResolveIncidentDialog/index.jsx"; -import useFetchIncidents from "../../hooks/useFetchIncidents"; // Utils import { useState } from "react"; @@ -14,12 +13,11 @@ import { TypeToPathMap } from "@/Utils/monitorUtils.js"; import { useNavigate } from "react-router-dom"; import PropTypes from "prop-types"; -const IncidentActionsMenu = ({ incident, onResolve, onOpenDetails }) => { +const IncidentActionsMenu = ({ incident, monitor, onResolve, onOpenDetails }) => { const [anchorEl, setAnchorEl] = useState(null); const [isDialogOpen, setIsDialogOpen] = useState(false); const theme = useTheme(); const { t } = useTranslation(); - const { fetchIncidentById } = useFetchIncidents(); const navigate = useNavigate(); const openMenu = (event) => { event.preventDefault(); @@ -59,12 +57,10 @@ const IncidentActionsMenu = ({ incident, onResolve, onOpenDetails }) => { e.stopPropagation(); closeMenu(e); try { - const incidentData = await fetchIncidentById(incident.id); - if (incidentData) { - const { type, _id: monitorId } = incidentData.monitorId; - const path = TypeToPathMap[type]; - if (path && monitorId) { - navigate(`/${path}/${monitorId}`); + if (monitor) { + const path = TypeToPathMap[monitor.type]; + if (path && monitor.id) { + navigate(`/${path}/${monitor.id}`); } else { console.error(`Monitor type not found`); } @@ -149,6 +145,10 @@ IncidentActionsMenu.propTypes = { id: PropTypes.string.isRequired, status: PropTypes.bool.isRequired, }).isRequired, + monitor: PropTypes.shape({ + id: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + }).isRequired, onResolve: PropTypes.func.isRequired, onOpenDetails: PropTypes.func, }; diff --git a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx index 7d23d8ebd..05242c1f3 100644 --- a/client/src/Pages/Incidents/Components/IncidentTable/index.jsx +++ b/client/src/Pages/Incidents/Components/IncidentTable/index.jsx @@ -129,6 +129,7 @@ const IncidentTable = ({ return ( monitor.id === row.monitorId)} onResolve={handleResolveIncident} onOpenDetails={onOpenDetails} /> From a11d551a998c3a62f729f164d8d02c724d7acd60 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 23 Jan 2026 17:38:30 +0000 Subject: [PATCH 350/400] add SWR hook and API client --- client/src/Hooks/UseApi.ts | 41 ++++++++++++++++++++++++ client/src/Utils/ApiClient.ts | 59 +++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 client/src/Hooks/UseApi.ts create mode 100644 client/src/Utils/ApiClient.ts diff --git a/client/src/Hooks/UseApi.ts b/client/src/Hooks/UseApi.ts new file mode 100644 index 000000000..58580e4f3 --- /dev/null +++ b/client/src/Hooks/UseApi.ts @@ -0,0 +1,41 @@ +import useSWR from "swr"; +import type { SWRConfiguration } from "swr"; +import type { AxiosRequestConfig } from "axios"; +import { get, post, patch, deleteOp } from "@/Utils/ApiClient"; + +interface ApiResponse { + success: boolean; + msg: string; + data: T; +} + +interface UseGetReturn { + data: T | undefined; + isLoading: boolean; + error: Error | undefined; + refetch: () => Promise | undefined>; +} + +const fetcher = async (url: string, config?: AxiosRequestConfig) => { + const res = await get(url, config); + return res.data; +}; + +export const useGet = ( + url: string | null, + axiosConfig?: AxiosRequestConfig, + swrConfig?: SWRConfiguration +): UseGetReturn => { + const { data, error, isLoading, mutate } = useSWR>( + url, + (url: string) => fetcher(url, axiosConfig), + swrConfig + ); + + return { + data: data?.data, + isLoading, + error, + refetch: mutate, + }; +}; diff --git a/client/src/Utils/ApiClient.ts b/client/src/Utils/ApiClient.ts new file mode 100644 index 000000000..e375b07e7 --- /dev/null +++ b/client/src/Utils/ApiClient.ts @@ -0,0 +1,59 @@ +import axios from "axios"; +import type { AxiosError } from "axios"; +import type { AxiosRequestConfig, AxiosResponse } from "axios"; +import { store } from "@/store"; +const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL; + +const api = axios.create({ + baseURL: BASE_URL, +}); + +api.interceptors.request.use( + (config) => { + const authToken = store.getState().auth?.authToken ?? ""; + + config.headers.set("Authorization", `Bearer ${authToken}`); + if (!config.headers.has("Accept-Language")) { + config.headers.set("Accept-Language", "en"); + } + + return config; + }, + (error) => { + return Promise.reject(error); + } +); + +const onSuccess = (response: AxiosResponse) => response; +const onError = (error: AxiosError) => { + if (error.response?.status === 401) { + window.location.href = "/login"; + } + return Promise.reject(error); +}; + +api.interceptors.response.use(onSuccess, onError); + +export const get = ( + url: string, + config: AxiosRequestConfig = {} +): Promise> => api.get(url, config); + +export const post = ( + url: string, + data: unknown, + config: AxiosRequestConfig = {} +): Promise> => api.post(url, data, config); + +export const patch = ( + url: string, + data: unknown, + config: AxiosRequestConfig = {} +): Promise> => api.patch(url, data, config); + +export const deleteOp = ( + url: string, + config: AxiosRequestConfig = {} +): Promise> => api.delete(url, config); + +export default api; From 00cf0b65217d1da559486d6ee06ebaf48d41eca9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 23 Jan 2026 18:14:16 +0000 Subject: [PATCH 351/400] fix get return type --- client/package.json | 2 +- client/src/Hooks/UseApi.ts | 15 ++++----------- client/src/Types/Monitor.ts | 2 +- 3 files changed, 6 insertions(+), 13 deletions(-) diff --git a/client/package.json b/client/package.json index 50fafb35c..7c5acb441 100644 --- a/client/package.json +++ b/client/package.json @@ -43,7 +43,7 @@ "react-toastify": "^10.0.5", "recharts": "2.15.2", "redux-persist": "6.0.0", - "swr": "2.3.6", + "swr": "^2.3.6", "vite-plugin-svgr": "^4.2.0", "zod": "4.1.11" }, diff --git a/client/src/Hooks/UseApi.ts b/client/src/Hooks/UseApi.ts index 58580e4f3..06845d6c1 100644 --- a/client/src/Hooks/UseApi.ts +++ b/client/src/Hooks/UseApi.ts @@ -1,7 +1,7 @@ import useSWR from "swr"; import type { SWRConfiguration } from "swr"; import type { AxiosRequestConfig } from "axios"; -import { get, post, patch, deleteOp } from "@/Utils/ApiClient"; +import { get } from "@/Utils/ApiClient"; interface ApiResponse { success: boolean; @@ -9,15 +9,8 @@ interface ApiResponse { data: T; } -interface UseGetReturn { - data: T | undefined; - isLoading: boolean; - error: Error | undefined; - refetch: () => Promise | undefined>; -} - const fetcher = async (url: string, config?: AxiosRequestConfig) => { - const res = await get(url, config); + const res = await get>(url, config); return res.data; }; @@ -25,7 +18,7 @@ export const useGet = ( url: string | null, axiosConfig?: AxiosRequestConfig, swrConfig?: SWRConfiguration -): UseGetReturn => { +) => { const { data, error, isLoading, mutate } = useSWR>( url, (url: string) => fetcher(url, axiosConfig), @@ -33,7 +26,7 @@ export const useGet = ( ); return { - data: data?.data, + data: data?.data ?? null, isLoading, error, refetch: mutate, diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index 05f7d7d6f..e01c9f844 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -1,7 +1,7 @@ import type { Check } from "@/Types/Check"; export type MonitorStatus = "up" | "down" | "initializing"; -export interface IMonitor { +export interface Monitor { checks: Check[]; createdAt: string; createdBy: string; From 363d89e4f4a3acad8336a2347ea082c887c71fc6 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 23 Jan 2026 19:58:37 +0000 Subject: [PATCH 352/400] initial commit --- .../v2/design-elements/BasePage.tsx | 159 ++++++++++++++++ .../v2/design-elements/Breadcrumb.tsx | 117 ++++++++++++ .../v2/design-elements/BulletPointCheck.tsx | 46 +++++ .../v2/design-elements/Fallback.tsx | 171 ++++++++++++++++++ .../v2/design-elements/SkeletonCard.tsx | 147 +++++++++++++++ .../Components/v2/design-elements/index.tsx | 5 + client/src/Components/v2/inputs/Button.tsx | 37 ++++ client/src/Components/v2/inputs/index.tsx | 1 + client/src/Pages/Uptime/Monitors/index.tsx | 17 ++ .../Uptime/Monitors/{index.jsx => old.jsx} | 0 client/src/Routes/index.jsx | 14 +- client/src/Utils/Theme/v2Palette.ts | 34 ++++ client/src/Utils/Theme/v2Theme.ts | 148 +++++++++++++++ client/src/locales/en.json | 7 + 14 files changed, 901 insertions(+), 2 deletions(-) create mode 100644 client/src/Components/v2/design-elements/BasePage.tsx create mode 100644 client/src/Components/v2/design-elements/Breadcrumb.tsx create mode 100644 client/src/Components/v2/design-elements/BulletPointCheck.tsx create mode 100644 client/src/Components/v2/design-elements/Fallback.tsx create mode 100644 client/src/Components/v2/design-elements/SkeletonCard.tsx create mode 100644 client/src/Components/v2/design-elements/index.tsx create mode 100644 client/src/Components/v2/inputs/Button.tsx create mode 100644 client/src/Components/v2/inputs/index.tsx create mode 100644 client/src/Pages/Uptime/Monitors/index.tsx rename client/src/Pages/Uptime/Monitors/{index.jsx => old.jsx} (100%) create mode 100644 client/src/Utils/Theme/v2Palette.ts create mode 100644 client/src/Utils/Theme/v2Theme.ts diff --git a/client/src/Components/v2/design-elements/BasePage.tsx b/client/src/Components/v2/design-elements/BasePage.tsx new file mode 100644 index 000000000..e3785936b --- /dev/null +++ b/client/src/Components/v2/design-elements/BasePage.tsx @@ -0,0 +1,159 @@ +import Stack from "@mui/material/Stack"; +import { + ErrorFallback, + EmptyFallback, + EmptyMonitorFallback, +} from "@/Components/v2/design-elements/Fallback"; +import { Breadcrumb } from "@/Components/v2/design-elements/Breadcrumb"; +import CircularProgress from "@mui/material/CircularProgress"; + +import type { StackProps } from "@mui/material/Stack"; +import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; +interface BasePageProps extends StackProps { + loading?: boolean; + error?: boolean; + children: React.ReactNode; + breadcrumbOverride?: string[]; +} + +export const BasePage = ({ + loading, + error, + children, + breadcrumbOverride, + ...props +}: BasePageProps) => { + const theme = useTheme(); + + if (loading) { + return ( + + + + ); + } + + if (error) { + return ( + + ); + } + + return ( + + + {children} + + ); +}; + +interface BasePageWithStatesProps extends StackProps { + loading: boolean; + error: any; + items: any[]; + bullets: string[] | unknown; + page: string; + actionButtonText: string; + actionLink: string; + children: React.ReactNode; +} + +export const BasePageWithStates = ({ + loading, + error, + items, + page, + bullets, + actionButtonText, + actionLink, + children, + ...props +}: BasePageWithStatesProps) => { + const showLoading = loading && (!items || items.length === 0); + + if (isEmpty(items)) { + return ( + + ); + } + + return ( + + {children} + + ); +}; + +interface MonitorBasePageWithStatesProps extends StackProps { + loading: boolean; + error: any; + items: any[]; + page: string; + actionLink?: string; + children: React.ReactNode; +} + +const isEmpty = (items: any[]) => { + if (!items) return true; + if (Array.isArray(items) && items.length === 0) return true; + return false; +}; + +export const MonitorBasePageWithStates = ({ + loading, + error, + items, + page, + actionLink, + children, + ...props +}: MonitorBasePageWithStatesProps) => { + const { t } = useTranslation(); + + const showLoading = loading && (!items || items.length === 0); + + if (!loading && isEmpty(items)) { + return ( + + ); + } + + return ( + + {children} + + ); +}; diff --git a/client/src/Components/v2/design-elements/Breadcrumb.tsx b/client/src/Components/v2/design-elements/Breadcrumb.tsx new file mode 100644 index 000000000..4489f992d --- /dev/null +++ b/client/src/Components/v2/design-elements/Breadcrumb.tsx @@ -0,0 +1,117 @@ +import MuiBreadcrumbs from "@mui/material/Breadcrumbs"; +import Typography from "@mui/material/Typography"; +import { Link, useLocation } from "react-router-dom"; +import { useTheme } from "@mui/material/styles"; +import { ChevronRight, Home } from "lucide-react"; +import { useTranslation } from "react-i18next"; + +export const Breadcrumb = ({ breadcrumbOverride }: { breadcrumbOverride?: string[] }) => { + const { t } = useTranslation(); + const theme = useTheme(); + const location = useLocation(); + + // Don't show breadcrumb on dashboard + if (location.pathname === "/" || location.pathname === "/uptime") { + return null; + } + + const pathnames = breadcrumbOverride || location.pathname.split("/").filter((x) => x); + + // Don't show if only one level (e.g., /uptime) + if (pathnames.length === 0) { + return null; + } + + return ( + + } + sx={{ + fontSize: "14px", + marginBottom: theme.spacing(6), + "& .MuiBreadcrumbs-separator": { + color: theme.palette.text.secondary, + }, + }} + > + + + + Home + + + {pathnames.map((value, index) => { + const last = index === pathnames.length - 1; + const to = `/${pathnames.slice(0, index + 1).join("/")}`; + + let displayName = ""; + + // Paths that are IDs should be detail pages + if (value.length === 24 || value.match(/^[a-f0-9-]{36}$/)) { + displayName = "Details"; + } else { + displayName = + t(`common.breadcrumbs.${value}`) || + value.charAt(0).toUpperCase() + value.slice(1); + } + + return last ? ( + + {displayName} + + ) : ( + + + {displayName} + + + ); + })} + + ); +}; diff --git a/client/src/Components/v2/design-elements/BulletPointCheck.tsx b/client/src/Components/v2/design-elements/BulletPointCheck.tsx new file mode 100644 index 000000000..2893ee4c2 --- /dev/null +++ b/client/src/Components/v2/design-elements/BulletPointCheck.tsx @@ -0,0 +1,46 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { Check } from "lucide-react"; +import { useTheme } from "@mui/material/styles"; + +export const BulletPointCheck = ({ + text, + variant = "info", +}: { + text: string; + noHighlightText?: string; + variant?: "success" | "error" | "info"; +}) => { + const theme = useTheme(); + const colors: Record = { + success: theme.palette.success.main, + error: theme.palette.error.main, + info: theme.palette.text.secondary, + }; + + return ( + + + + {text} + + + ); +}; diff --git a/client/src/Components/v2/design-elements/Fallback.tsx b/client/src/Components/v2/design-elements/Fallback.tsx new file mode 100644 index 000000000..9dfeb5122 --- /dev/null +++ b/client/src/Components/v2/design-elements/Fallback.tsx @@ -0,0 +1,171 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { BulletPointCheck, SkeletonCard } from "@/Components/v2/design-elements"; +import { Button } from "@/Components/v2/inputs"; + +import { useNavigate } from "react-router"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useTheme } from "@mui/material/styles"; +import type { StackProps } from "@mui/material/Stack"; + +interface BaseFallbackProps extends StackProps { + children: React.ReactNode; +} + +export const BaseFallback = ({ children, ...props }: BaseFallbackProps) => { + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + + return ( + + + {children} + + ); +}; + +export const ErrorFallback = ({ + title, + subtitle, +}: { + title: string; + subtitle: string; +}) => { + const theme = useTheme(); + return ( + + + {title} + + {subtitle} + + ); +}; + +export const EmptyFallback = ({ + title, + bullets, + actionButtonText, + actionLink, +}: { + title: string; + bullets: any; + actionButtonText: string; + actionLink: string; +}) => { + const theme = useTheme(); + const navigate = useNavigate(); + return ( + + + + {title} + + + {Array.isArray(bullets) && + bullets?.map((bullet: string) => ( + + ))} + + + + + + + ); +}; + +export const EmptyMonitorFallback = ({ + page, + title, + bullets, + actionButtonText, + actionLink, +}: { + page: string; + title: string; + bullets: any; + actionButtonText: string; + actionLink: string; +}) => { + const theme = useTheme(); + const navigate = useNavigate(); + return ( + + + + {title} + + + {Array.isArray(bullets) && + bullets?.map((bullet: string, index: number) => ( + + ))} + + + + ); +}; diff --git a/client/src/Components/v2/design-elements/SkeletonCard.tsx b/client/src/Components/v2/design-elements/SkeletonCard.tsx new file mode 100644 index 000000000..05f0c462a --- /dev/null +++ b/client/src/Components/v2/design-elements/SkeletonCard.tsx @@ -0,0 +1,147 @@ +import { Box, Stack, useTheme } from "@mui/material"; + +interface SkeletonCardProps { + width?: number | string | { xs?: number | string; md?: number | string }; + showHalo?: boolean; +} + +export const SkeletonCard = ({ + width = { xs: 130, md: 216 }, + showHalo = true, +}: SkeletonCardProps) => { + const theme = useTheme(); + const pulseAnimation = { + "@keyframes pulse": { + "0%, 100%": { opacity: 1 }, + "50%": { opacity: 0.72 }, + }, + }; + + const floatAnimation = { + "@keyframes float": { + "0%, 100%": { transform: "rotate(5deg) translate(12px, -20px)" }, + "50%": { transform: "rotate(5deg) translate(12px, -24px)" }, + }, + }; + + const cardStyle = { + width: width, + height: 57, + borderRadius: theme.shape.borderRadius, + background: + theme.palette.mode === "dark" + ? theme.palette.secondary.dark + : theme.palette.background.paper, + boxShadow: + theme.palette.mode === "dark" + ? "0 8px 30px rgba(0,0,0,.3), 0 0 0 1px rgba(255,255,255,.05) inset" + : "0 8px 30px rgba(0,0,0,.08), 0 0 0 1px rgba(0,0,0,.05) inset", + display: "flex", + gap: theme.spacing(5), + alignItems: "center", + padding: theme.spacing(5), + }; + + const blockStyle = { + width: 55, + height: 34, + borderRadius: theme.shape.borderRadius, + background: theme.palette.secondary.main, + border: `1px solid ${theme.palette.divider}`, + animation: "pulse 1.6s ease-in-out infinite", + ...pulseAnimation, + }; + + const lineStyle = { + height: 7, + borderRadius: theme.shape.borderRadius, + background: theme.palette.secondary.main, + border: `1px solid ${theme.palette.divider}`, + animation: "pulse 1.6s ease-in-out infinite", + ...pulseAnimation, + }; + + return ( + + {showHalo && ( + ); diff --git a/client/src/Components/v2/inputs/Checkbox.tsx b/client/src/Components/v2/inputs/Checkbox.tsx new file mode 100644 index 000000000..195a796dd --- /dev/null +++ b/client/src/Components/v2/inputs/Checkbox.tsx @@ -0,0 +1,56 @@ +import { forwardRef } from "react"; +import Checkbox from "@mui/material/Checkbox"; +import type { CheckboxProps } from "@mui/material/Checkbox"; +import { Square, SquareCheck } from "lucide-react"; +import { useTheme } from "@mui/material/styles"; + +type CheckboxInputProps = CheckboxProps & { + label?: string; +}; +export const CheckboxInput = forwardRef( + function CheckboxInput( + { sx, ...props }: CheckboxInputProps, + ref: React.Ref + ) { + const theme = useTheme(); + return ( + + } + checkedIcon={ + + } + sx={{ + color: theme.palette.text.secondary, + "&.Mui-checked": { + color: theme.palette.primary.main, + }, + "&:hover": { backgroundColor: "transparent" }, + "& svg": { + stroke: "currentColor", + }, + "& svg path, & svg line, & svg polyline, & svg rect": { + stroke: "currentColor", + fill: "none", + }, + + ...sx, + }} + /> + ); + } +); diff --git a/client/src/Components/v2/inputs/FieldLabel.tsx b/client/src/Components/v2/inputs/FieldLabel.tsx new file mode 100644 index 000000000..552991538 --- /dev/null +++ b/client/src/Components/v2/inputs/FieldLabel.tsx @@ -0,0 +1,38 @@ +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; + +interface FieldLabelProps { + children: React.ReactNode; + required?: boolean; + htmlFor?: string; +} + +export const FieldLabel = ({ children, required = false, htmlFor }: FieldLabelProps) => { + const theme = useTheme(); + + return ( + + {children} + {required && ( + + * + + )} + + ); +}; diff --git a/client/src/Components/v2/inputs/Select.tsx b/client/src/Components/v2/inputs/Select.tsx new file mode 100644 index 000000000..057b0e72c --- /dev/null +++ b/client/src/Components/v2/inputs/Select.tsx @@ -0,0 +1,102 @@ +import Select from "@mui/material/Select"; +import React from "react"; +import { useTheme } from "@mui/material/styles"; +import type { SelectProps } from "@mui/material/Select"; +import Stack from "@mui/material/Stack"; +import { FieldLabel } from "./FieldLabel"; +import { ChevronDown } from "lucide-react"; +import Typography from "@mui/material/Typography"; + +interface SelectInputProps extends Omit, "label"> { + fieldLabel?: string; + required?: boolean; + placeholder?: string; + placeholderColor?: string; +} + +export const SelectInput = ({ + fieldLabel, + required, + placeholder, + placeholderColor, + ...props +}: SelectInputProps) => { + const theme = useTheme(); + const emptyPlaceholderColor = placeholderColor || theme.palette.text.disabled; + + const renderValue = (selected: any) => { + const isMultiple = Boolean((props as any).multiple); + const isEmpty = isMultiple + ? !Array.isArray(selected) || selected.length === 0 + : selected === undefined || selected === null || selected === ""; + + if (isEmpty && placeholder) { + return {placeholder}; + } + + if (isMultiple) { + const items: string[] = Array.isArray(selected) ? selected : []; + const capitalized = items.map( + (item) => item.charAt(0).toUpperCase() + item.slice(1) + ); + return ({capitalized.join(" | ")}) as any; + } + + const nodes = React.Children.toArray(props.children as React.ReactNode); + for (const node of nodes) { + if (!React.isValidElement(node)) continue; + const el = node as React.ReactElement<{ + value?: unknown; + children?: React.ReactNode; + }>; + if (el.props?.value === selected) { + return (el.props.children ?? selected) as React.ReactNode; + } + } + return selected as React.ReactNode; + }; + + const select = ( + + {...props} + displayEmpty + renderValue={renderValue} + inputProps={{ + ...(props.inputProps || {}), + "aria-placeholder": placeholder, + }} + IconComponent={() => ( + + )} + sx={{ + height: "34px", + "& .MuiSelect-icon": { + right: theme.spacing(3), + }, + "& .MuiOutlinedInput-notchedOutline": { + borderRadius: theme.shape.borderRadius, + borderColor: theme.palette.divider, + }, + "&:hover .MuiOutlinedInput-notchedOutline": { + borderColor: theme.palette.divider, + }, + ...props.sx, + }} + /> + ); + + if (fieldLabel) { + return ( + + {fieldLabel} + {select} + + ); + } + + return select; +}; diff --git a/client/src/Components/v2/inputs/index.tsx b/client/src/Components/v2/inputs/index.tsx index 9b582f3d7..3cd2516b1 100644 --- a/client/src/Components/v2/inputs/index.tsx +++ b/client/src/Components/v2/inputs/index.tsx @@ -1 +1,4 @@ export { ButtonInput as Button } from "./Button"; +export * from "./FieldLabel"; +export { SelectInput as Select } from "./Select"; +export { CheckboxInput as Checkbox } from "./Checkbox"; diff --git a/client/src/Components/v2/monitors/FilterControls.tsx b/client/src/Components/v2/monitors/FilterControls.tsx new file mode 100644 index 000000000..09776f71e --- /dev/null +++ b/client/src/Components/v2/monitors/FilterControls.tsx @@ -0,0 +1,11 @@ +import Stack from "@mui/material/Stack"; +import { Select } from "@/Components/v2/inputs"; +export const FilterControls = () => { + return ( + + + + + ); +}; diff --git a/client/src/Components/v2/monitors/index.tsx b/client/src/Components/v2/monitors/index.tsx new file mode 100644 index 000000000..aa1464399 --- /dev/null +++ b/client/src/Components/v2/monitors/index.tsx @@ -0,0 +1 @@ +export * from "./FilterControls"; diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index 9883c1470..5dc1100bf 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -1,15 +1,25 @@ +import { useGet } from "@/Hooks/UseApi"; import { MonitorBasePageWithStates } from "@/Components/v2/design-elements"; +import { type Monitor } from "@/Types/Monitor"; +import { FilterControls } from "@/Components/v2/monitors"; const UptimeMonitorsPage = () => { + const { + data: monitors, + isLoading, + error, + } = useGet("/monitors/team?type=http&type=ping&type=port&type=docker"); + return ( - Test + +
{JSON.stringify(monitors, null, 2)}
); }; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 1b3792e9e..d7c0d1cda 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -128,7 +128,14 @@ "pages": { "uptime": { "fallback": { - "title": "An uptime monitor is used to:" + "title": "An uptime monitor is used to:", + "actionButton": "Create a monitor!", + "checks": [ + "Check if websites or servers are online & responsive", + "Alert teams about downtime or performance issues", + "Monitor HTTP endpoints, pings, containers & ports", + "Track historical uptime and reliability trends" + ] } } }, From c2dece862c4744632727583b9a9f726bcaecb59b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 23 Jan 2026 21:30:34 +0000 Subject: [PATCH 354/400] controls --- .../Components/v2/monitors/FilterControls.tsx | 93 +++++++++++++++++-- client/src/Pages/Uptime/Monitors/index.tsx | 55 ++++++++++- client/src/Types/Monitor.ts | 30 ++++++ client/src/Types/state.ts | 40 ++++++++ 4 files changed, 208 insertions(+), 10 deletions(-) create mode 100644 client/src/Types/state.ts diff --git a/client/src/Components/v2/monitors/FilterControls.tsx b/client/src/Components/v2/monitors/FilterControls.tsx index 09776f71e..d5ec8cc55 100644 --- a/client/src/Components/v2/monitors/FilterControls.tsx +++ b/client/src/Components/v2/monitors/FilterControls.tsx @@ -1,11 +1,92 @@ import Stack from "@mui/material/Stack"; -import { Select } from "@/Components/v2/inputs"; -export const FilterControls = () => { +import { Select, Button } from "@/Components/v2/inputs"; +import MenuItem from "@mui/material/MenuItem"; + +import type { MonitorType } from "@/Types/Monitor"; +import { Typography, useTheme } from "@mui/material"; + +const types = ["http", "ping", "port", "docker"]; +const statuses = ["up", "down"]; +const states = ["active", "paused"]; + +export const FilterControls = ({ + selectedTypes, + setSelectedTypes, + selectedStatus, + setSelectedStatus, + selectedState, + setSelectedState, +}: { + selectedTypes: MonitorType[]; + setSelectedTypes: React.Dispatch>; + selectedStatus: string; + setSelectedStatus: React.Dispatch>; + selectedState: string; + setSelectedState: React.Dispatch>; +}) => { + const theme = useTheme(); + const isFilterActive = + selectedTypes.length > 0 || selectedStatus !== "" || selectedState !== ""; return ( - - - + + + + + {isFilterActive && ( + + )} ); }; diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index 5dc1100bf..2d7a851d2 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -1,24 +1,71 @@ import { useGet } from "@/Hooks/UseApi"; import { MonitorBasePageWithStates } from "@/Components/v2/design-elements"; -import { type Monitor } from "@/Types/Monitor"; +import type { Monitor, MonitorType, MonitorsWithChecksResponse } from "@/Types/Monitor"; import { FilterControls } from "@/Components/v2/monitors"; +import { useState, useMemo } from "react"; +import { useSelector } from "react-redux"; +import type { RootState } from "@/Types/state"; const UptimeMonitorsPage = () => { + // Redux state + const rowsPerPage = useSelector((state: RootState) => state.ui.monitors.rowsPerPage); + + // Local state + const [selectedTypes, setSelectedTypes] = useState([]); + const [selectedStatus, setSelectedStatus] = useState(""); + const [selectedState, setSelectedState] = useState(""); + const [page, setPage] = useState(0); + const [search, setSearch] = useState(""); + const [sort, setSort] = useState< + { field: string; order: "asc" | "desc" } | undefined + >(); + + // Build URL for monitors with checks + const monitorsWithChecksUrl = useMemo(() => { + const params = new URLSearchParams(); + const types = + selectedTypes.length > 0 ? selectedTypes : ["http", "ping", "docker", "port"]; + types.forEach((type) => params.append("type", type)); + params.append("limit", "25"); + if (page) params.append("page", String(page)); + if (rowsPerPage) params.append("rowsPerPage", String(rowsPerPage)); + if (search) params.append("filter", search); + if (sort?.field) params.append("field", sort.field); + if (sort?.order) params.append("order", sort.order); + return `/monitors/team/with-checks?${params.toString()}`; + }, [selectedTypes, page, rowsPerPage, search, sort]); + + // Data fetching const { data: monitors, isLoading, error, } = useGet("/monitors/team?type=http&type=ping&type=port&type=docker"); + const { + data: monitorsWithChecksData, + isLoading: monitorsWithChecksLoading, + error: monitorsWithChecksError, + } = useGet(monitorsWithChecksUrl); + + const { monitors: monitorsWithChecks, summary, count } = monitorsWithChecksData ?? {}; + return ( - +
{JSON.stringify(monitors, null, 2)}
); diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index e01c9f844..8eff63518 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -1,6 +1,18 @@ import type { Check } from "@/Types/Check"; export type MonitorStatus = "up" | "down" | "initializing"; +export const MonitorTypes = [ + "http", + "ping", + "pagespeed", + "hardware", + "docker", + "port", + "game", + "unknown", +] as const; +export type MonitorType = (typeof MonitorTypes)[number]; + export interface Monitor { checks: Check[]; createdAt: string; @@ -18,3 +30,21 @@ export interface Monitor { __v: number; _id: string; } + +export interface MonitorWithChecks extends Monitor { + checks: Check[]; + uptimePercentage?: number; +} + +export interface MonitorsSummary { + totalMonitors: number; + totalUp: number; + totalDown: number; + totalPaused: number; +} + +export interface MonitorsWithChecksResponse { + count: number; + monitors: MonitorWithChecks[]; + summary: MonitorsSummary; +} diff --git a/client/src/Types/state.ts b/client/src/Types/state.ts new file mode 100644 index 000000000..fc25798c4 --- /dev/null +++ b/client/src/Types/state.ts @@ -0,0 +1,40 @@ +export interface AuthState { + isLoading: boolean; + authToken: string; + user: string; + success: boolean | null; + msg: string | null; +} + +export interface UIState { + monitors: { + rowsPerPage: number; + }; + team: { + rowsPerPage: number; + }; + maintenance: { + rowsPerPage: number; + }; + infrastructure: { + rowsPerPage: number; + }; + sidebar: { + collapsed: boolean; + }; + mode: "light" | "dark"; + showURL: boolean; + greeting: { + index: number; + lastUpdate: string | null; + }; + timezone: string; + distributedUptimeEnabled: boolean; + language: string; + starPromptOpen: boolean; +} + +export interface RootState { + auth: AuthState; + ui: UIState; +} From a0a8ceb0db06dcdb47edceb8027fdcea50f88ef3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 23 Jan 2026 23:54:58 +0000 Subject: [PATCH 355/400] uptime --- .../src/Components/v2/actions-menu/index.tsx | 56 ++ .../v2/common/HeatmapResponseTime.tsx | 140 +++++ .../v2/common/HeatmapResponseTimeTooltip.tsx | 71 +++ .../v2/common/HistogramResponseTime.tsx | 102 ++++ client/src/Components/v2/common/index.ts | 3 + .../Components/v2/design-elements/BaseBox.tsx | 19 + .../v2/design-elements/StatusBox.tsx | 112 ++++ .../v2/design-elements/StatusLabel.tsx | 101 +++ .../Components/v2/design-elements/Table.tsx | 576 ++++++++++++++++++ .../Components/v2/design-elements/index.tsx | 5 + .../src/Components/v2/inputs/AutoComplete.tsx | 88 +++ client/src/Components/v2/inputs/TextInput.tsx | 56 ++ client/src/Components/v2/inputs/index.tsx | 2 + .../Components/v2/monitors/FilterControls.tsx | 8 +- client/src/Features/UI/uiSlice.js | 5 + client/src/Hooks/UseApi.ts | 77 ++- client/src/Hooks/UseToast.ts | 23 + .../Components/UptimeMonitorsTable.tsx | 276 +++++++++ client/src/Pages/Uptime/Monitors/index.tsx | 152 ++++- client/src/Types/Check.ts | 148 ++++- client/src/Types/Monitor.ts | 11 +- client/src/Types/mui.d.ts | 22 +- client/src/Types/state.ts | 1 + client/src/Utils/DataUtils.ts | 129 ++++ client/src/Utils/MonitorUtils.ts | 120 ++++ client/src/Utils/TimeUtils.ts | 52 ++ client/src/locales/en.json | 65 +- 27 files changed, 2326 insertions(+), 94 deletions(-) create mode 100644 client/src/Components/v2/actions-menu/index.tsx create mode 100644 client/src/Components/v2/common/HeatmapResponseTime.tsx create mode 100644 client/src/Components/v2/common/HeatmapResponseTimeTooltip.tsx create mode 100644 client/src/Components/v2/common/HistogramResponseTime.tsx create mode 100644 client/src/Components/v2/common/index.ts create mode 100644 client/src/Components/v2/design-elements/BaseBox.tsx create mode 100644 client/src/Components/v2/design-elements/StatusBox.tsx create mode 100644 client/src/Components/v2/design-elements/StatusLabel.tsx create mode 100644 client/src/Components/v2/design-elements/Table.tsx create mode 100644 client/src/Components/v2/inputs/AutoComplete.tsx create mode 100644 client/src/Components/v2/inputs/TextInput.tsx create mode 100644 client/src/Hooks/UseToast.ts create mode 100644 client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx create mode 100644 client/src/Utils/DataUtils.ts create mode 100644 client/src/Utils/MonitorUtils.ts create mode 100644 client/src/Utils/TimeUtils.ts diff --git a/client/src/Components/v2/actions-menu/index.tsx b/client/src/Components/v2/actions-menu/index.tsx new file mode 100644 index 000000000..3b2f3688a --- /dev/null +++ b/client/src/Components/v2/actions-menu/index.tsx @@ -0,0 +1,56 @@ +import React, { useState } from "react"; +import Menu from "@mui/material/Menu"; +import MenuItem from "@mui/material/MenuItem"; +import IconButton from "@mui/material/IconButton"; +import { Settings } from "lucide-react"; + +export type ActionMenuItem = { + id: number | string; + label: React.ReactNode; + action: Function; + closeMenu?: boolean; +}; + +export const ActionsMenu = ({ items }: { items: ActionMenuItem[] }) => { + const [anchorEl, setAnchorEl] = useState(null); + const open = Boolean(anchorEl); + + const handleClick = (e: React.MouseEvent) => { + e.stopPropagation(); + setAnchorEl(e.currentTarget); + }; + + const handleClose = (e: React.MouseEvent) => { + e.stopPropagation(); + setAnchorEl(null); + }; + + return ( +
+ + + + + {items.map((item) => ( + ) => { + e.stopPropagation(); + if (item.closeMenu) handleClose(e); + item.action(); + }} + > + {item.label} + + ))} + +
+ ); +}; diff --git a/client/src/Components/v2/common/HeatmapResponseTime.tsx b/client/src/Components/v2/common/HeatmapResponseTime.tsx new file mode 100644 index 000000000..e81a2701e --- /dev/null +++ b/client/src/Components/v2/common/HeatmapResponseTime.tsx @@ -0,0 +1,140 @@ +import Box from "@mui/material/Box"; +import { useTheme } from "@mui/material/styles"; +import type { Check } from "@/Types/Check"; +import { getResponseColor } from "@/Utils/DataUtils"; +import { HeatmapResponseTimeTooltip } from "./HeatmapResponseTimeTooltip"; +import type { SxProps } from "@mui/material/styles"; +import type { ResponsiveStyleValue } from "@mui/system"; + +interface PlaceholderCheck { + status: "placeholder"; + responseTime: 0; + createdAt: ""; +} + +interface HeatmapResponseTimeProps { + checks: Check[]; + gap?: ResponsiveStyleValue; + availabilityCellSx?: SxProps; + responseCellSx?: SxProps; +} + +export const HeatmapResponseTime = ({ + checks, + gap, + availabilityCellSx, + responseCellSx, +}: HeatmapResponseTimeProps) => { + const theme = useTheme(); + if (!gap) { + gap = theme.spacing(0.5); + } + + if (!checks || checks.length === 0) return null; + + const latestChecks = checks.slice(-25).reverse(); + + let data: Array; + if (latestChecks.length !== 25) { + const placeholders = Array(25 - latestChecks.length).fill({ + status: "placeholder" as const, + }); + data = [...latestChecks, ...placeholders]; + } else { + data = latestChecks; + } + return ( + + + {data.map((check, index) => { + const statusBg = + check.status === "placeholder" + ? theme.palette.action.hover + : check.status === true + ? theme.palette.success.light + : theme.palette.error.light; + const respBg = + check.status === "placeholder" + ? theme.palette.action.hover + : getResponseColor(check.responseTime || 0, { + start: theme.palette.success.main, + mid: theme.palette.warning.main, + end: theme.palette.error.main, + }); + const statusBorder = + check.status === "placeholder" + ? theme.palette.divider + : check.status === true + ? theme.palette.success.main + : theme.palette.error.main; + + return ( + + + + + + + + + + + + ); + })} + + + ); +}; diff --git a/client/src/Components/v2/common/HeatmapResponseTimeTooltip.tsx b/client/src/Components/v2/common/HeatmapResponseTimeTooltip.tsx new file mode 100644 index 000000000..448a60e49 --- /dev/null +++ b/client/src/Components/v2/common/HeatmapResponseTimeTooltip.tsx @@ -0,0 +1,71 @@ +import Stack from "@mui/material/Stack"; +import Tooltip from "@mui/material/Tooltip"; +import Typography from "@mui/material/Typography"; +import { formatDateWithTz } from "@/Utils/TimeUtils"; +import type { Check } from "@/Types/Check"; +import { useTheme } from "@mui/material/styles"; +import { useSelector } from "react-redux"; +import type { RootState } from "@/Types/state"; + +type HeatmapCheck = Check | { status: "placeholder"; responseTime: 0; createdAt: "" }; + +export const HeatmapResponseTimeTooltip = ({ + children, + check, +}: { + children: React.ReactElement; + check: HeatmapCheck; +}) => { + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + const theme = useTheme(); + + const getColor = (status: boolean) => { + if (status === true) return theme.palette.success.light; + if (status === false) return theme.palette.error.light; + }; + + if (check.status === "placeholder") { + return children; + } + + return ( + + + {formatDateWithTz(check?.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)} + + {check?.originalResponseTime && ( + + Response Time: {check.originalResponseTime.toFixed()} ms + + )} + + + Status:{" "} + {check?.status} + +
+ } + > + {children} + + ); +}; diff --git a/client/src/Components/v2/common/HistogramResponseTime.tsx b/client/src/Components/v2/common/HistogramResponseTime.tsx new file mode 100644 index 000000000..bbe3bd9d4 --- /dev/null +++ b/client/src/Components/v2/common/HistogramResponseTime.tsx @@ -0,0 +1,102 @@ +import Box from "@mui/material/Box"; +import { useTheme } from "@mui/material/styles"; +import type { ResponsiveStyleValue } from "@mui/system"; +import type { Check } from "@/Types/Check"; +import { HeatmapResponseTimeTooltip } from "./HeatmapResponseTimeTooltip"; + +interface HistogramResponseTimeProps { + checks: Check[]; + height?: ResponsiveStyleValue; + gap?: ResponsiveStyleValue; +} + +const DEFAULT_HEIGHT = 50; + +export const HistogramResponseTime = ({ + checks, + height = DEFAULT_HEIGHT, + gap, +}: HistogramResponseTimeProps) => { + const theme = useTheme(); + + if (!Array.isArray(checks) || checks.length === 0) return null; + + let data = Array(); + + if (!checks || checks.length === 0) { + data = []; + } + if (checks.length !== 25) { + const placeholders = Array(25 - checks.length).fill({ + status: "placeholder", + }); + data = [...checks, ...placeholders]; + } else { + data = checks; + } + const chartHeight = typeof height === "number" ? `${height}px` : height; + const gridGap = gap ?? theme.spacing(0.5); + return ( + + + {data.map((check, index) => { + const isPlaceholder = (check as any).status === "placeholder"; + const heightPct = `${Math.max(0, Math.min(100, (check as any).responseTime ?? 0))}%`; + const barColor = + check.status === true + ? theme.palette.success.light + : theme.palette.error.light; + const bar = ( + + + + ); + + return isPlaceholder ? ( + + {bar} + + ) : ( + + {bar} + + ); + })} + + + ); +}; diff --git a/client/src/Components/v2/common/index.ts b/client/src/Components/v2/common/index.ts new file mode 100644 index 000000000..698d25d1e --- /dev/null +++ b/client/src/Components/v2/common/index.ts @@ -0,0 +1,3 @@ +export * from "./HistogramResponseTime"; +export * from "./HeatmapResponseTime"; +export * from "./HeatmapResponseTimeTooltip"; diff --git a/client/src/Components/v2/design-elements/BaseBox.tsx b/client/src/Components/v2/design-elements/BaseBox.tsx new file mode 100644 index 000000000..580d15dcf --- /dev/null +++ b/client/src/Components/v2/design-elements/BaseBox.tsx @@ -0,0 +1,19 @@ +import Box, { type BoxProps } from "@mui/material/Box"; +import { useTheme } from "@mui/material/styles"; + +export const BaseBox = ({ sx, ...rest }: BoxProps) => { + const theme = useTheme(); + return ( + + ); +}; diff --git a/client/src/Components/v2/design-elements/StatusBox.tsx b/client/src/Components/v2/design-elements/StatusBox.tsx new file mode 100644 index 000000000..6b1c1920c --- /dev/null +++ b/client/src/Components/v2/design-elements/StatusBox.tsx @@ -0,0 +1,112 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import Box from "@mui/material/Box"; +import { BaseBox } from "@/Components/v2/design-elements"; +import Background from "@/assets/Images/background-grid.svg?react"; +import { useTranslation } from "react-i18next"; + +import { useTheme } from "@mui/material/styles"; + +type StatusBoxProps = React.PropsWithChildren<{ children: React.ReactNode }>; + +export const BGBox = ({ children }: StatusBoxProps) => { + const theme = useTheme(); + return ( + + + + + {children} + + ); +}; + +const StatusBox = ({ + label, + n, + color, +}: { + label: string; + n: number; + color: string | undefined; +}) => { + const theme = useTheme(); + return ( + + + + {label} + + + {n} + + + + ); +}; + +export const UpStatusBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; + +export const DownStatusBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; + +export const PausedStatusBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; + +export const InitializingStatusBox = ({ n }: { n: number }) => { + const theme = useTheme(); + const { t } = useTranslation(); + return ( + + ); +}; diff --git a/client/src/Components/v2/design-elements/StatusLabel.tsx b/client/src/Components/v2/design-elements/StatusLabel.tsx new file mode 100644 index 000000000..30adf5917 --- /dev/null +++ b/client/src/Components/v2/design-elements/StatusLabel.tsx @@ -0,0 +1,101 @@ +import Box from "@mui/material/Box"; +import { BaseBox } from "@/Components/v2/design-elements"; +import Typography from "@mui/material/Typography"; + +import type { MonitorStatus } from "@/Types/Monitor"; +import type { SxProps } from "@mui/material/styles"; +import { getStatusPalette, getValuePalette } from "@/Utils/MonitorUtils"; +import { useTheme } from "@mui/material/styles"; +import { useTranslation } from "react-i18next"; + +export const ValueTypes = ["positive", "negative", "neutral"] as const; +export type ValueType = (typeof ValueTypes)[number]; + +export const StatusLabel = ({ + status, + isActive, + sx, +}: { + status: MonitorStatus; + isActive: boolean; + sx?: SxProps; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const palette = getStatusPalette(status); + + const determineStatus = (isActive: boolean, status: MonitorStatus): string => { + if (isActive === false) { + return t("pages.common.monitors.status.paused"); + } + + if (status === true) { + return t("pages.common.monitors.status.up"); + } + if (status === false) { + return t("pages.common.monitors.status.down"); + } + return t("pages.common.monitors.status.initializing"); + }; + + return ( + + + + {determineStatus(isActive, status)} + + + ); +}; + +export const ValueLabel = ({ value, text }: { value: ValueType; text: string }) => { + const theme = useTheme(); + const palette = getValuePalette(value); + const transformedText = text.charAt(0).toUpperCase() + text.slice(1).toLowerCase(); + + return ( + + + {transformedText} + + ); +}; diff --git a/client/src/Components/v2/design-elements/Table.tsx b/client/src/Components/v2/design-elements/Table.tsx new file mode 100644 index 000000000..39f465b79 --- /dev/null +++ b/client/src/Components/v2/design-elements/Table.tsx @@ -0,0 +1,576 @@ +import Stack from "@mui/material/Stack"; +import Grid2 from "@mui/material/Grid2"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import Paper from "@mui/material/Paper"; +import Table from "@mui/material/Table"; +import TableBody from "@mui/material/TableBody"; +import TableCell from "@mui/material/TableCell"; +import TableContainer from "@mui/material/TableContainer"; +import TableHead from "@mui/material/TableHead"; +import TableRow from "@mui/material/TableRow"; +import Collapse from "@mui/material/Collapse"; +import Tooltip from "@mui/material/Tooltip"; + +import IconButton from "@mui/material/IconButton"; +import { + ChevronsLeft, + ChevronsRight, + ChevronLeft, + ChevronRight, + Coffee, + Ellipsis, + PartyPopper, +} from "lucide-react"; + +import TablePagination from "@mui/material/TablePagination"; +import type { TablePaginationProps, TablePaginationOwnProps } from "@mui/material/TablePagination"; + +import { useTranslation } from "react-i18next"; +import { useState, Fragment } from "react"; +import { useTheme } from "@mui/material/styles"; +import useMediaQuery from "@mui/material/useMediaQuery"; + +export type Header = { + id: number | string; + content: React.ReactNode; + onClick?: (event: React.MouseEvent, row: T) => void; + render: (row: T) => React.ReactNode; +}; + +type DataTableProps = { + headers: Header[]; + data: T[]; + onRowClick?: (row: T) => void; + cardsOnSmallScreens?: boolean; + expandableRows?: boolean; + renderExpandedContent?: (row: T) => React.ReactNode; + emptyViewText?: string; + emptyViewPositive?: boolean; +}; + +export function DataTable< + T extends { + id?: string | number; + _id?: string | number; + onRowClick?: (row: T) => void; + }, +>({ + headers, + data, + onRowClick, + cardsOnSmallScreens = true, + expandableRows = false, + renderExpandedContent, + emptyViewText, + emptyViewPositive, +}: DataTableProps) { + const theme = useTheme(); + const [expanded, setExpanded] = useState<(string | number) | null>(null); + const handleExpand = (row: T) => { + const key = row.id || row._id || null; + setExpanded(expanded === key ? null : key); + }; + + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + + if (data.length === 0 || headers.length === 0) { + return ( + + ); + } + + const keys = []; + // Return stack of cards for small screens + if (isSmall && cardsOnSmallScreens) { + return ( + + {data.map((row) => { + const key = row.id || row._id || Math.random(); + keys.push(key); + return ( + (onRowClick ? onRowClick(row) : null)} + spacing={theme.spacing(4)} + sx={{ + borderStyle: "solid", + borderWidth: 1, + borderColor: theme.palette.divider, + borderRadius: theme.shape.borderRadius, + padding: theme.spacing(4), + cursor: onRowClick ? "pointer" : "default", + }} + key={key} + > + {headers.map((header) => { + return ( + + + + {header.content} + + + + {header.render(row)}{" "} + + + ); + })} + {expandableRows && renderExpandedContent && renderExpandedContent(row) && ( + {renderExpandedContent(row)} + )} + + ); + })} + + ); + } + + return ( + + + + + {headers.map((header, idx) => { + return ( + + {header.content} + + ); + })} + + + + {data.map((row) => { + const key = row.id || row._id || Math.random(); + const isExpanded = expanded === key; + + return ( + + { + if (expandableRows) handleExpand(row); + else if (onRowClick) onRowClick(row); + }} + > + {headers.map((header, index) => { + return ( + header.onClick!(e, row) : undefined + } + > + {header.render(row)} + + ); + })} + + {expandableRows && + renderExpandedContent && + renderExpandedContent(row) && ( + + + + {renderExpandedContent(row)} + + + + )} + + ); + })} + +
+
+ ); +} + +interface TablePaginationActionsProps { + count: number; + page: number; + rowsPerPage: number; + onPageChange: (event: React.MouseEvent, newPage: number) => void; +} + +function TablePaginationActions(props: TablePaginationActionsProps) { + const theme = useTheme(); + const { count, page, rowsPerPage, onPageChange } = props; + + const handleFirstPageButtonClick = (event: React.MouseEvent) => { + onPageChange(event, 0); + }; + + const handleBackButtonClick = (event: React.MouseEvent) => { + onPageChange(event, page - 1); + }; + + const handleNextButtonClick = (event: React.MouseEvent) => { + onPageChange(event, page + 1); + }; + + const handleLastPageButtonClick = (event: React.MouseEvent) => { + onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1)); + }; + + return ( + + + {theme.direction === "rtl" ? ( + + ) : ( + + )} + + + {theme.direction === "rtl" ? ( + + ) : ( + + )} + + = Math.ceil(count / rowsPerPage) - 1} + aria-label="next page" + > + {theme.direction === "rtl" ? ( + + ) : ( + + )} + + = Math.ceil(count / rowsPerPage) - 1} + aria-label="last page" + > + {theme.direction === "rtl" ? ( + + ) : ( + + )} + + + ); +} + +interface HasMoreTablePaginationActionsProps { + hasMore?: boolean; + page: number; + onPageChange: (event: React.MouseEvent, newPage: number) => void; +} +function HasMoreTablePaginationActions(props: HasMoreTablePaginationActionsProps) { + const theme = useTheme(); + const { hasMore, page, onPageChange } = props; + + const handleFirstPageButtonClick = (event: React.MouseEvent) => { + onPageChange(event, 0); + }; + + const handleBackButtonClick = (event: React.MouseEvent) => { + onPageChange(event, page - 1); + }; + + const handleNextButtonClick = (event: React.MouseEvent) => { + onPageChange(event, page + 1); + }; + + return ( + + + {theme.direction === "rtl" ? ( + + ) : ( + + )} + + + {theme.direction === "rtl" ? ( + + ) : ( + + )} + + + {theme.direction === "rtl" ? ( + + ) : ( + + )} + + + + + + + + ); +} + +interface PaginationProps extends TablePaginationOwnProps { + component?: React.ElementType; + hasMore?: boolean; +} + +export const Pagination = ({ ...props }: PaginationProps) => { + const { hasMore, ...rest } = props; + const isSmall = useMediaQuery((theme: any) => theme.breakpoints.down("sm")); + const theme = useTheme(); + const labelDisplayedRows = ({ + from, + to, + count, + }: { + from: number; + to: number; + count: number; + }) => { + if (hasMore) { + return to === 0 ? "" : `${from}–${to}`; + } + return `${from}–${to} of ${count}`; + }; + + return ( + + typeof hasMore === "boolean" ? ( + + ) : ( + + ) + } + rowsPerPageOptions={[5, 10, 25]} + labelDisplayedRows={labelDisplayedRows} + {...rest} + sx={{ + "& .MuiTablePagination-toolbar": isSmall + ? { + display: "grid", + gridTemplateColumns: "1fr 1fr", + gridAutoRows: "auto", + rowGap: theme.spacing(1), + alignItems: "center", + justifyItems: "center", + paddingLeft: 0, + paddingRight: 0, + } + : { display: "flex", alignItems: "center" }, + "& .MuiTablePagination-selectLabel": { + gridColumn: "1", + gridRow: "1", + justifySelf: "center", + }, + "& .MuiTablePagination-select": { + gridColumn: "2", + gridRow: "1", + justifySelf: "center", + }, + "& .MuiTablePagination-displayedRows": isSmall + ? { gridColumn: "1 / span 2", gridRow: "2", justifySelf: "center" } + : {}, + "& .table-pagination-actions": isSmall + ? { gridColumn: "1 / span 2", gridRow: "3", justifySelf: "center" } + : {}, + "& .MuiSelect-select": { + border: 1, + borderColor: theme.palette.divider, + borderRadius: theme.shape.borderRadius, + }, + }} + /> + ); +}; + +const EmptyView = ({ text, positive }: { text?: string; positive?: boolean }) => { + const theme = useTheme(); + const { t } = useTranslation(); + const Icon = positive ? PartyPopper : Coffee; + return ( + + + + + + {text ?? t("common.table.empty")} + + + ); +}; diff --git a/client/src/Components/v2/design-elements/index.tsx b/client/src/Components/v2/design-elements/index.tsx index 9970ed980..374dabef7 100644 --- a/client/src/Components/v2/design-elements/index.tsx +++ b/client/src/Components/v2/design-elements/index.tsx @@ -3,3 +3,8 @@ export * from "./BasePage"; export * from "./Fallback"; export * from "./SkeletonCard"; export * from "./BulletPointCheck"; +export * from "./StatusBox"; +export * from "./BaseBox"; +export * from "./Table"; +export { DataTable as Table } from "./Table"; +export * from "./StatusLabel"; diff --git a/client/src/Components/v2/inputs/AutoComplete.tsx b/client/src/Components/v2/inputs/AutoComplete.tsx new file mode 100644 index 000000000..af0e8cbfd --- /dev/null +++ b/client/src/Components/v2/inputs/AutoComplete.tsx @@ -0,0 +1,88 @@ +import Autocomplete from "@mui/material/Autocomplete"; +import type { AutocompleteProps } from "@mui/material/Autocomplete"; +import { TextField, Checkbox } from "@/Components/v2/inputs"; +import ListItem from "@mui/material/ListItem"; +import Stack from "@mui/material/Stack"; +import { useTheme } from "@mui/material/styles"; +import { FieldLabel } from "./FieldLabel"; +import { ChevronDown } from "lucide-react"; + +type AutoCompleteInputProps = Omit< + AutocompleteProps, + "renderInput" +> & { + renderInput?: AutocompleteProps["renderInput"]; + fieldLabel?: string; + required?: boolean; +}; + +export const AutoCompleteInput = ({ + fieldLabel, + required, + renderInput, + ...props +}: AutoCompleteInputProps) => { + const theme = useTheme(); + const multiple = props.multiple; + + const defaultRenderInput = (params: any) => ( + + ); + + const autocomplete = ( + + } + renderInput={renderInput || defaultRenderInput} + getOptionKey={(option) => option.id} + renderTags={() => null} + renderOption={(props, option, { selected }) => { + const { key, ...optionProps } = props; + return ( + + + {multiple && } + {option.name} + + + ); + }} + sx={{ + "&.MuiAutocomplete-root .MuiAutocomplete-input": { + padding: `0 ${theme.spacing(5)}`, + }, + "& .MuiInputBase-root .MuiAutocomplete-endAdornment": { + right: theme.spacing(3), + }, + }} + /> + ); + + if (fieldLabel) { + return ( + + {fieldLabel} + {autocomplete} + + ); + } + + return autocomplete; +}; diff --git a/client/src/Components/v2/inputs/TextInput.tsx b/client/src/Components/v2/inputs/TextInput.tsx new file mode 100644 index 000000000..9a16a43c7 --- /dev/null +++ b/client/src/Components/v2/inputs/TextInput.tsx @@ -0,0 +1,56 @@ +import { forwardRef } from "react"; +import TextField from "@mui/material/TextField"; +import type { TextFieldProps } from "@mui/material"; +import { typographyLevels } from "@/Utils/Theme/v2Palette"; +import { useTheme } from "@mui/material/styles"; +import Stack from "@mui/material/Stack"; +import { FieldLabel } from "./FieldLabel"; + +interface TextInputProps extends Omit { + fieldLabel?: string; + required?: boolean; +} + +export const TextInput = forwardRef(function TextInput( + { fieldLabel, required, ...props }, + ref +) { + const theme = useTheme(); + + const input = ( + + ); + + if (fieldLabel) { + return ( + + {fieldLabel} + {input} + + ); + } + + return input; +}); +TextInput.displayName = "TextInput"; diff --git a/client/src/Components/v2/inputs/index.tsx b/client/src/Components/v2/inputs/index.tsx index 3cd2516b1..9a76f6f7f 100644 --- a/client/src/Components/v2/inputs/index.tsx +++ b/client/src/Components/v2/inputs/index.tsx @@ -2,3 +2,5 @@ export { ButtonInput as Button } from "./Button"; export * from "./FieldLabel"; export { SelectInput as Select } from "./Select"; export { CheckboxInput as Checkbox } from "./Checkbox"; +export { AutoCompleteInput as Autocomplete } from "./AutoComplete"; +export { TextInput as TextField } from "./TextInput"; diff --git a/client/src/Components/v2/monitors/FilterControls.tsx b/client/src/Components/v2/monitors/FilterControls.tsx index d5ec8cc55..5d68bf49a 100644 --- a/client/src/Components/v2/monitors/FilterControls.tsx +++ b/client/src/Components/v2/monitors/FilterControls.tsx @@ -16,6 +16,7 @@ export const FilterControls = ({ setSelectedStatus, selectedState, setSelectedState, + onClearFilters, }: { selectedTypes: MonitorType[]; setSelectedTypes: React.Dispatch>; @@ -23,6 +24,7 @@ export const FilterControls = ({ setSelectedStatus: React.Dispatch>; selectedState: string; setSelectedState: React.Dispatch>; + onClearFilters: () => void; }) => { const theme = useTheme(); const isFilterActive = @@ -78,11 +80,7 @@ export const FilterControls = ({ {isFilterActive && ( diff --git a/client/src/Features/UI/uiSlice.js b/client/src/Features/UI/uiSlice.js index af9731e9b..65aff99a8 100644 --- a/client/src/Features/UI/uiSlice.js +++ b/client/src/Features/UI/uiSlice.js @@ -29,6 +29,7 @@ const initialState = { distributedUptimeEnabled: false, language: "en", starPromptOpen: true, + chartType: "histogram", }; const uiSlice = createSlice({ @@ -73,6 +74,9 @@ const uiSlice = createSlice({ setStarPromptOpen: (state, action) => { state.starPromptOpen = action.payload; }, + setChartType: (state, action) => { + state.chartType = action.payload; + }, }, }); @@ -88,4 +92,5 @@ export const { setDistributedUptimeEnabled, setLanguage, setStarPromptOpen, + setChartType, } = uiSlice.actions; diff --git a/client/src/Hooks/UseApi.ts b/client/src/Hooks/UseApi.ts index 06845d6c1..441c2b77b 100644 --- a/client/src/Hooks/UseApi.ts +++ b/client/src/Hooks/UseApi.ts @@ -1,7 +1,9 @@ import useSWR from "swr"; import type { SWRConfiguration } from "swr"; import type { AxiosRequestConfig } from "axios"; -import { get } from "@/Utils/ApiClient"; +import { get, patch, post } from "@/Utils/ApiClient"; +import { useState } from "react"; +import { useToast } from "@/Hooks/UseToast"; interface ApiResponse { success: boolean; @@ -32,3 +34,76 @@ export const useGet = ( refetch: mutate, }; }; + +export const usePost = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const { toastError, toastSuccess } = useToast(); + + const postFn = async ( + endpoint: string, + body: B, + config?: AxiosRequestConfig + ): Promise | null> => { + setLoading(true); + setError(null); + + try { + const res = await post>(endpoint, body, { + ...config, + headers: { + ...config?.headers, + }, + }); + toastSuccess(res.data?.msg || "Operation successful"); + + return res.data; + } catch (err: any) { + const errMsg = err?.response?.data?.msg || err.message || "An error occurred"; + console.error(err); + toastError(errMsg); + setError(errMsg); + return null; + } finally { + setLoading(false); + } + }; + + return { post: postFn, loading, error }; +}; + +export const usePatch = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const { toastError, toastSuccess } = useToast(); + + const patchFn = async ( + endpoint: string, + body?: B, + config?: AxiosRequestConfig + ): Promise | null> => { + setLoading(true); + setError(null); + + try { + const res = await patch>(endpoint, body, { + ...config, + headers: { + ...config?.headers, + }, + }); + toastSuccess(res.data?.msg || "Operation successful"); + return res.data; + } catch (err: any) { + console.error(err); + const errMsg = err?.response?.data?.msg || err.message || "An error occurred"; + toastError(errMsg); + setError(errMsg); + return null; + } finally { + setLoading(false); + } + }; + + return { patch: patchFn, loading, error }; +}; diff --git a/client/src/Hooks/UseToast.ts b/client/src/Hooks/UseToast.ts new file mode 100644 index 000000000..2ee275253 --- /dev/null +++ b/client/src/Hooks/UseToast.ts @@ -0,0 +1,23 @@ +import { toast, type ToastOptions } from "react-toastify"; +export const useToast = () => { + const showToast = (message: string, options?: ToastOptions) => { + toast.dismiss(); + const baseStyle: React.CSSProperties = { + whiteSpace: "pre-line", + wordBreak: "break-word", + }; + toast(message, { + ...options, + style: { ...baseStyle, ...(options?.style || {}) }, + }); + }; + + const toastSuccess = (msg: string, opts?: ToastOptions) => + showToast(msg, { ...opts, type: "success" }); + const toastError = (msg: string, opts?: ToastOptions) => + showToast(msg, { ...opts, type: "error" }); + const toastInfo = (msg: string, opts?: ToastOptions) => + showToast(msg, { ...opts, type: "info" }); + + return { showToast, toastSuccess, toastError, toastInfo }; +}; diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx new file mode 100644 index 000000000..978d1d45d --- /dev/null +++ b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx @@ -0,0 +1,276 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { + Table, + type Header, + Pagination, + StatusLabel, +} from "@/Components/v2/design-elements"; +import { HeatmapResponseTime, HistogramResponseTime } from "@/Components/v2/common"; +import { ActionsMenu } from "@/Components/v2/actions-menu"; +import { ArrowDown, ArrowUp } from "lucide-react"; + +import { useTranslation } from "react-i18next"; +import { useTheme } from "@mui/material/styles"; +import { useNavigate } from "react-router-dom"; +import { usePost } from "@/Hooks/UseApi"; + +import type { Check } from "@/Types/Check"; +import type { Monitor } from "@/Types/Monitor"; +import type { ActionMenuItem } from "@/Components/v2/actions-menu"; +// import type { ChartType } from "@/features/uiSlice"; + +export const MonitorTable = ({ + monitors, + refetch, + // setSelectedMonitor, + // sortField, + // setSortField, + // sortOrder, + // setSortOrder, + count, + page, + setPage, + rowsPerPage, + setRowsPerPage, +}: { + monitors: Monitor[]; + refetch: Function; + // setSelectedMonitor: Function; + // sortField: string; + // setSortField: (field: string) => void; + // sortOrder: "asc" | "desc"; + // setSortOrder: (order: "asc" | "desc") => void; + count: number; + page: number; + setPage: (page: number) => void; + rowsPerPage: number; + setRowsPerPage: (rowsPerPage: number) => void; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const navigate = useNavigate(); + const chartType = "histogram"; + const { + post, + // loading: isPosting, + // error: postError, + } = usePost(); + + // const handlePageChange = ( + // _e: React.MouseEvent | null, + // newPage: number + // ) => { + // setPage(newPage); + // }; + + // const handleRowsPerPageChange = ( + // e: React.ChangeEvent + // ) => { + // const value = Number(e.target.value); + // setPage(0); + // setRowsPerPage(value); + // }; + + // const handleSort = (e: any, field: string) => { + // e.preventDefault(); + // e.stopPropagation(); + // if (sortField === field) { + // const newOrder = sortOrder === "asc" ? "desc" : "asc"; + // setSortOrder(newOrder); + // } else { + // setSortField(field); + // setSortOrder("asc"); + // } + // refetch(); + // }; + + const getActions = (monitor: Monitor): ActionMenuItem[] => { + return [ + { + id: 1, + label: t("pages.common.monitors.actions.openSite"), + action: () => { + window.open(monitor.url, "_blank", "noreferrer"); + }, + closeMenu: true, + }, + { + id: 2, + label: t("pages.common.monitors.actions.details"), + action: () => { + navigate(`${monitor.id}`); + }, + }, + { + id: 3, + label: t("pages.common.monitors.actions.incidents"), + action: () => { + navigate(`/incidents?monitorId=${monitor.id}`); + }, + }, + { + id: 4, + label: t("pages.common.monitors.actions.configure"), + action: () => { + navigate(`/uptime/create/${monitor.id}`); + }, + }, + // { + // id: 5, + // label: "Clone", + // action: () => { + + // }, + // }, + { + id: 6, + label: + monitor.isActive === false + ? t("pages.common.monitors.actions.resume") + : t("pages.common.monitors.actions.pause"), + action: async () => { + await post(`/monitors/pause/${monitor.id}`, {}); + refetch(); + }, + closeMenu: true, + }, + { + id: 7, + label: ( + + {t("pages.common.monitors.actions.delete")} + + ), + action: () => { + // setSelectedMonitor(monitor); + }, + closeMenu: true, + }, + ]; + }; + + const getHeaders = (chartType: string) => { + const renderSortIcon = (isActive: boolean) => ( + + {/* {isActive ? ( + sortOrder === "asc" ? ( + + ) : ( + + ) + ) : null} */} + + ); + const headers: Header[] = [ + { + id: "name", + content: ( + handleSort(e, "name")} + sx={{ cursor: "pointer" }} + > + {t("common.table.headers.name")} + {/* {renderSortIcon(sortField === "name")} */} + + ), + + render: (row) => { + return row?.name; + }, + }, + { + id: "status", + content: ( + handleSort(e, "status")} + sx={{ cursor: "pointer" }} + > + {t("common.table.headers.status")} + {/* {renderSortIcon(sortField === "status")} */} + + ), + render: (row) => { + return ( + + ); + }, + }, + { + id: "histogram", + content: t("pages.uptime.table.headers.responseTime"), + render: (row) => { + if (chartType === "histogram") { + return ; + } else { + return ; + } + }, + }, + { + id: "type", + content: ( + handleSort(e, "type")} + sx={{ cursor: "pointer" }} + > + {t("common.table.headers.type")} + {/* {renderSortIcon(sortField === "type")} */} + + ), + render: (row) => { + return row.type; + }, + }, + { + id: "actions", + content: t("common.table.headers.actions"), + render: (row) => { + return ; + }, + }, + ]; + return headers; + }; + + const headers = getHeaders(chartType); + + return ( + + { + navigate(`/uptime/${row.id}`); + }} + /> + setPage(newPage)} + onRowsPerPageChange={(e) => setRowsPerPage(Number(e.target.value))} + /> + + ); +}; diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index 2d7a851d2..255a50874 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -1,39 +1,86 @@ -import { useGet } from "@/Hooks/UseApi"; -import { MonitorBasePageWithStates } from "@/Components/v2/design-elements"; -import type { Monitor, MonitorType, MonitorsWithChecksResponse } from "@/Types/Monitor"; import { FilterControls } from "@/Components/v2/monitors"; -import { useState, useMemo } from "react"; -import { useSelector } from "react-redux"; +import { + MonitorBasePageWithStates, + UpStatusBox, + DownStatusBox, + PausedStatusBox, +} from "@/Components/v2/design-elements"; +import { TextField } from "@/Components/v2/inputs"; +import Stack from "@mui/material/Stack"; +import { MonitorTable } from "@/Pages/Uptime/Monitors/Components/UptimeMonitorsTable"; +import { useTranslation } from "react-i18next"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useGet } from "@/Hooks/UseApi"; +import type { Monitor, MonitorType, MonitorsWithChecksResponse } from "@/Types/Monitor"; +import { useState, useMemo, useCallback } from "react"; +import { useSelector, useDispatch } from "react-redux"; +import { setRowsPerPage } from "@/Features/UI/uiSlice.js"; + import type { RootState } from "@/Types/state"; +import { useTheme } from "@mui/material"; const UptimeMonitorsPage = () => { + const { t } = useTranslation(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const dispatch = useDispatch(); + // Redux state - const rowsPerPage = useSelector((state: RootState) => state.ui.monitors.rowsPerPage); + const rowsPerPage = useSelector( + (state: RootState) => state.ui?.monitors?.rowsPerPage ?? 10 + ); // Local state const [selectedTypes, setSelectedTypes] = useState([]); const [selectedStatus, setSelectedStatus] = useState(""); const [selectedState, setSelectedState] = useState(""); - const [page, setPage] = useState(0); - const [search, setSearch] = useState(""); - const [sort, setSort] = useState< - { field: string; order: "asc" | "desc" } | undefined - >(); + const [page, setPage] = useState(0); + const [search, setSearch] = useState(""); + const [sort, setSort] = useState<{ field: string; order: "asc" | "desc" } | undefined>( + undefined + ); + + // Convert filter selections to API filter values + // Status: "up" -> true, "down" -> false + // State: "active" -> true, "paused" -> false + const toFilterStatus = useMemo(() => { + if (selectedStatus === "up") return "true"; + if (selectedStatus === "down") return "false"; + return undefined; + }, [selectedStatus]); + + const toFilterActive = useMemo(() => { + if (selectedState === "active") return "true"; + if (selectedState === "paused") return "false"; + return undefined; + }, [selectedState]); + + // Determine field and filter for the API request + // Priority: status > isActive > search > sort + const filterLookup = new Map([ + [toFilterStatus, "status"], + [toFilterActive, "isActive"], + ]); + const activeFilter = [...filterLookup].find(([key]) => key !== undefined); + const field = activeFilter?.[1] || (search ? "name" : sort?.field); + const filter = activeFilter?.[0] || search; + + // Default to all types when none selected + const effectiveTypes = + selectedTypes.length > 0 ? selectedTypes : ["http", "ping", "docker", "port"]; // Build URL for monitors with checks const monitorsWithChecksUrl = useMemo(() => { const params = new URLSearchParams(); - const types = - selectedTypes.length > 0 ? selectedTypes : ["http", "ping", "docker", "port"]; - types.forEach((type) => params.append("type", type)); + effectiveTypes.forEach((type) => params.append("type", type)); params.append("limit", "25"); - if (page) params.append("page", String(page)); + if (page !== undefined) params.append("page", String(page)); if (rowsPerPage) params.append("rowsPerPage", String(rowsPerPage)); - if (search) params.append("filter", search); - if (sort?.field) params.append("field", sort.field); + if (filter) params.append("filter", filter); + if (field) params.append("field", field); if (sort?.order) params.append("order", sort.order); return `/monitors/team/with-checks?${params.toString()}`; - }, [selectedTypes, page, rowsPerPage, search, sort]); + }, [effectiveTypes, page, rowsPerPage, filter, field, sort?.order]); // Data fetching const { @@ -46,10 +93,23 @@ const UptimeMonitorsPage = () => { data: monitorsWithChecksData, isLoading: monitorsWithChecksLoading, error: monitorsWithChecksError, - } = useGet(monitorsWithChecksUrl); + refetch, + } = useGet( + monitorsWithChecksUrl, + {}, + { refreshInterval: 5000 } + ); const { monitors: monitorsWithChecks, summary, count } = monitorsWithChecksData ?? {}; + // Handlers + const handleClearFilters = useCallback(() => { + setSelectedTypes([]); + setSelectedStatus(""); + setSelectedState(""); + setSearch(""); + }, []); + return ( { page="uptime" actionLink="/uptime/create" > - + + + + + + + + { + setSearch(event.target.value); + }} + /> + + { + dispatch( + setRowsPerPage({ + value: parseInt(e.target.value, 10), + table: "monitors", + }) + ); + setPage(0); + }} /> -
{JSON.stringify(monitors, null, 2)}
); }; diff --git a/client/src/Types/Check.ts b/client/src/Types/Check.ts index cccc56f3d..4d4a3d066 100644 --- a/client/src/Types/Check.ts +++ b/client/src/Types/Check.ts @@ -1,50 +1,142 @@ -export interface CheckTimingPhases { - wait: number; - dns: number; - tcp: number; - tls: number; - request: number; - firstByte: number; - download: number; - total: number; +export interface CheckMetadata { + monitorId: string; + teamId: string; + type: string; +} + +export interface CheckCpuInfo { + physical_core?: number; + logical_core?: number; + frequency?: number; + temperature?: number[]; + free_percent?: number; + usage_percent?: number; +} + +export interface CheckMemoryInfo { + total_bytes?: number; + available_bytes?: number; + used_bytes?: number; + usage_percent?: number; +} + +export interface CheckHostInfo { + os?: string; + platform?: string; + kernel_version?: string; +} + +export interface CheckCaptureInfo { + version?: string; + mode?: string; +} + +export interface CheckDiskInfo { + device?: string; + mountpoint?: string; + read_speed_bytes?: number; + write_speed_bytes?: number; + total_bytes?: number; + free_bytes?: number; + usage_percent?: number; +} + +export interface CheckErrorInfo { + metric: string[]; + err: string; +} + +export interface CheckNetworkInterfaceInfo { + name: string; + bytes_sent: number; + bytes_recv: number; + packets_sent: number; + packets_recv: number; + err_in: number; + err_out: number; + drop_in: number; + drop_out: number; + fifo_in: number; + fifo_out: number; +} + +export interface CheckAudits { + cls?: ILighthouseAudit; + si?: ILighthouseAudit; + fcp?: ILighthouseAudit; + lcp?: ILighthouseAudit; + tbt?: ILighthouseAudit; +} + +export interface ILighthouseAudit { + id?: string; + title?: string; + score?: number | null; + displayValue?: string; + numericValue?: number; + numericUnit?: string; } export interface CheckTimings { - start: string; - socket: string; - lookup: string; - connect: string; - secureConnect: string; - response: string; - end: string; - phases: CheckTimingPhases; + start?: number; + socket?: number; + lookup?: number; + connect?: number; + secureConnect?: number; + response?: number; + end?: number; + abort?: number; + error?: number; + phases?: { + wait?: number; + dns?: number; + tcp?: number; + tls?: number; + request?: number; + firstByte?: number; + download?: number; + total?: number; + }; } export interface Check { - _id: string; - monitorId: string; - type: string; - status: string; - message: string; + id: string; + metadata: CheckMetadata; + status: boolean; + originalResponseTime?: number; responseTime: number; - normalResponseTime?: number; - httpStatusCode: number; + timings?: CheckTimings; + statusCode: number; + message: string; ack: boolean; + ackAt?: string | null; expiry: string; + cpu?: CheckCpuInfo; + memory?: CheckMemoryInfo; + disk?: CheckDiskInfo[]; + host?: CheckHostInfo; + errors?: CheckErrorInfo[]; + capture?: CheckCaptureInfo; + net?: CheckNetworkInterfaceInfo[]; + accessibility?: number; + bestPractices?: number; + seo?: number; + performance?: number; + audits?: CheckAudits; + __v: number; createdAt: string; updatedAt: string; - timings: CheckTimings; } export interface GroupedCheck { _id: string; avgResponseTime: number; - count: number; + totalChecks: number; } export interface LatestCheck { - status: string; + status: boolean; responseTime: number; checkedAt: string; - _id: string; + id: string; } diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index 8eff63518..8a0f9c7e8 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -1,5 +1,5 @@ import type { Check } from "@/Types/Check"; -export type MonitorStatus = "up" | "down" | "initializing"; +export type MonitorStatus = boolean | undefined; export const MonitorTypes = [ "http", @@ -27,8 +27,7 @@ export interface Monitor { updatedAt: string; updatedBy: string; url: string; - __v: number; - _id: string; + id: string; } export interface MonitorWithChecks extends Monitor { @@ -38,9 +37,9 @@ export interface MonitorWithChecks extends Monitor { export interface MonitorsSummary { totalMonitors: number; - totalUp: number; - totalDown: number; - totalPaused: number; + upMonitors: number; + downMonitors: number; + pausedMonitors: number; } export interface MonitorsWithChecksResponse { diff --git a/client/src/Types/mui.d.ts b/client/src/Types/mui.d.ts index 84bcd799f..3878f9391 100644 --- a/client/src/Types/mui.d.ts +++ b/client/src/Types/mui.d.ts @@ -1,24 +1,16 @@ import "@mui/material/Button"; declare module "@mui/material/styles" { - interface Palette { - accent: Palette["primary"]; - tertiary: Palette["primary"]; - } - interface PaletteOptions { - accent?: PaletteOptions["primary"]; - tertiary?: PaletteOptions["primary"]; - } - - interface PaletteColor { - contrastTextSecondary?: string; - contrastTextTertiary?: string; - lowContrast?: string; - } + interface Palette {} + interface PaletteOptions {} } declare module "@mui/material/Button" { interface ButtonPropsColorOverrides { - accent: true; + neutral: true; } } + +declare module "@mui/material/CircularProgress" { + interface CircularProgressPropsColorOverrides {} +} diff --git a/client/src/Types/state.ts b/client/src/Types/state.ts index fc25798c4..137d67b66 100644 --- a/client/src/Types/state.ts +++ b/client/src/Types/state.ts @@ -32,6 +32,7 @@ export interface UIState { distributedUptimeEnabled: boolean; language: string; starPromptOpen: boolean; + chartType: string; } export interface RootState { diff --git a/client/src/Utils/DataUtils.ts b/client/src/Utils/DataUtils.ts new file mode 100644 index 000000000..c0bbb2d2a --- /dev/null +++ b/client/src/Utils/DataUtils.ts @@ -0,0 +1,129 @@ +const MIN_OUT = 10; +const MAX_OUT = 100; + +export const normalizeResponseTimes = ( + checks: T[], + key: K +): (T & { normalResponseTime: number })[] => { + if (!Array.isArray(checks) || checks.length === 0) + return checks as (T & { + normalResponseTime: number; + })[]; + + if (checks.length === 1) { + return [ + { + ...checks[0], + normalResponseTime: 50, + }, + ]; + } + + const getVal = (check: T): number => { + const v = check[key] as unknown; + return typeof v === "number" && Number.isFinite(v) ? v : 0; + }; + + const { min, max } = checks.reduce( + (acc, check) => { + const v = getVal(check); + if (v > acc.max) acc.max = v; + if (v < acc.min) acc.min = v; + return acc; + }, + { max: -Infinity, min: Infinity } + ); + + const range = max - min || 1; + + return checks.map((check) => ({ + ...check, + normalResponseTime: MIN_OUT + ((getVal(check) - min) * (MAX_OUT - MIN_OUT)) / range, + })); +}; + +// Interpolate color between three theme colors over 0-100 range. +// 0-50 => start (success.light), 50-75 => mid (warning.light), 75-100 => end (error.light) +export const getResponseColor = ( + ms: number, + colors: { + start: string | undefined; + mid: string | undefined; + end: string | undefined; + } +): string => { + // New ranges with open high end: + // 0–300ms: interpolate start (good) -> mid (warning) + // 300–600ms: interpolate mid (warning) -> end (error) + // >600ms: solid end (error) + const safe = { ...colors }; + if (!safe.start) safe.start = "#4caf50"; // fallback green + if (!safe.mid) safe.mid = "#ff9800"; // fallback orange + if (!safe.end) safe.end = "#f44336"; // fallback red + + const toHex = (c: number) => c.toString(16).padStart(2, "0"); + const clamp = (n: number) => Math.min(255, Math.max(0, Math.round(n))); + const rgbToHex = (r: number, g: number, b: number) => + `#${toHex(clamp(r))}${toHex(clamp(g))}${toHex(clamp(b))}`; + + const normalizeToHex = (value: string) => { + const v = value.trim(); + if (v.startsWith("#")) { + const h = v.slice(1); + const full = + h.length === 3 + ? h + .split("") + .map((c) => c + c) + .join("") + : h.substring(0, 6); + return `#${full.toLowerCase()}`; + } + const m = v + .replace(/\s+/g, "") + .match(/^rgba?\((\d{1,3}),(\d{1,3}),(\d{1,3})(?:,(0|0?\.\d+|1))?\)$/i); + if (m) { + const r = parseInt(m[1], 10); + const g = parseInt(m[2], 10); + const b = parseInt(m[3], 10); + return rgbToHex(r, g, b); + } + // Fallback neutral + return "#7f7f7f"; + }; + + const parseHex = (hex: string) => { + const h = hex.replace("#", ""); + const full = + h.length === 3 + ? h + .split("") + .map((c) => c + c) + .join("") + : h; + const n = parseInt(full, 16); + return { r: (n >> 16) & 255, g: (n >> 8) & 255, b: n & 255 }; + }; + const mix = (a: number, b: number, t: number) => Math.round(a + (b - a) * t); + + const v = Math.max(0, ms); + if (v <= 300) { + const t = v / 300; // 0..1 from start->mid + const s = parseHex(normalizeToHex(safe.start)); + const m = parseHex(normalizeToHex(safe.mid)); + const r = mix(s.r, m.r, t); + const g = mix(s.g, m.g, t); + const b = mix(s.b, m.b, t); + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + } + if (v <= 600) { + const t = (v - 300) / 300; // 0..1 from mid->end + const m = parseHex(normalizeToHex(safe.mid)); + const e = parseHex(normalizeToHex(safe.end)); + const r = mix(m.r, e.r, t); + const g = mix(m.g, e.g, t); + const b = mix(m.b, e.b, t); + return `#${toHex(r)}${toHex(g)}${toHex(b)}`; + } + return normalizeToHex(safe.end); +}; diff --git a/client/src/Utils/MonitorUtils.ts b/client/src/Utils/MonitorUtils.ts new file mode 100644 index 000000000..ed4e2894e --- /dev/null +++ b/client/src/Utils/MonitorUtils.ts @@ -0,0 +1,120 @@ +import type { Monitor, MonitorStatus, MonitorType } from "@/Types/Monitor"; +import type { PaletteKey } from "@/Utils/Theme/v2Theme"; +import type { ValueType } from "@/Components/v2/design-elements/StatusLabel"; + +export const getMonitorPath = (type: MonitorType): string => { + const pathMap: Record = { + http: "uptime", + port: "uptime", + ping: "uptime", + hardware: "hardware", + pagespeed: "pagespeed", + docker: "docker", + game: "game-servers", + unknown: "uptime", + }; + return pathMap[type]; +}; + +export const getStatusPalette = (status: MonitorStatus): PaletteKey => { + if (status === true) { + return "success"; + } + if (status === false) { + return "error"; + } + return "warning"; +}; + +export const getValuePalette = (value: ValueType): PaletteKey => { + const paletteMap: Record = { + positive: "success", + negative: "error", + neutral: "warning", + }; + return paletteMap[value]; +}; + +export const getStatusColor = (status: MonitorStatus, theme: any): string => { + if (status === true) { + return theme.palette.success.light; + } + + if (status === false) { + return theme.palette.error.light; + } + + return theme.palette.warning.light; +}; + +export const getResponseTimeColor = (responseTime: number): PaletteKey => { + if (responseTime < 200) { + return "success"; + } else if (responseTime < 300) { + return "warning"; + } else { + return "error"; + } +}; + +export const getInfraGaugeColor = (val: number, theme: any) => { + if (val < 50) { + return theme.palette.success.main; + } else if (val < 80) { + return theme.palette.warning.light; + } else { + return theme.palette.error.light; + } +}; + +export const getPageSpeedPalette = (score: number): PaletteKey => { + if (score >= 90) return "success"; + else if (score >= 50) return "warning"; + else return "error"; +}; + +export const formatUrl = (url: string, maxLength: number = 55) => { + if (!url) return ""; + + const strippedUrl = url.replace(/^https?:\/\//, ""); + return strippedUrl.length > maxLength + ? `${strippedUrl.slice(0, maxLength)}…` + : strippedUrl; +}; + +export interface IStatusPageHeaderConfig { + paletteKey: PaletteKey; + message: string; +} +export const getStatusPageHeaderConfig = ( + monitors: Monitor[], + t: any +): IStatusPageHeaderConfig => { + if (!monitors || monitors.length === 0) { + return { paletteKey: "error", message: "No monitors available" }; + } + + const allUp = monitors.every((monitor) => monitor.status === true); + const anyDown = monitors.some((monitor) => monitor.status === false); + const allDown = monitors.every((monitor) => monitor.status === false); + + if (allUp) + return { + paletteKey: "success", + message: t("statusPage.details.statusHeader.allUp"), + }; + if (allDown) + return { + paletteKey: "error", + message: t("statusPage.details.statusHeader.allDown"), + }; + if (anyDown) + return { + paletteKey: "warning", + message: t("statusPage.details.statusHeader.anyDown"), + }; + return { + paletteKey: "warning", + message: t("statusPage.details.statusHeader.anyDown"), + }; +}; diff --git a/client/src/Utils/TimeUtils.ts b/client/src/Utils/TimeUtils.ts new file mode 100644 index 000000000..3b629baf0 --- /dev/null +++ b/client/src/Utils/TimeUtils.ts @@ -0,0 +1,52 @@ +import dayjs from "dayjs"; +import duration from "dayjs/plugin/duration"; +import utc from "dayjs/plugin/utc"; +import timezone from "dayjs/plugin/timezone"; +import customParseFormat from "dayjs/plugin/customParseFormat"; + +dayjs.extend(utc); +dayjs.extend(timezone); +dayjs.extend(customParseFormat); +dayjs.extend(duration); + +export const MS_PER_SECOND = 1000; +export const MS_PER_MINUTE = 60 * MS_PER_SECOND; +export const MS_PER_HOUR = 60 * MS_PER_MINUTE; +export const MS_PER_DAY = 24 * MS_PER_HOUR; +export const MS_PER_WEEK = MS_PER_DAY * 7; + +export const formatDateWithTz = (timestamp: string, format: string, timezone: string) => { + if (!timestamp) { + return "Unknown time"; + } + const formattedDate = dayjs(timestamp).tz(timezone).format(format); + return formattedDate; +}; + +export const tickDateFormatLookup = (range: string) => { + const tickFormatLookup: Record = { + "1h": "h:mm A", + "24h": "h:mm A", + "7d": "MM/D, h:mm A", + "30d": "ddd. M/D", + }; + const format = tickFormatLookup[range]; + if (format === undefined) { + return ""; + } + return format; +}; + +export const tooltipDateFormatLookup = (range: string) => { + const dateFormatLookup: Record = { + "1h": "ddd. MMMM D, YYYY, hh:mm A", + "24h": "ddd. MMMM D, YYYY, hh:mm A", + "7d": "ddd. MMMM D, YYYY, hh:mm A", + "30d": "ddd. MMMM D, YYYY", + }; + const format = dateFormatLookup[range]; + if (format === undefined) { + return ""; + } + return format; +}; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index d7c0d1cda..0610d229c 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -1,4 +1,27 @@ { + "common": { + "appName": "Checkmate", + "monitoringAgentName": "Capture", + "buttons": { + "toggleTheme": "Toggles light & dark" + }, + "toasts": { + "networkError": "Network error", + "checkConnection": "Please check your connection", + "unknownError": "Unknown error" + }, + "table": { + "headers": { + "actions": "Actions", + "active": "Active", + "message": "Message", + "monitor": "Monitor", + "name": "Name", + "status": "Status", + "type": "Type" + } + } + }, "submit": "Submit", "title": "Title", "distributedStatusHeaderText": "Real-time, real-device coverage", @@ -126,7 +149,31 @@ "add": "Add", "monitors": "monitors", "pages": { + "common": { + "monitors": { + "status": { + "up": "up", + "down": "down", + "paused": "paused", + "initializing": "initializing" + }, + "actions": { + "openSite": "Open site", + "details": "Details", + "incidents": "Incidents", + "configure": "Configure", + "pause": "Pause", + "resume": "Resume", + "delete": "Delete" + } + } + }, "uptime": { + "table": { + "headers": { + "responseTime": "Response time" + } + }, "fallback": { "title": "An uptime monitor is used to:", "actionButton": "Create a monitor!", @@ -136,6 +183,11 @@ "Monitor HTTP endpoints, pings, containers & ports", "Track historical uptime and reliability trends" ] + }, + "filters": { + "search": { + "placeholder": "Search monitors..." + } } } }, @@ -523,18 +575,7 @@ "game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.", "https": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard." }, - "common": { - "appName": "Checkmate", - "monitoringAgentName": "Capture", - "buttons": { - "toggleTheme": "Toggles light & dark" - }, - "toasts": { - "networkError": "Network error", - "checkConnection": "Please check your connection", - "unknownError": "Unknown error" - } - }, + "auth": { "common": { "navigation": { From 61f3aa1d1c2de1bd72314517cab7b4a0c4dda251 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sat, 24 Jan 2026 00:29:19 +0000 Subject: [PATCH 356/400] reenable sorting --- .../Components/v2/design-elements/Table.tsx | 2 +- .../Components/UptimeMonitorsTable.tsx | 79 ++++++++----------- client/src/Pages/Uptime/Monitors/index.tsx | 15 ++-- 3 files changed, 41 insertions(+), 55 deletions(-) diff --git a/client/src/Components/v2/design-elements/Table.tsx b/client/src/Components/v2/design-elements/Table.tsx index 39f465b79..c87fa41b8 100644 --- a/client/src/Components/v2/design-elements/Table.tsx +++ b/client/src/Components/v2/design-elements/Table.tsx @@ -24,7 +24,7 @@ import { } from "lucide-react"; import TablePagination from "@mui/material/TablePagination"; -import type { TablePaginationProps, TablePaginationOwnProps } from "@mui/material/TablePagination"; +import type { TablePaginationOwnProps } from "@mui/material/TablePagination"; import { useTranslation } from "react-i18next"; import { useState, Fragment } from "react"; diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx index 978d1d45d..21e014688 100644 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx +++ b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx @@ -15,20 +15,19 @@ import { useTranslation } from "react-i18next"; import { useTheme } from "@mui/material/styles"; import { useNavigate } from "react-router-dom"; import { usePost } from "@/Hooks/UseApi"; +import { useSelector } from "react-redux"; -import type { Check } from "@/Types/Check"; import type { Monitor } from "@/Types/Monitor"; import type { ActionMenuItem } from "@/Components/v2/actions-menu"; -// import type { ChartType } from "@/features/uiSlice"; +import type { RootState } from "@/Types/state"; export const MonitorTable = ({ monitors, refetch, - // setSelectedMonitor, - // sortField, - // setSortField, - // sortOrder, - // setSortOrder, + sortField, + setSortField, + sortOrder, + setSortOrder, count, page, setPage, @@ -37,11 +36,10 @@ export const MonitorTable = ({ }: { monitors: Monitor[]; refetch: Function; - // setSelectedMonitor: Function; - // sortField: string; - // setSortField: (field: string) => void; - // sortOrder: "asc" | "desc"; - // setSortOrder: (order: "asc" | "desc") => void; + sortField: string; + setSortField: (field: string) => void; + sortOrder: "asc" | "desc"; + setSortOrder: (order: "asc" | "desc") => void; count: number; page: number; setPage: (page: number) => void; @@ -51,40 +49,25 @@ export const MonitorTable = ({ const { t } = useTranslation(); const theme = useTheme(); const navigate = useNavigate(); - const chartType = "histogram"; + const chartType = useSelector((state: RootState) => state.ui?.chartType ?? "histogram"); const { post, // loading: isPosting, // error: postError, } = usePost(); - // const handlePageChange = ( - // _e: React.MouseEvent | null, - // newPage: number - // ) => { - // setPage(newPage); - // }; - - // const handleRowsPerPageChange = ( - // e: React.ChangeEvent - // ) => { - // const value = Number(e.target.value); - // setPage(0); - // setRowsPerPage(value); - // }; - - // const handleSort = (e: any, field: string) => { - // e.preventDefault(); - // e.stopPropagation(); - // if (sortField === field) { - // const newOrder = sortOrder === "asc" ? "desc" : "asc"; - // setSortOrder(newOrder); - // } else { - // setSortField(field); - // setSortOrder("asc"); - // } - // refetch(); - // }; + const handleSort = (e: any, field: string) => { + e.preventDefault(); + e.stopPropagation(); + if (sortField === field) { + const newOrder = sortOrder === "asc" ? "desc" : "asc"; + setSortOrder(newOrder); + } else { + setSortField(field); + setSortOrder("asc"); + } + refetch(); + }; const getActions = (monitor: Monitor): ActionMenuItem[] => { return [ @@ -158,13 +141,13 @@ export const MonitorTable = ({ display="inline-flex" justifyContent="center" > - {/* {isActive ? ( + {isActive ? ( sortOrder === "asc" ? ( ) : ( ) - ) : null} */} + ) : null} ); const headers: Header[] = [ @@ -175,11 +158,11 @@ export const MonitorTable = ({ gap={theme.spacing(4)} direction={"row"} alignItems={"center"} - // onClick={(e) => handleSort(e, "name")} + onClick={(e) => handleSort(e, "name")} sx={{ cursor: "pointer" }} > {t("common.table.headers.name")} - {/* {renderSortIcon(sortField === "name")} */} + {renderSortIcon(sortField === "name")} ), @@ -195,11 +178,11 @@ export const MonitorTable = ({ direction={"row"} justifyContent={"center"} alignItems={"center"} - // onClick={(e) => handleSort(e, "status")} + onClick={(e) => handleSort(e, "status")} sx={{ cursor: "pointer" }} > {t("common.table.headers.status")} - {/* {renderSortIcon(sortField === "status")} */} + {renderSortIcon(sortField === "status")} ), render: (row) => { @@ -230,11 +213,11 @@ export const MonitorTable = ({ direction={"row"} justifyContent={"center"} alignItems={"center"} - // onClick={(e) => handleSort(e, "type")} + onClick={(e) => handleSort(e, "type")} sx={{ cursor: "pointer" }} > {t("common.table.headers.type")} - {/* {renderSortIcon(sortField === "type")} */} + {renderSortIcon(sortField === "type")} ), render: (row) => { diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index 255a50874..548bfe03c 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -36,9 +36,8 @@ const UptimeMonitorsPage = () => { const [selectedState, setSelectedState] = useState(""); const [page, setPage] = useState(0); const [search, setSearch] = useState(""); - const [sort, setSort] = useState<{ field: string; order: "asc" | "desc" } | undefined>( - undefined - ); + const [sortField, setSortField] = useState(""); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); // Convert filter selections to API filter values // Status: "up" -> true, "down" -> false @@ -62,7 +61,7 @@ const UptimeMonitorsPage = () => { [toFilterActive, "isActive"], ]); const activeFilter = [...filterLookup].find(([key]) => key !== undefined); - const field = activeFilter?.[1] || (search ? "name" : sort?.field); + const field = activeFilter?.[1] || (search ? "name" : sortField || undefined); const filter = activeFilter?.[0] || search; // Default to all types when none selected @@ -78,9 +77,9 @@ const UptimeMonitorsPage = () => { if (rowsPerPage) params.append("rowsPerPage", String(rowsPerPage)); if (filter) params.append("filter", filter); if (field) params.append("field", field); - if (sort?.order) params.append("order", sort.order); + if (sortOrder) params.append("order", sortOrder); return `/monitors/team/with-checks?${params.toString()}`; - }, [effectiveTypes, page, rowsPerPage, filter, field, sort?.order]); + }, [effectiveTypes, page, rowsPerPage, filter, field, sortOrder]); // Data fetching const { @@ -155,6 +154,10 @@ const UptimeMonitorsPage = () => { page={page} setPage={setPage} rowsPerPage={rowsPerPage} + sortField={sortField} + setSortField={setSortField} + sortOrder={sortOrder} + setSortOrder={setSortOrder} setRowsPerPage={(e: any) => { dispatch( setRowsPerPage({ From 99046265265d3ef7f0133c42132dc9ff83620e44 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sat, 24 Jan 2026 00:51:13 +0000 Subject: [PATCH 357/400] chart type option --- client/src/Pages/Settings/DummyChart.tsx | 67 ++++++++++++++++++++++++ client/src/Pages/Settings/SettingsUI.jsx | 20 ++++++- client/src/Pages/Settings/index.jsx | 10 +++- client/src/Types/Check.ts | 1 - client/src/locales/en.json | 3 ++ 5 files changed, 97 insertions(+), 4 deletions(-) create mode 100644 client/src/Pages/Settings/DummyChart.tsx diff --git a/client/src/Pages/Settings/DummyChart.tsx b/client/src/Pages/Settings/DummyChart.tsx new file mode 100644 index 000000000..6996af9c5 --- /dev/null +++ b/client/src/Pages/Settings/DummyChart.tsx @@ -0,0 +1,67 @@ +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import { HeatmapResponseTime, HistogramResponseTime } from "@/Components/v2/common"; +import type { Check } from "@/Types/Check"; + +interface DummyChartProps { + chartType: string; +} + +const generateDummyChecks = (): Check[] => { + const checks: Check[] = []; + for (let i = 0; i < 25; i++) { + const isUp = Math.random() > 0.1; + checks.push({ + id: `dummy-${i}`, + metadata: { + monitorId: "dummy-monitor", + teamId: "dummy-team", + type: "http", + }, + status: isUp, + statusCode: isUp ? 200 : 500, + responseTime: Math.floor(Math.random() * 80) + 20, + message: "", + ack: false, + expiry: new Date(Date.now() + 86400000).toISOString(), + createdAt: new Date(Date.now() - i * 60000).toISOString(), + updatedAt: new Date(Date.now() - i * 60000).toISOString(), + }); + } + return checks; +}; + +const dummyChecks = generateDummyChecks(); + +const DummyChart = ({ chartType }: DummyChartProps) => { + const theme = useTheme(); + + return ( + + + Preview + + {chartType === "histogram" ? ( + + ) : ( + + )} + + ); +}; + +export default DummyChart; diff --git a/client/src/Pages/Settings/SettingsUI.jsx b/client/src/Pages/Settings/SettingsUI.jsx index 7129e775b..ef9f44eaa 100644 --- a/client/src/Pages/Settings/SettingsUI.jsx +++ b/client/src/Pages/Settings/SettingsUI.jsx @@ -3,16 +3,20 @@ import Stack from "@mui/material/Stack"; import Typography from "@mui/material/Typography"; import ConfigBox from "@/Components/v1/ConfigBox/index.jsx"; import Select from "@/Components/v1/Inputs/Select/index.jsx"; +import DummyChart from "./DummyChart"; +import { ThemeProvider } from "@mui/material/styles"; +import { lightTheme, darkTheme } from "@/Utils/Theme/v2Theme"; // Utils import { useTheme } from "@emotion/react"; import { PropTypes } from "prop-types"; import { useTranslation } from "react-i18next"; -const SettingsUI = ({ HEADING_SX, handleChange, mode, language }) => { +const SettingsUI = ({ HEADING_SX, handleChange, mode, language, chartType }) => { const { t, i18n } = useTranslation(); const theme = useTheme(); const languages = Object.keys(i18n.options.resources || {}); + const v2Theme = mode === "dark" ? darkTheme : lightTheme; return ( @@ -44,6 +48,19 @@ const SettingsUI = ({ HEADING_SX, handleChange, mode, language }) => { onChange={handleChange} items={languages.map((lang) => ({ _id: lang, name: lang.toUpperCase() }))} > + + + + ); @@ -54,6 +71,7 @@ SettingsUI.propTypes = { handleChange: PropTypes.func, mode: PropTypes.string, language: PropTypes.string, + chartType: PropTypes.string, }; export default SettingsUI; diff --git a/client/src/Pages/Settings/index.jsx b/client/src/Pages/Settings/index.jsx index 621640d8a..3f62f4ea4 100644 --- a/client/src/Pages/Settings/index.jsx +++ b/client/src/Pages/Settings/index.jsx @@ -17,7 +17,7 @@ import { useState } from "react"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; import { useSelector, useDispatch } from "react-redux"; -import { setTimezone, setMode, setLanguage, setShowURL } from "@/Features/UI/uiSlice.js"; +import { setTimezone, setMode, setLanguage, setShowURL, setChartType } from "@/Features/UI/uiSlice.js"; import SettingsStats from "./SettingsStats.jsx"; import { useFetchSettings, useSaveSettings } from "@/Hooks/settingsHooks.js"; @@ -33,7 +33,7 @@ const BREADCRUMBS = [{ name: `Settings`, path: "/settings" }]; const Settings = () => { // Redux state - const { mode, language = "en", timezone, showURL } = useSelector((state) => state.ui); + const { mode, language = "en", timezone, showURL, chartType = "histogram" } = useSelector((state) => state.ui); // Local state const [settingsData, setSettingsData] = useState({}); @@ -112,6 +112,11 @@ const Settings = () => { return; } + if (name === "chartType") { + dispatch(setChartType(value)); + return; + } + if (name === "deleteStats") { await deleteMonitorStats(); return; @@ -209,6 +214,7 @@ const Settings = () => { handleChange={handleChange} mode={mode} language={language} + chartType={chartType} /> Date: Sat, 24 Jan 2026 01:08:44 +0000 Subject: [PATCH 358/400] stacking fields --- client/src/Components/v2/monitors/FilterControls.tsx | 4 +++- client/src/Pages/Uptime/Monitors/index.tsx | 5 +++-- client/src/locales/en.json | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/client/src/Components/v2/monitors/FilterControls.tsx b/client/src/Components/v2/monitors/FilterControls.tsx index 5d68bf49a..6ee29bbd9 100644 --- a/client/src/Components/v2/monitors/FilterControls.tsx +++ b/client/src/Components/v2/monitors/FilterControls.tsx @@ -1,6 +1,7 @@ import Stack from "@mui/material/Stack"; import { Select, Button } from "@/Components/v2/inputs"; import MenuItem from "@mui/material/MenuItem"; +import useMediaQuery from "@mui/material/useMediaQuery"; import type { MonitorType } from "@/Types/Monitor"; import { Typography, useTheme } from "@mui/material"; @@ -27,11 +28,12 @@ export const FilterControls = ({ onClearFilters: () => void; }) => { const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); const isFilterActive = selectedTypes.length > 0 || selectedStatus !== "" || selectedState !== ""; return (
{ + navigate(`/checks/${row.id}`); + }} + /> + + + ); +}; diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx index 79758041c..85207e195 100644 --- a/client/src/Pages/Uptime/Details/index.tsx +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -7,6 +7,7 @@ import { HistogramDetails, } from "@/Components/v2/monitors"; import { TrendingUp, AlertTriangle } from "lucide-react"; +import { ChecksTable } from "@/Pages/Uptime/Details/Components/ChecksTable"; import { useTheme } from "@mui/material/styles"; import { useIsAdmin } from "@/Hooks/useIsAdmin"; @@ -98,7 +99,7 @@ const UptimeDetailsPage = () => { data: checksData, isLoading: checksIsLoading, error: checksError, - } = useGet(checksUrl); + } = useGet(checksUrl, {}, { keepPreviousData: true }); const checks = checksData?.checks ?? []; const checksCount = checksData?.checksCount ?? 0; @@ -109,6 +110,7 @@ const UptimeDetailsPage = () => { path="uptime" monitor={monitor} isAdmin={isAdmin} + refetch={refetchMonitor} /> { checks={monitorData?.groupedChecks || []} range={dateRange} /> + ); }; From 64de01437e19b5ee563b13b6a93b1040ccf11dd7 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 02:48:56 +0000 Subject: [PATCH 376/400] remove toast styling --- client/src/Hooks/UseToast.ts | 1 + client/src/index.css | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/client/src/Hooks/UseToast.ts b/client/src/Hooks/UseToast.ts index 2ee275253..8823aa675 100644 --- a/client/src/Hooks/UseToast.ts +++ b/client/src/Hooks/UseToast.ts @@ -1,4 +1,5 @@ import { toast, type ToastOptions } from "react-toastify"; + export const useToast = () => { const showToast = (message: string, options?: ToastOptions) => { toast.dismiss(); diff --git a/client/src/index.css b/client/src/index.css index 8b2a1cfce..48737d4bb 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -50,7 +50,8 @@ html { cursor: not-allowed; } -.Toastify__toast-container { +/* .Toastify__toast-container { + min-width: 300px; width: auto; } .Toastify__toast-body .alert { @@ -68,7 +69,7 @@ html { } .Toastify [class*="Toastify__toast-theme"] { background-color: transparent; -} +} */ .MuiTouchRipple-root { display: none; From 174665e70a4b976fdef9a06bc5dbf006503d102d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 02:55:52 +0000 Subject: [PATCH 377/400] remove unused --- .../v2/monitors/charts/HistogramDetails.tsx | 1 - .../v2/monitors/charts/HistogramStatus.tsx | 2 +- .../Details/Components/ChartBoxes/index.jsx | 160 ------------- .../Components/ChartBoxes/skeleton.jsx | 30 --- .../Components/Charts/CustomLabels.jsx | 42 ---- .../Components/Charts/DownBarChart.jsx | 90 ------- .../Components/Charts/ResponseGaugeChart.jsx | 118 ---------- .../Components/Charts/ResponseTimeChart.jsx | 36 --- .../Charts/ResponseTimeChartSkeleton.jsx | 12 - .../Details/Components/Charts/UpBarChart.jsx | 107 --------- .../Uptime/Details/Components/ChecksTable.tsx | 4 +- .../Components/ResponseTable/index.jsx | 222 ------------------ .../Components/ResponseTable/skeleton.jsx | 13 - .../Components/UptimeStatusBoxes/index.jsx | 84 ------- client/src/locales/en.json | 8 + 15 files changed, 11 insertions(+), 918 deletions(-) delete mode 100644 client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx delete mode 100644 client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx delete mode 100644 client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx delete mode 100644 client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx delete mode 100644 client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx delete mode 100644 client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx delete mode 100644 client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx delete mode 100644 client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx delete mode 100644 client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx delete mode 100644 client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx delete mode 100644 client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx diff --git a/client/src/Components/v2/monitors/charts/HistogramDetails.tsx b/client/src/Components/v2/monitors/charts/HistogramDetails.tsx index f3ca2acda..63b81cb71 100644 --- a/client/src/Components/v2/monitors/charts/HistogramDetails.tsx +++ b/client/src/Components/v2/monitors/charts/HistogramDetails.tsx @@ -84,7 +84,6 @@ export const HistogramDetails = ({ checks: GroupedCheck[]; range: string; }) => { - console.log(range); const theme = useTheme(); const uiTimezone = useSelector((state: RootState) => state.ui.timezone); return ( diff --git a/client/src/Components/v2/monitors/charts/HistogramStatus.tsx b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx index 2805013eb..a41c1f3a0 100644 --- a/client/src/Components/v2/monitors/charts/HistogramStatus.tsx +++ b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx @@ -75,7 +75,7 @@ export const HistogramStatus = ({ if (!active || !payload?.length) return null; const d = payload[0]?.payload; const avg = d?.avgResponseTime ?? 0; - const titleText = t("common.charts.labels.averageRepsonseTime"); + const titleText = t("common.charts.labels.averageResponseTime"); const fmt = range === "30d" ? "MMM D, YYYY" : "ddd, MMM D, YYYY, h:mm A"; let dateLabel = ""; if (d?._id) { diff --git a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx b/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx deleted file mode 100644 index 73d241dce..000000000 --- a/client/src/Pages/Uptime/Details/Components/ChartBoxes/index.jsx +++ /dev/null @@ -1,160 +0,0 @@ -// Components -import { Stack, Typography, Box } from "@mui/material"; -import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; -import Icon from "@/Components/v1/Icon"; -import UpBarChart from "../Charts/UpBarChart.jsx"; -import DownBarChart from "../Charts/DownBarChart.jsx"; -import ResponseGaugeChart from "../Charts/ResponseGaugeChart.jsx"; -import SkeletonLayout from "./skeleton.jsx"; -// Utils -import { formatDateWithTz } from "../../../../../Utils/timeUtilsLegacy.js"; -import PropTypes from "prop-types"; -import { useTheme } from "@emotion/react"; -import { useState } from "react"; - -const ChartBoxes = ({ - isLoading = false, - monitorData, - dateRange, - uiTimezone, - dateFormat, -}) => { - // Local state - const [hoveredUptimeData, setHoveredUptimeData] = useState(null); - const [hoveredIncidentsData, setHoveredIncidentsData] = useState(null); - const theme = useTheme(); - if (isLoading) { - return ; - } - - const noIncidentsMessage = "Great. No Incidents, yet!"; - - return ( - - - } - header="Uptime" - isEmpty={monitorData?.groupedUpChecks?.length === 0} - > - - - Total Checks - - {hoveredUptimeData !== null - ? hoveredUptimeData.totalChecks - : (monitorData?.groupedUpChecks?.reduce((count, checkGroup) => { - return count + checkGroup.totalChecks; - }, 0) ?? 0)} - - {hoveredUptimeData !== null && hoveredUptimeData.time !== null && ( - - {formatDateWithTz(hoveredUptimeData._id, dateFormat, uiTimezone)} - - )} - - - - {hoveredUptimeData !== null ? "Avg Response Time" : "Uptime Percentage"} - - - {hoveredUptimeData !== null - ? Math.floor(hoveredUptimeData?.originalAvgResponseTime ?? 0) - : Math.floor((monitorData?.groupedUptimePercentage ?? 0) * 100)} - - {hoveredUptimeData !== null ? " ms" : " %"} - - - - - - - - } - header="Incidents" - noDataMessage={noIncidentsMessage} - isEmpty={monitorData?.groupedDownChecks?.length === 0} - > - - - - {hoveredIncidentsData !== null - ? hoveredIncidentsData.totalChecks - : (monitorData?.groupedDownChecks?.reduce((count, checkGroup) => { - return count + checkGroup.totalChecks; - }, 0) ?? 0)} - - {hoveredIncidentsData !== null && hoveredIncidentsData.time !== null && ( - - {formatDateWithTz(hoveredIncidentsData._id, dateFormat, uiTimezone)} - - )} - - - - - - } - header="Average Response Time" - > - - - - ); -}; - -export default ChartBoxes; - -ChartBoxes.propTypes = { - isLoading: PropTypes.bool, - monitorData: PropTypes.object, - dateRange: PropTypes.string.isRequired, - uiTimezone: PropTypes.string.isRequired, - dateFormat: PropTypes.string.isRequired, - hoveredUptimeData: PropTypes.object, - setHoveredUptimeData: PropTypes.func, - hoveredIncidentsData: PropTypes.object, - setHoveredIncidentsData: PropTypes.func, -}; diff --git a/client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx b/client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx deleted file mode 100644 index 750b3b20a..000000000 --- a/client/src/Pages/Uptime/Details/Components/ChartBoxes/skeleton.jsx +++ /dev/null @@ -1,30 +0,0 @@ -import { Skeleton, Stack } from "@mui/material"; -import { useTheme } from "@emotion/react"; - -const SkeletonLayout = () => { - const theme = useTheme(); - return ( - - - - - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx b/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx deleted file mode 100644 index ffdaa5bbb..000000000 --- a/client/src/Pages/Uptime/Details/Components/Charts/CustomLabels.jsx +++ /dev/null @@ -1,42 +0,0 @@ -import PropTypes from "prop-types"; -import { useSelector } from "react-redux"; -import { formatDateWithTz } from "../../../../../Utils/timeUtilsLegacy.js"; - -const CustomLabels = ({ x, width, height, firstDataPoint, lastDataPoint, type }) => { - const uiTimezone = useSelector((state) => state.ui.timezone); - const dateFormat = type === "day" ? "MMM D, h:mm A" : "MMM D"; - - return ( - <> - - {formatDateWithTz(firstDataPoint._id, dateFormat, uiTimezone)} - - - {formatDateWithTz(lastDataPoint._id, dateFormat, uiTimezone)} - - - ); -}; - -CustomLabels.propTypes = { - x: PropTypes.number.isRequired, - width: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - height: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), - firstDataPoint: PropTypes.object, - lastDataPoint: PropTypes.object, - type: PropTypes.string.isRequired, -}; - -export default CustomLabels; diff --git a/client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx deleted file mode 100644 index c5e08ecca..000000000 --- a/client/src/Pages/Uptime/Details/Components/Charts/DownBarChart.jsx +++ /dev/null @@ -1,90 +0,0 @@ -import { memo, useState } from "react"; -import { useTheme } from "@mui/material"; -import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts"; -import PropTypes from "prop-types"; -import CustomLabels from "./CustomLabels.jsx"; - -const DownBarChart = memo(({ groupedDownChecks = [], type, onBarHover }) => { - const theme = useTheme(); - - const [chartHovered, setChartHovered] = useState(false); - const [hoveredBarIndex, setHoveredBarIndex] = useState(null); - - return ( - - { - setChartHovered(true); - onBarHover({ time: null, totalChecks: 0 }); - }} - onMouseLeave={() => { - setChartHovered(false); - setHoveredBarIndex(null); - onBarHover(null); - }} - > - - } - /> - - {groupedDownChecks?.map((entry, index) => { - return ( - { - setHoveredBarIndex(index); - onBarHover(entry); - }} - onMouseLeave={() => { - setHoveredBarIndex(null); - onBarHover({ time: null, totalChecks: 0 }); - }} - /> - ); - })} - - - - ); -}); - -DownBarChart.displayName = "DownBarChart"; -DownBarChart.propTypes = { - monitor: PropTypes.shape({ - groupedDownChecks: PropTypes.arrayOf(PropTypes.object), - }), - type: PropTypes.string, - onBarHover: PropTypes.func, -}; -export default DownBarChart; diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx deleted file mode 100644 index 084a711c3..000000000 --- a/client/src/Pages/Uptime/Details/Components/Charts/ResponseGaugeChart.jsx +++ /dev/null @@ -1,118 +0,0 @@ -import PropTypes from "prop-types"; -import { useTheme } from "@mui/material"; -import { ResponsiveContainer, RadialBarChart, RadialBar, Cell } from "recharts"; -import { useTranslation } from "react-i18next"; - -const ResponseGaugeChart = ({ avgResponseTime }) => { - const theme = useTheme(); - const { t } = useTranslation(); - let max = 1000; // max ms - - const data = [ - { response: max, fill: "transparent", background: false }, - { response: avgResponseTime, background: true }, - ]; - let responseTime = Math.floor(avgResponseTime); - let responseProps = - responseTime <= 200 - ? { - category: "Excellent", - main: theme.palette.success.main, - bg: theme.palette.success.lowContrast, - } - : responseTime <= 500 - ? { - category: "Fair", - main: theme.palette.success.main, - bg: theme.palette.success.lowContrast, - } - : responseTime <= 600 - ? { - category: "Acceptable", - main: theme.palette.warning.main, - bg: theme.palette.warning.lowContrast, - } - : { - category: "Poor", - main: theme.palette.error.main, - bg: theme.palette.error.lowContrast, - }; - - return ( - - - - {t("low")} - - - {t("high")} - - - {responseProps.category} - - - {responseTime} ms - - - - - - - - ); -}; - -ResponseGaugeChart.propTypes = { - avgResponseTime: PropTypes.number.isRequired, -}; - -export default ResponseGaugeChart; diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx deleted file mode 100644 index 7e7a940e0..000000000 --- a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChart.jsx +++ /dev/null @@ -1,36 +0,0 @@ -import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; -import MonitorDetailsAreaChart from "@/Components/v1/Charts/MonitorDetailsAreaChart/index.jsx"; -import Icon from "@/Components/v1/Icon"; -import SkeletonLayout from "./ResponseTimeChartSkeleton.jsx"; -import PropTypes from "prop-types"; - -const ResponseTImeChart = ({ isLoading = false, groupedChecks = [], dateRange }) => { - if (isLoading) { - return ; - } - - return ( - - } - header="Response Times" - > - - - ); -}; - -ResponseTImeChart.propTypes = { - isLoading: PropTypes.bool, - groupedChecks: PropTypes.array, - dateRange: PropTypes.string, -}; - -export default ResponseTImeChart; diff --git a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx b/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx deleted file mode 100644 index a70757e36..000000000 --- a/client/src/Pages/Uptime/Details/Components/Charts/ResponseTimeChartSkeleton.jsx +++ /dev/null @@ -1,12 +0,0 @@ -import { Skeleton } from "@mui/material"; -const ResponseTimeChartSkeleton = () => { - return ( - - ); -}; - -export default ResponseTimeChartSkeleton; diff --git a/client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx b/client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx deleted file mode 100644 index 8f1af8912..000000000 --- a/client/src/Pages/Uptime/Details/Components/Charts/UpBarChart.jsx +++ /dev/null @@ -1,107 +0,0 @@ -import { memo, useState } from "react"; -import { useTheme } from "@mui/material"; -import { ResponsiveContainer, BarChart, XAxis, Bar, Cell } from "recharts"; -import PropTypes from "prop-types"; -import CustomLabels from "./CustomLabels.jsx"; - -const getThemeColor = (responseTime) => { - if (responseTime < 200) { - return "success"; - } else if (responseTime < 300) { - return "warning"; - } else { - return "error"; - } -}; - -const UpBarChart = memo(({ groupedUpChecks = [], type, onBarHover }) => { - const theme = useTheme(); - const [chartHovered, setChartHovered] = useState(false); - const [hoveredBarIndex, setHoveredBarIndex] = useState(null); - - return ( - - { - setChartHovered(true); - onBarHover({ time: null, totalChecks: 0, originalAvgResponseTime: 0 }); - }} - onMouseLeave={() => { - setChartHovered(false); - setHoveredBarIndex(null); - onBarHover(null); - }} - > - - } - /> - - {groupedUpChecks?.map((entry, index) => { - const themeColor = getThemeColor(entry.originalAvgResponseTime); - return ( - { - setHoveredBarIndex(index); - onBarHover(entry); - }} - onMouseLeave={() => { - setHoveredBarIndex(null); - onBarHover({ - time: null, - totalChecks: 0, - groupUptimePercentage: 0, - }); - }} - /> - ); - })} - - - - ); -}); - -// Add display name for the component -UpBarChart.displayName = "UpBarChart"; - -// Validate props using PropTypes -UpBarChart.propTypes = { - monitor: PropTypes.shape({ - groupedUpChecks: PropTypes.array, - }), - type: PropTypes.string, - onBarHover: PropTypes.func, -}; -export default UpBarChart; diff --git a/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx b/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx index 2d5080e77..2cfdc1f15 100644 --- a/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx +++ b/client/src/Pages/Uptime/Details/Components/ChecksTable.tsx @@ -20,7 +20,7 @@ const getHeaders = (t: Function, uiTimezone: string) => { }, { id: "date", - content: t("checks.table.headers.dateTime"), + content: t("pages.checks.table.headers.dateTime"), render: (row) => { return formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone); }, @@ -34,7 +34,7 @@ const getHeaders = (t: Function, uiTimezone: string) => { }, { id: "statusCode", - content: t("checks.table.headers.statusCode"), + content: t("pages.checks.table.headers.statusCode"), render: (row) => { return row.statusCode || "N/A"; }, diff --git a/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx b/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx deleted file mode 100644 index b115f67b0..000000000 --- a/client/src/Pages/Uptime/Details/Components/ResponseTable/index.jsx +++ /dev/null @@ -1,222 +0,0 @@ -import ChartBox from "@/Components/v1/Charts/ChartBox/index.jsx"; -import PropTypes from "prop-types"; -import Icon from "@/Components/v1/Icon"; -import Table from "@/Components/v1/Table/index.jsx"; -import TablePagination from "@/Components/v1/Table/TablePagination/index.jsx"; -import { StatusLabel } from "@/Components/v1/Label/index.jsx"; -import { useTranslation } from "react-i18next"; -import { formatDateWithTz } from "../../../../../Utils/timeUtilsLegacy.js"; -import SkeletonLayout from "./skeleton.jsx"; -import Tooltip from "@mui/material/Tooltip"; -import Box from "@mui/material/Box"; -import Typography from "@mui/material/Typography"; -import Stack from "@mui/material/Stack"; -import { lighten, useTheme } from "@mui/material"; - -/** - * Creates tooltip content with detailed timing breakdown - * Following the pattern from IncidentTable's GetTooltip function - * @param {Object} timings - Timing object (guaranteed to have phases by caller) - * @param {Object} theme - MUI theme object - * @param {Function} t - Translation function - * @returns {JSX.Element} Tooltip content - */ -const GetTooltip = (timings, theme, t) => { - const phases = timings.phases; - const timingDetails = [ - { label: t("dnsLookup"), value: phases.dns }, - { label: t("tcpConnection"), value: phases.tcp }, - { label: t("tlsHandshake"), value: phases.tls }, - { label: t("waitTime"), value: phases.wait }, - { label: t("timeToFirstByte"), value: phases.firstByte }, - { label: t("download"), value: phases.download }, - { label: t("total"), value: phases.total }, - ].filter((item) => item.value > 0); - - return ( - - - {t("responseTimeBreakdown")} - - {timingDetails.map((detail, index, array) => ( - - - {detail.label}: - - - {Math.round(detail.value)} ms - - - ))} - - ); -}; - -const ResponseTable = ({ - isLoading = false, - checks = [], - checksCount, - uiTimezone, - page, - setPage, - rowsPerPage, - setRowsPerPage, -}) => { - const { t } = useTranslation(); - const theme = useTheme(); - - if (isLoading) { - return ; - } - - const headers = [ - { - id: "status", - content: t("status"), - render: (row) => { - const status = row.status === true ? "up" : "down"; - - return ( - - ); - }, - }, - { - id: "date", - content: t("date&Time"), - render: (row) => - formatDateWithTz(row.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone), - }, - { - id: "statusCode", - content: t("statusCode"), - render: (row) => (row.statusCode ? row.statusCode : "N/A"), - }, - { - id: "message", - content: t("message"), - render: (row) => row.message, - }, - { - id: "responseTime", - content: t("responseTime"), - render: (row) => { - const hasTimings = row.timings && row.timings.phases; - const responseTime = row.responseTime; - const responseTimeDisplay = - responseTime !== null && responseTime !== undefined - ? `${Math.round(responseTime)} ms` - : "N/A"; - - if (!hasTimings) { - return responseTimeDisplay; - } - - return ( - - - {responseTimeDisplay} - - - ); - }, - }, - ]; - - return ( - - } - header="Response Times" - height="100%" - > -
- - - ); -}; - -ResponseTable.propTypes = { - isLoading: PropTypes.bool, - checks: PropTypes.array, - checksCount: PropTypes.number, - uiTimezone: PropTypes.string.isRequired, - page: PropTypes.number.isRequired, - setPage: PropTypes.func.isRequired, - rowsPerPage: PropTypes.number.isRequired, - setRowsPerPage: PropTypes.func.isRequired, -}; - -export default ResponseTable; diff --git a/client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx b/client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx deleted file mode 100644 index 2665ffaf1..000000000 --- a/client/src/Pages/Uptime/Details/Components/ResponseTable/skeleton.jsx +++ /dev/null @@ -1,13 +0,0 @@ -import { Skeleton } from "@mui/material"; - -const SkeletonLayout = () => { - return ( - - ); -}; - -export default SkeletonLayout; diff --git a/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx b/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx deleted file mode 100644 index 8c05e6c74..000000000 --- a/client/src/Pages/Uptime/Details/Components/UptimeStatusBoxes/index.jsx +++ /dev/null @@ -1,84 +0,0 @@ -import StatusBoxes from "@/Components/v1/StatusBoxes/index.jsx"; -import StatBox from "@/Components/v1/StatBox/index.jsx"; - -import PropTypes from "prop-types"; -import { getHumanReadableDuration } from "../../../../../Utils/timeUtilsLegacy.js"; -import { useTheme } from "@mui/material/styles"; -import { Typography } from "@mui/material"; -import { useMonitorUtils } from "../../../../../Hooks/useMonitorUtils.js"; - -const UptimeStatusBoxes = ({ - isLoading = false, - monitor, - monitorStats, - certificateExpiry, -}) => { - const theme = useTheme(); - const { determineState } = useMonitorUtils(); - - // Determine time since last failure - const timeOfLastFailure = monitorStats?.timeOfLastFailure; - const timeSinceLastFailure = - timeOfLastFailure > 0 - ? Date.now() - timeOfLastFailure - : Date.now() - new Date(monitorStats?.createdAt); - - // Determine time since last check - const timeOfLastCheck = monitorStats?.lastCheckTimestamp; - const timeSinceLastCheck = Date.now() - timeOfLastCheck; - - const streakTime = getHumanReadableDuration(timeSinceLastFailure); - - const lastCheckTime = getHumanReadableDuration(timeSinceLastCheck); - return ( - - - - {lastCheckTime} - {"ago"} - - } - /> - - {monitorStats?.lastResponseTime} - {"ms"} - - } - /> - {monitor?.type === "http" && ( - - {certificateExpiry} - - } - /> - )} - - ); -}; - -UptimeStatusBoxes.propTypes = { - isLoading: PropTypes.bool, - monitor: PropTypes.object, - monitorStats: PropTypes.object, - certificateExpiry: PropTypes.string, -}; - -export default UptimeStatusBoxes; diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 8cba70259..d098e54de 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -208,6 +208,14 @@ "placeholder": "Search monitors..." } } + }, + "checks": { + "table": { + "headers": { + "dateTime": "Date & time", + "statusCode": "Status code" + } + } } }, "distributedUptimeStatusCreateStatusPage": "status page", From 11c95f885d680c86952de924ea78c4f4af6fa146 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 03:19:33 +0000 Subject: [PATCH 378/400] bucket date --- .../src/Components/v2/monitors/MonitorStatBoxes.tsx | 2 +- .../v2/monitors/charts/HistogramDetails.tsx | 2 +- .../v2/monitors/charts/HistogramStatus.tsx | 12 ++++++------ client/src/Pages/Uptime/Details/index.tsx | 6 +++++- client/src/Pages/Uptime/Monitors/index.tsx | 8 ++++++-- client/src/Types/Check.ts | 2 +- .../src/repositories/checks/MongoChecksRepistory.ts | 3 +++ server/src/types/check.ts | 2 +- 8 files changed, 24 insertions(+), 13 deletions(-) diff --git a/client/src/Components/v2/monitors/MonitorStatBoxes.tsx b/client/src/Components/v2/monitors/MonitorStatBoxes.tsx index 4b3a49fe9..65be06f3b 100644 --- a/client/src/Components/v2/monitors/MonitorStatBoxes.tsx +++ b/client/src/Components/v2/monitors/MonitorStatBoxes.tsx @@ -28,7 +28,7 @@ export const MonitorStatBoxes = ({ const timeSinceLastFailure = timeOfLastFailure > 0 ? Date.now() - timeOfLastFailure - : Date.now() - new Date(monitorStats?.createdAt); + : Date.now() - new Date(monitorStats?.createdAt || 0).getTime(); // Determine time since last check const timeOfLastCheck = monitorStats?.lastCheckTimestamp || 0; diff --git a/client/src/Components/v2/monitors/charts/HistogramDetails.tsx b/client/src/Components/v2/monitors/charts/HistogramDetails.tsx index 63b81cb71..8858a9784 100644 --- a/client/src/Components/v2/monitors/charts/HistogramDetails.tsx +++ b/client/src/Components/v2/monitors/charts/HistogramDetails.tsx @@ -131,7 +131,7 @@ export const HistogramDetails = ({ ( { fontSize={11} fill={theme.palette.text.secondary} > - {formatDateWithTz(p1._id, dateFormat, uiTimezone)} + {formatDateWithTz(p1.bucketDate, dateFormat, uiTimezone)} { fontSize={11} fill={theme.palette.text.secondary} > - {formatDateWithTz(p2._id, dateFormat, uiTimezone)} + {formatDateWithTz(p2.bucketDate, dateFormat, uiTimezone)} ); @@ -78,8 +78,8 @@ export const HistogramStatus = ({ const titleText = t("common.charts.labels.averageResponseTime"); const fmt = range === "30d" ? "MMM D, YYYY" : "ddd, MMM D, YYYY, h:mm A"; let dateLabel = ""; - if (d?._id) { - const base = new Date(d._id); + if (d?.bucketDate) { + const base = new Date(d.bucketDate); const midBucket = range === "30d" ? new Date(base.getTime() + 12 * 60 * 60 * 1000) : base; dateLabel = formatDateWithTz(midBucket.toISOString(), fmt, uiTimezone); @@ -151,11 +151,11 @@ export const HistogramStatus = ({ maxBarSize={7} background={{ fill: "transparent" }} > - {checks?.map((groupedCheck) => { + {checks?.map((groupedCheck, index) => { const fillColor = getResponseTimeColor(groupedCheck.avgResponseTime); return ( ); diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx index 85207e195..a148e38fa 100644 --- a/client/src/Pages/Uptime/Details/index.tsx +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -53,7 +53,11 @@ const UptimeDetailsPage = () => { isLoading: monitorIsLoading, error: monitorError, refetch: refetchMonitor, - } = useGet(monitorDetailsUrl); + } = useGet( + monitorDetailsUrl, + {}, + { refreshInterval: 10000, keepPreviousData: true } + ); const monitorData = monitorDetailsData?.monitorData; const monitor = monitorData?.monitor; diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index 5b05817bb..e3e90429a 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -88,7 +88,11 @@ const UptimeMonitorsPage = () => { data: monitors, isLoading: monitorsLoading, error, - } = useGet("/monitors/team?type=http&type=ping&type=port&type=docker"); + } = useGet( + "/monitors/team?type=http&type=ping&type=port&type=docker", + {}, + { keepPreviousData: true } + ); const { data: monitorsWithChecksData, @@ -98,7 +102,7 @@ const UptimeMonitorsPage = () => { } = useGet( monitorsWithChecksUrl, {}, - { refreshInterval: 5000 } + { refreshInterval: 5000, keepPreviousData: true } ); const { monitors: monitorsWithChecks, summary, count } = monitorsWithChecksData ?? {}; diff --git a/client/src/Types/Check.ts b/client/src/Types/Check.ts index 4e4fcc03b..0aa953dbb 100644 --- a/client/src/Types/Check.ts +++ b/client/src/Types/Check.ts @@ -128,7 +128,7 @@ export interface Check { } export interface GroupedCheck { - _id: string; + bucketDate: string; avgResponseTime: number; totalChecks: number; } diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index b9cf2041f..0783c0040 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -483,6 +483,7 @@ class MongoChecksRepository implements IChecksRepository { }, }, { $sort: { _id: 1 } }, + { $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } }, ], groupedUpChecks: [ { $match: { status: true } }, @@ -496,6 +497,7 @@ class MongoChecksRepository implements IChecksRepository { }, }, { $sort: { _id: 1 } }, + { $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } }, ], groupedDownChecks: [ { $match: { status: false } }, @@ -509,6 +511,7 @@ class MongoChecksRepository implements IChecksRepository { }, }, { $sort: { _id: 1 } }, + { $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } }, ], }, }, diff --git a/server/src/types/check.ts b/server/src/types/check.ts index 3195cc06a..ee40ff6dc 100644 --- a/server/src/types/check.ts +++ b/server/src/types/check.ts @@ -157,7 +157,7 @@ export interface HardwareChecksResult { } export interface GroupedCheck { - _id: string; + bucketDate: string; avgResponseTime: number; totalChecks: number; } From 93a5eff562bce81514afa5ea264e9a32e8e4c66f Mon Sep 17 00:00:00 2001 From: karenvicent Date: Mon, 26 Jan 2026 11:40:44 -0500 Subject: [PATCH 379/400] fix maintenance datepicker theme color --- client/src/Pages/Maintenance/CreateMaintenance/index.jsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx b/client/src/Pages/Maintenance/CreateMaintenance/index.jsx index a300cc3b5..af3f34512 100644 --- a/client/src/Pages/Maintenance/CreateMaintenance/index.jsx +++ b/client/src/Pages/Maintenance/CreateMaintenance/index.jsx @@ -228,6 +228,13 @@ const CreateMaintenance = () => { slotProps={{ switchViewButton: { sx: { display: "none" } }, nextIconButton: { sx: { ml: theme.spacing(2) } }, + day: { + sx: { + "&.MuiPickersDay-root.Mui-disabled": { + color: theme.palette.primary.contrastTextTertiary, + }, + }, + }, field: { sx: { width: "fit-content", From d71c7c6ee1c06c82152f583ad239b84e88502a76 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 17:11:09 +0000 Subject: [PATCH 380/400] format --- client/src/Components/v2/design-elements/Icon.tsx | 7 ++++++- client/src/Components/v2/inputs/index.tsx | 5 ++++- client/src/Types/Monitor.ts | 1 - 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/client/src/Components/v2/design-elements/Icon.tsx b/client/src/Components/v2/design-elements/Icon.tsx index bc49ddcc0..b32cd84b2 100644 --- a/client/src/Components/v2/design-elements/Icon.tsx +++ b/client/src/Components/v2/design-elements/Icon.tsx @@ -7,7 +7,12 @@ interface IconProps { } const Icon = ({ icon: Icon, size = 20, strokeWidth = 1.5 }: IconProps) => { - return ; + return ( + + ); }; export default Icon; diff --git a/client/src/Components/v2/inputs/index.tsx b/client/src/Components/v2/inputs/index.tsx index 07e919932..87698a4f0 100644 --- a/client/src/Components/v2/inputs/index.tsx +++ b/client/src/Components/v2/inputs/index.tsx @@ -4,4 +4,7 @@ export { SelectInput as Select } from "./Select"; export { CheckboxInput as Checkbox } from "./Checkbox"; export { AutoCompleteInput as Autocomplete } from "./AutoComplete"; export { TextInput as TextField } from "./TextInput"; -export { ToggleButtonInput as ToggleButton, ToggleButtonGroupInput as ToggleButtonGroup } from "./ToggleButton"; +export { + ToggleButtonInput as ToggleButton, + ToggleButtonGroupInput as ToggleButtonGroup, +} from "./ToggleButton"; diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index cd5245386..a21d17f60 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -48,7 +48,6 @@ export interface MonitorsWithChecksResponse { summary: MonitorsSummary; } - export interface MonitorStats { id: string; monitorId: string; From 967b2b200bdc85f8594989828f33a0ba207b3ff1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 17:15:08 +0000 Subject: [PATCH 381/400] build errors --- .../common/controls/HeaderMonitorControls.tsx | 2 +- .../v2/common/controls/HeaderTimeRange.tsx | 28 ++++++++++++++++--- .../v2/monitors/charts/HistogramStatus.tsx | 3 -- client/src/Pages/Uptime/Details/index.tsx | 17 +++++------ 4 files changed, 32 insertions(+), 18 deletions(-) diff --git a/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx b/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx index fb9161425..17576a8b2 100644 --- a/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx +++ b/client/src/Components/v2/common/controls/HeaderMonitorControls.tsx @@ -66,7 +66,7 @@ export const HeaderMonitorControls = ({ variant="contained" color="secondary" startIcon={} - onClick={(e) => { + onClick={() => { navigate(`/incidents/${monitor?.id}`); }} > diff --git a/client/src/Components/v2/common/controls/HeaderTimeRange.tsx b/client/src/Components/v2/common/controls/HeaderTimeRange.tsx index 772683b43..57b61473a 100644 --- a/client/src/Components/v2/common/controls/HeaderTimeRange.tsx +++ b/client/src/Components/v2/common/controls/HeaderTimeRange.tsx @@ -37,10 +37,30 @@ export const HeaderTimeRange = ({ onChange={handleChange} size="small" > - Recent - Day - Week - Month + + Recent + + + Day + + + Week + + + Month + ); } diff --git a/client/src/Components/v2/monitors/charts/HistogramStatus.tsx b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx index 432f1a591..1c25c339e 100644 --- a/client/src/Components/v2/monitors/charts/HistogramStatus.tsx +++ b/client/src/Components/v2/monitors/charts/HistogramStatus.tsx @@ -8,7 +8,6 @@ import { useTranslation } from "react-i18next"; import { ResponsiveContainer, BarChart, XAxis, Bar, Cell, Tooltip } from "recharts"; import { getResponseTimeColor } from "@/Utils/MonitorUtils"; import type { GroupedCheck } from "@/Types/Check"; -import type { MonitorData } from "@/Types/Monitor"; import type { RootState } from "@/Types/state"; interface XLabelProps { @@ -50,7 +49,6 @@ const XLabel = ({ p1, p2, range }: XLabelProps) => { interface HistogramStatusProps { title: string; icon: React.ReactNode; - monitorData?: MonitorData; checks?: GroupedCheck[]; range: string; } @@ -58,7 +56,6 @@ interface HistogramStatusProps { export const HistogramStatus = ({ title, icon, - monitorData, checks = [], range, }: HistogramStatusProps) => { diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx index a148e38fa..1a6177ddc 100644 --- a/client/src/Pages/Uptime/Details/index.tsx +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -51,7 +51,6 @@ const UptimeDetailsPage = () => { const { data: monitorDetailsData, isLoading: monitorIsLoading, - error: monitorError, refetch: refetchMonitor, } = useGet( monitorDetailsUrl, @@ -99,11 +98,11 @@ const UptimeDetailsPage = () => { return `/checks/${monitorId}?${params.toString()}`; }, [monitorId, monitor?.type, dateRange, page, rowsPerPage]); - const { - data: checksData, - isLoading: checksIsLoading, - error: checksError, - } = useGet(checksUrl, {}, { keepPreviousData: true }); + const { data: checksData, isLoading: checksIsLoading } = useGet( + checksUrl, + {}, + { keepPreviousData: true } + ); const checks = checksData?.checks ?? []; const checksCount = checksData?.checksCount ?? 0; @@ -134,15 +133,13 @@ const UptimeDetailsPage = () => { } - monitorData={monitorData} - checks={monitorData?.groupedUpChecks} + checks={monitorData?.groupedUpChecks ?? []} range={dateRange} /> } - monitorData={monitorData} - checks={monitorData?.groupedDownChecks} + checks={monitorData?.groupedDownChecks ?? []} range={dateRange} /> Date: Mon, 26 Jan 2026 18:24:58 +0000 Subject: [PATCH 382/400] initial commit --- .../v2/monitors/charts/HistogramPageSpeed.tsx | 106 ++++++++ .../charts/HistogramPageSpeedTooltip.tsx | 40 +++ client/src/Components/v2/monitors/index.tsx | 2 + .../Components/PageSpeedMonitorsTable.tsx | 253 ++++++++++++++++++ client/src/Pages/PageSpeed/Monitors/index.tsx | 86 ++++++ .../PageSpeed/Monitors/{index.jsx => old.jsx} | 0 .../Components/UptimeMonitorsTable.tsx | 2 +- client/src/Routes/index.jsx | 15 +- client/src/Types/Settings.ts | 34 +++ client/src/locales/en.json | 12 +- 10 files changed, 544 insertions(+), 6 deletions(-) create mode 100644 client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx create mode 100644 client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx create mode 100644 client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx create mode 100644 client/src/Pages/PageSpeed/Monitors/index.tsx rename client/src/Pages/PageSpeed/Monitors/{index.jsx => old.jsx} (100%) create mode 100644 client/src/Types/Settings.ts diff --git a/client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx b/client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx new file mode 100644 index 000000000..509886434 --- /dev/null +++ b/client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx @@ -0,0 +1,106 @@ +import Stack from "@mui/material/Stack"; +import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts"; +import { HistogramPageSpeedTooltip } from "@/Components/v2/monitors/charts/HistogramPageSpeedTooltip"; + +import { useTheme } from "@mui/material/styles"; +import type { Check } from "@/Types/Check"; +import type { MonitorStatus } from "@/Types/Monitor"; + +import { getStatusColor } from "@/Utils/MonitorUtils"; + +const processData = (checks: Check[]) => { + if (checks.length === 0) return []; + const formattedData: { score: number; date: string }[] = []; + + const calculateScore = (entry: Check) => { + const accessibility = entry?.accessibility || 0; + const bestPractices = entry?.bestPractices || 0; + const performance = entry?.performance || 0; + const seo = entry?.seo || 0; + return Math.floor(((accessibility + bestPractices + performance + seo) / 4) * 100); + }; + + checks.forEach((entry) => { + const score = calculateScore(entry); + const date = entry.createdAt; + formattedData.push({ score, date }); + }); + + return formattedData; +}; + +export const HistogramPageSpeed = ({ + checks, + status, +}: { + checks: Check[]; + status: MonitorStatus; +}) => { + const theme = useTheme(); + const scores = processData(checks ?? []); + const color = getStatusColor(status, theme); + return ( + event.stopPropagation()} + overflow={"hidden"} + sx={{ + cursor: "default", + position: "relative", + }} + > + + + + } + /> + + + + + + + + + + + ); +}; diff --git a/client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx b/client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx new file mode 100644 index 000000000..6fd18251c --- /dev/null +++ b/client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx @@ -0,0 +1,40 @@ +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { formatDateWithTz } from "@/Utils/TimeUtils"; +import type { TooltipProps } from "recharts"; +import { useTheme } from "@mui/material/styles"; +import { useSelector } from "react-redux"; +import type { RootState } from "@/Types/state"; + +export const HistogramPageSpeedTooltip = ({ + payload, +}: TooltipProps) => { + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + const theme = useTheme(); + return ( + + + {formatDateWithTz( + payload?.[0]?.payload?.date ?? 0, + "MMMM D, YYYY, HH:mm A", + uiTimezone + )} + + + Score: {payload?.[0]?.payload?.score} + + + ); +}; diff --git a/client/src/Components/v2/monitors/index.tsx b/client/src/Components/v2/monitors/index.tsx index de38d2cab..ca0aaa15e 100644 --- a/client/src/Components/v2/monitors/index.tsx +++ b/client/src/Components/v2/monitors/index.tsx @@ -3,3 +3,5 @@ export * from "./MonitorStatBoxes"; export * from "./charts/HistogramStatus"; export * from "./charts/RadialAvgResponse"; export * from "./charts/HistogramDetails"; +export * from "./charts/HistogramPageSpeed"; +export * from "./charts/HistogramPageSpeedTooltip"; diff --git a/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx b/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx new file mode 100644 index 000000000..0322948c3 --- /dev/null +++ b/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx @@ -0,0 +1,253 @@ +import Stack from "@mui/material/Stack"; +import Box from "@mui/material/Box"; +import Typography from "@mui/material/Typography"; +import { Table, Pagination, StatusLabel } from "@/Components/v2/design-elements"; +import { HistogramPageSpeed } from "@/Components/v2/monitors"; +import { ArrowDown, ArrowUp } from "lucide-react"; +import type { Header } from "@/Components/v2/design-elements/Table"; +import { ActionsMenu } from "@/Components/v2/actions-menu"; + +import { useTranslation } from "react-i18next"; +import useMediaQuery from "@mui/material/useMediaQuery"; +import { useTheme } from "@mui/material/styles"; +import { useNavigate } from "react-router-dom"; +import { usePatch } from "@/Hooks/UseApi"; + +import type { Check } from "@/Types/Check"; +import type { Monitor, MonitorWithChecks } from "@/Types/Monitor"; +import type { ActionMenuItem } from "@/Components/v2/actions-menu"; + +export const PageSpeedMonitorsTable = ({ + monitors, + refetch, + sortField, + setSortField, + sortOrder, + setSortOrder, + count, + page, + setPage, + rowsPerPage, + setRowsPerPage, +}: { + monitors: MonitorWithChecks[]; + refetch: Function; + sortField: string; + setSortField: (field: string) => void; + sortOrder: "asc" | "desc"; + setSortOrder: (order: "asc" | "desc") => void; + count: number; + page: number; + setPage: (page: number) => void; + rowsPerPage: number; + setRowsPerPage: (rowsPerPage: number) => void; +}) => { + const { t } = useTranslation(); + const theme = useTheme(); + const isSmall = useMediaQuery(theme.breakpoints.down("md")); + const navigate = useNavigate(); + const { + patch, + // loading: isPatching, + // error: postError, + } = usePatch(); + + const handlePageChange = ( + _e: React.MouseEvent | null, + newPage: number + ) => { + setPage(newPage); + }; + + const handleRowsPerPageChange = ( + e: React.ChangeEvent + ) => { + const value = Number(e.target.value); + setPage(0); + setRowsPerPage(value); + }; + + const handleSort = (e: any, field: string) => { + e.preventDefault(); + e.stopPropagation(); + if (sortField === field) { + const newOrder = sortOrder === "asc" ? "desc" : "asc"; + setSortOrder(newOrder); + } else { + setSortField(field); + setSortOrder("asc"); + } + refetch(); + }; + + const getActions = (monitor: Monitor): ActionMenuItem[] => { + return [ + { + id: 1, + label: t("pages.common.monitors.actions.openSite"), + action: () => { + window.open(monitor.url, "_blank", "noreferrer"); + }, + closeMenu: true, + }, + { + id: 2, + label: t("pages.common.monitors.actions.details"), + action: () => { + navigate(`${monitor.id}`); + }, + }, + { + id: 3, + label: t("pages.common.monitors.actions.incidents"), + action: () => { + navigate(`/incidents?monitorId=${monitor.id}`); + }, + }, + { + id: 4, + label: t("pages.common.monitors.actions.configure"), + action: () => { + navigate(`/pagespeed/configure/${monitor.id}`); + }, + }, + // { + // id: 5, + // label: "Clone", + // action: () => { + + // }, + // }, + { + id: 6, + label: + monitor.isActive === false + ? t("common.buttons.resume") + : t("common.buttons.pause"), + action: async () => { + await patch(`/monitors/${monitor.id}/active`); + refetch(); + }, + closeMenu: true, + }, + { + id: 7, + label: ( + + {t("common.buttons.delete")} + + ), + action: () => { + // setSelectedMonitor(monitor); + }, + closeMenu: true, + }, + ]; + }; + + const getHeaders = () => { + const renderSortIcon = (isActive: boolean) => ( + + {isActive ? ( + sortOrder === "asc" ? ( + + ) : ( + + ) + ) : null} + + ); + + const headers: Header[] = [ + { + id: "name", + content: ( + handleSort(e, "name")} + sx={{ cursor: "pointer" }} + > + {t("common.table.headers.name")} + {renderSortIcon(sortField === "name")} + + ), + render: (row) => { + return row.name; + }, + }, + { + id: "status", + content: ( + handleSort(e, "status")} + sx={{ cursor: "pointer" }} + > + {t("common.table.headers.status")} + {renderSortIcon(sortField === "status")} + + ), + render: (row) => { + return ; + }, + }, + { + id: "histogram", + content: t("pages.pageSpeed.table.headers.pageSpeedScore"), + render: (row) => { + return ( + + + + ); + }, + }, + + { + id: "actions", + content: t("common.table.headers.actions"), + render: (row) => { + return ; + }, + }, + ]; + return headers; + }; + + let headers = getHeaders(); + + if (isSmall) { + headers = headers.filter((h) => h.id !== "histogram"); + } + return ( + +
{ + navigate(`/pagespeed/${row.id}`); + }} + /> + + + ); +}; diff --git a/client/src/Pages/PageSpeed/Monitors/index.tsx b/client/src/Pages/PageSpeed/Monitors/index.tsx new file mode 100644 index 000000000..158260456 --- /dev/null +++ b/client/src/Pages/PageSpeed/Monitors/index.tsx @@ -0,0 +1,86 @@ +import { + MonitorBasePageWithStates, + // UpStatusBox, + // DownStatusBox, + // PausedStatusBox, +} from "@/Components/v2/design-elements"; +import { HeaderCreate } from "@/Components/v2/common"; +// import Stack from "@mui/material/Stack"; +import { PageSpeedMonitorsTable } from "@/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable"; + +import { useState } from "react"; +import { useTheme } from "@mui/material"; +import { useIsAdmin } from "@/Hooks/useIsAdmin"; +import { useGet } from "@/Hooks/UseApi"; +import type { MonitorsWithChecksResponse } from "@/Types/Monitor"; +import type { AppSettingsResponse } from "@/Types/Settings"; + +const PageSpeedMonitorsPage = () => { + const theme = useTheme(); + const isAdmin = useIsAdmin(); + const monitorsUrl = "/monitors/team/with-checks?type=pagespeed&limit=10"; + const { + data: monitorsData, + isLoading: monitorsIsLoading, + error: monitorsError, + refetch, + } = useGet(monitorsUrl); + + const settingsUrl = "/settings"; + const { + data: settingsData, + isLoading: settingsIsLoading, + error: settingsError, + } = useGet(settingsUrl); + + const monitors = monitorsData?.monitors; + const monitorsCount = monitorsData?.count; + // const summary = monitorsData?.summary; + const settings = settingsData; + + const isLoading = monitorsIsLoading || settingsIsLoading; + + const [sortField, setSortField] = useState("name"); + const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); + const [page, setPage] = useState(0); + const [rowsPerPage, setRowsPerPage] = useState(10); + + return ( + + + {/* + + + + */} + + + ); +}; + +export default PageSpeedMonitorsPage; diff --git a/client/src/Pages/PageSpeed/Monitors/index.jsx b/client/src/Pages/PageSpeed/Monitors/old.jsx similarity index 100% rename from client/src/Pages/PageSpeed/Monitors/index.jsx rename to client/src/Pages/PageSpeed/Monitors/old.jsx diff --git a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx index 21e014688..59daf1702 100644 --- a/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx +++ b/client/src/Pages/Uptime/Monitors/Components/UptimeMonitorsTable.tsx @@ -97,7 +97,7 @@ export const MonitorTable = ({ id: 4, label: t("pages.common.monitors.actions.configure"), action: () => { - navigate(`/uptime/create/${monitor.id}`); + navigate(`/uptime/configure/${monitor.id}`); }, }, // { diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 715d4374e..c9a1084d6 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -20,7 +20,7 @@ import UptimeDetails from "../Pages/Uptime/Details"; import UptimeCreate from "../Pages/Uptime/Create/index.jsx"; // PageSpeed -import PageSpeed from "../Pages/PageSpeed/Monitors/index.jsx"; +import PageSpeed from "../Pages/PageSpeed/Monitors/index"; import PageSpeedDetails from "../Pages/PageSpeed/Details/index.jsx"; import PageSpeedCreate from "../Pages/PageSpeed/Create/index.jsx"; @@ -63,6 +63,7 @@ import Logs from "../Pages/Logs/index.jsx"; const Routes = () => { const mode = useSelector((state) => state.ui.mode); const AdminCheckedRegister = withAdminCheck(AuthRegister); + const v2theme = mode === "light" ? lightTheme : darkTheme; return ( { path="/uptime" element={ <> - + @@ -105,7 +106,7 @@ const Routes = () => { path="/uptime/:monitorId/" element={ <> - + @@ -118,7 +119,13 @@ const Routes = () => { } + element={ + <> + + + + + } /> Date: Mon, 26 Jan 2026 18:27:12 +0000 Subject: [PATCH 383/400] fix: add overflowX hidden to table cells to prevent horizontal scrollbar --- client/src/Components/v2/design-elements/Table.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/Components/v2/design-elements/Table.tsx b/client/src/Components/v2/design-elements/Table.tsx index c87fa41b8..7270ac519 100644 --- a/client/src/Components/v2/design-elements/Table.tsx +++ b/client/src/Components/v2/design-elements/Table.tsx @@ -173,6 +173,7 @@ export function DataTable< color: theme.palette.text.secondary, padding: `${theme.spacing(7)} ${theme.spacing(8)}`, fontSize: theme.typography.fontSize, + overflowX: "hidden", }, "& .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root": { borderBottom: "none", From 67f74dd7de8b8fb61e213603d54c13347ab8584d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 18:28:55 +0000 Subject: [PATCH 384/400] fix: prevent horizontal scrollbar in table by adding overflowX hidden and making histogram responsive --- client/src/Components/v2/design-elements/Table.tsx | 2 +- .../src/Components/v2/monitors/charts/HistogramPageSpeed.tsx | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/client/src/Components/v2/design-elements/Table.tsx b/client/src/Components/v2/design-elements/Table.tsx index 7270ac519..0516f1ddd 100644 --- a/client/src/Components/v2/design-elements/Table.tsx +++ b/client/src/Components/v2/design-elements/Table.tsx @@ -147,7 +147,7 @@ export function DataTable<
event.stopPropagation()} overflow={"hidden"} sx={{ From 6cb45ea6deb2245d783aac5b76bc71b75ce609a2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 19:36:29 +0000 Subject: [PATCH 385/400] delete pagepseed --- client/src/Components/v2/inputs/Dialog.tsx | 56 +++++++++++++++++++ client/src/Components/v2/inputs/index.tsx | 1 + client/src/Hooks/UseApi.ts | 37 +++++++++++- .../Components/PageSpeedMonitorsTable.tsx | 5 +- client/src/Pages/PageSpeed/Monitors/index.tsx | 45 +++++++++++---- client/src/Pages/Uptime/Monitors/index.tsx | 1 - client/src/locales/en.json | 10 +++- 7 files changed, 140 insertions(+), 15 deletions(-) create mode 100644 client/src/Components/v2/inputs/Dialog.tsx diff --git a/client/src/Components/v2/inputs/Dialog.tsx b/client/src/Components/v2/inputs/Dialog.tsx new file mode 100644 index 000000000..373d95023 --- /dev/null +++ b/client/src/Components/v2/inputs/Dialog.tsx @@ -0,0 +1,56 @@ +import Dialog from "@mui/material/Dialog"; +import DialogActions from "@mui/material/DialogActions"; +import DialogContent from "@mui/material/DialogContent"; +import DialogContentText from "@mui/material/DialogContentText"; +import DialogTitle from "@mui/material/DialogTitle"; +import { Button } from "@/Components/v2/inputs"; +import { typographyLevels } from "@/Utils/Theme/v2Palette"; +import { useTranslation } from "react-i18next"; + +export const DialogInput = ({ + open, + title, + content, + onConfirm, + onCancel, + confirmColor = "primary", + cancelColor = "error", + loading = false, +}: { + open: boolean; + title?: string; + content?: string; + onConfirm?(item: any): any; + onCancel?(item: any): any; + confirmColor?: "error" | "primary"; + cancelColor?: "error" | "primary"; + loading?: boolean; +}) => { + const { t } = useTranslation(); + return ( + + {title} + + {content} + + + + + + + ); +}; diff --git a/client/src/Components/v2/inputs/index.tsx b/client/src/Components/v2/inputs/index.tsx index 87698a4f0..70e740dbd 100644 --- a/client/src/Components/v2/inputs/index.tsx +++ b/client/src/Components/v2/inputs/index.tsx @@ -8,3 +8,4 @@ export { ToggleButtonInput as ToggleButton, ToggleButtonGroupInput as ToggleButtonGroup, } from "./ToggleButton"; +export { DialogInput as Dialog } from "./Dialog"; diff --git a/client/src/Hooks/UseApi.ts b/client/src/Hooks/UseApi.ts index 441c2b77b..aa71f01af 100644 --- a/client/src/Hooks/UseApi.ts +++ b/client/src/Hooks/UseApi.ts @@ -1,7 +1,7 @@ import useSWR from "swr"; import type { SWRConfiguration } from "swr"; import type { AxiosRequestConfig } from "axios"; -import { get, patch, post } from "@/Utils/ApiClient"; +import { get, patch, post, deleteOp } from "@/Utils/ApiClient"; import { useState } from "react"; import { useToast } from "@/Hooks/UseToast"; @@ -107,3 +107,38 @@ export const usePatch = () => { return { patch: patchFn, loading, error }; }; + +export const useDelete = () => { + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const { toastError, toastSuccess } = useToast(); + + const deleteFn = async ( + endpoint: string, + config?: AxiosRequestConfig + ): Promise | null> => { + setLoading(true); + setError(null); + + try { + const res = await deleteOp>(endpoint, { + ...config, + headers: { + ...config?.headers, + }, + }); + toastSuccess(res.data?.msg || "Operation successful"); + return res.data; + } catch (err: any) { + console.error(err); + const errMsg = err?.response?.data?.message || err.message || "An error occurred"; + toastError(errMsg); + setError(errMsg); + return null; + } finally { + setLoading(false); + } + }; + + return { deleteFn, loading, error }; +}; diff --git a/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx b/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx index 0322948c3..f38e1c706 100644 --- a/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx +++ b/client/src/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable.tsx @@ -13,13 +13,13 @@ import { useTheme } from "@mui/material/styles"; import { useNavigate } from "react-router-dom"; import { usePatch } from "@/Hooks/UseApi"; -import type { Check } from "@/Types/Check"; import type { Monitor, MonitorWithChecks } from "@/Types/Monitor"; import type { ActionMenuItem } from "@/Components/v2/actions-menu"; export const PageSpeedMonitorsTable = ({ monitors, refetch, + setSelectedMonitor, sortField, setSortField, sortOrder, @@ -32,6 +32,7 @@ export const PageSpeedMonitorsTable = ({ }: { monitors: MonitorWithChecks[]; refetch: Function; + setSelectedMonitor: Function; sortField: string; setSortField: (field: string) => void; sortOrder: "asc" | "desc"; @@ -138,7 +139,7 @@ export const PageSpeedMonitorsTable = ({ ), action: () => { - // setSelectedMonitor(monitor); + setSelectedMonitor(monitor); }, closeMenu: true, }, diff --git a/client/src/Pages/PageSpeed/Monitors/index.tsx b/client/src/Pages/PageSpeed/Monitors/index.tsx index 158260456..64b39dbce 100644 --- a/client/src/Pages/PageSpeed/Monitors/index.tsx +++ b/client/src/Pages/PageSpeed/Monitors/index.tsx @@ -4,20 +4,24 @@ import { // DownStatusBox, // PausedStatusBox, } from "@/Components/v2/design-elements"; +import { Dialog } from "@/Components/v2/inputs"; import { HeaderCreate } from "@/Components/v2/common"; // import Stack from "@mui/material/Stack"; import { PageSpeedMonitorsTable } from "@/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable"; +import type { Monitor } from "@/Types/Monitor"; +import { useTranslation } from "react-i18next"; import { useState } from "react"; -import { useTheme } from "@mui/material"; import { useIsAdmin } from "@/Hooks/useIsAdmin"; -import { useGet } from "@/Hooks/UseApi"; +import { useGet, useDelete } from "@/Hooks/UseApi"; import type { MonitorsWithChecksResponse } from "@/Types/Monitor"; import type { AppSettingsResponse } from "@/Types/Settings"; const PageSpeedMonitorsPage = () => { - const theme = useTheme(); + const { t } = useTranslation(); const isAdmin = useIsAdmin(); + const { deleteFn, loading: isDeleting } = useDelete(); + const monitorsUrl = "/monitors/team/with-checks?type=pagespeed&limit=10"; const { data: monitorsData, @@ -33,18 +37,30 @@ const PageSpeedMonitorsPage = () => { error: settingsError, } = useGet(settingsUrl); - const monitors = monitorsData?.monitors; - const monitorsCount = monitorsData?.count; - // const summary = monitorsData?.summary; - const settings = settingsData; - - const isLoading = monitorsIsLoading || settingsIsLoading; - + const [selectedMonitor, setSelectedMonitor] = useState(null); + const isDialogOpen = Boolean(selectedMonitor); const [sortField, setSortField] = useState("name"); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); const [page, setPage] = useState(0); const [rowsPerPage, setRowsPerPage] = useState(10); + const monitors = monitorsData?.monitors; + const monitorsCount = monitorsData?.count; + // const summary = monitorsData?.summary; + + const isLoading = monitorsIsLoading || settingsIsLoading; + + const handleConfirm = async () => { + if (!selectedMonitor) return; + await deleteFn(`/monitors/${selectedMonitor.id}`); + setSelectedMonitor(null); + refetch(); + }; + + const handleCancel = () => { + setSelectedMonitor(null); + }; + return ( { { rowsPerPage={rowsPerPage} setRowsPerPage={setRowsPerPage} /> + ); }; diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index e3e90429a..061fb9454 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -173,7 +173,6 @@ const UptimeMonitorsPage = () => { sortOrder={sortOrder} setSortOrder={setSortOrder} setRowsPerPage={(rowsPerPage: number) => { - console.log("wtf"); dispatch( setRowsPerPage({ value: rowsPerPage, diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 8a44872ff..a630b68e2 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -9,7 +9,15 @@ "incidents": "Incidents", "pause": "Pause", "resume": "Resume", - "delete": "Delete" + "delete": "Delete", + "cancel": "Cancel", + "confirm": "Confirm" + }, + "dialogs": { + "delete": { + "description": "This action cannot be undone.", + "title": "Are you sure you want to delete this?" + } }, "toasts": { "networkError": "Network error", From 6380dcc6badd90f135f2b722f9929a8a3511d38c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 20:05:11 +0000 Subject: [PATCH 386/400] summary --- client/src/Pages/PageSpeed/Monitors/index.tsx | 16 +++++----- .../Components/UptimeMonitorsTable.tsx | 6 ++-- client/src/Pages/Uptime/Monitors/index.tsx | 31 +++++++++++++++++-- 3 files changed, 41 insertions(+), 12 deletions(-) diff --git a/client/src/Pages/PageSpeed/Monitors/index.tsx b/client/src/Pages/PageSpeed/Monitors/index.tsx index 64b39dbce..33a04bb6b 100644 --- a/client/src/Pages/PageSpeed/Monitors/index.tsx +++ b/client/src/Pages/PageSpeed/Monitors/index.tsx @@ -1,15 +1,16 @@ import { MonitorBasePageWithStates, - // UpStatusBox, - // DownStatusBox, - // PausedStatusBox, + UpStatusBox, + DownStatusBox, + PausedStatusBox, } from "@/Components/v2/design-elements"; import { Dialog } from "@/Components/v2/inputs"; import { HeaderCreate } from "@/Components/v2/common"; -// import Stack from "@mui/material/Stack"; +import Stack from "@mui/material/Stack"; import { PageSpeedMonitorsTable } from "@/Pages/PageSpeed/Monitors/Components/PageSpeedMonitorsTable"; import type { Monitor } from "@/Types/Monitor"; +import { useTheme } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useState } from "react"; import { useIsAdmin } from "@/Hooks/useIsAdmin"; @@ -19,6 +20,7 @@ import type { AppSettingsResponse } from "@/Types/Settings"; const PageSpeedMonitorsPage = () => { const { t } = useTranslation(); + const theme = useTheme(); const isAdmin = useIsAdmin(); const { deleteFn, loading: isDeleting } = useDelete(); @@ -46,7 +48,7 @@ const PageSpeedMonitorsPage = () => { const monitors = monitorsData?.monitors; const monitorsCount = monitorsData?.count; - // const summary = monitorsData?.summary; + const summary = monitorsData?.summary; const isLoading = monitorsIsLoading || settingsIsLoading; @@ -74,14 +76,14 @@ const PageSpeedMonitorsPage = () => { isLoading={isLoading} isAdmin={isAdmin} /> - {/* - */} + void; sortField: string; setSortField: (field: string) => void; sortOrder: "asc" | "desc"; @@ -126,9 +128,7 @@ export const MonitorTable = ({ {t("pages.common.monitors.actions.delete")} ), - action: () => { - // setSelectedMonitor(monitor); - }, + action: () => setSelectedMonitor(monitor), closeMenu: true, }, ]; diff --git a/client/src/Pages/Uptime/Monitors/index.tsx b/client/src/Pages/Uptime/Monitors/index.tsx index 061fb9454..5c3828d07 100644 --- a/client/src/Pages/Uptime/Monitors/index.tsx +++ b/client/src/Pages/Uptime/Monitors/index.tsx @@ -5,14 +5,14 @@ import { DownStatusBox, PausedStatusBox, } from "@/Components/v2/design-elements"; -import { TextField } from "@/Components/v2/inputs"; +import { TextField, Dialog } from "@/Components/v2/inputs"; import Stack from "@mui/material/Stack"; import { MonitorTable } from "@/Pages/Uptime/Monitors/Components/UptimeMonitorsTable"; import { HeaderCreate } from "@/Components/v2/common"; import { useTranslation } from "react-i18next"; import useMediaQuery from "@mui/material/useMediaQuery"; -import { useGet } from "@/Hooks/UseApi"; +import { useGet, useDelete } from "@/Hooks/UseApi"; import type { Monitor, MonitorType, MonitorsWithChecksResponse } from "@/Types/Monitor"; import { useState, useMemo, useCallback } from "react"; import { useSelector, useDispatch } from "react-redux"; @@ -40,6 +40,8 @@ const UptimeMonitorsPage = () => { const [search, setSearch] = useState(""); const [sortField, setSortField] = useState(""); const [sortOrder, setSortOrder] = useState<"asc" | "desc">("asc"); + const [selectedMonitor, setSelectedMonitor] = useState(null); + const isDialogOpen = Boolean(selectedMonitor); // Convert filter selections to API filter values // Status: "up" -> true, "down" -> false @@ -88,6 +90,7 @@ const UptimeMonitorsPage = () => { data: monitors, isLoading: monitorsLoading, error, + refetch: refetchMonitorsAndSummary, } = useGet( "/monitors/team?type=http&type=ping&type=port&type=docker", {}, @@ -107,6 +110,9 @@ const UptimeMonitorsPage = () => { const { monitors: monitorsWithChecks, summary, count } = monitorsWithChecksData ?? {}; + // Delete hook + const { deleteFn, loading: isDeleting } = useDelete(); + // Handlers const handleClearFilters = useCallback(() => { setSelectedTypes([]); @@ -115,6 +121,18 @@ const UptimeMonitorsPage = () => { setSearch(""); }, []); + const handleConfirm = async () => { + if (!selectedMonitor) return; + await deleteFn(`/monitors/${selectedMonitor.id}`); + setSelectedMonitor(null); + refetch(); + refetchMonitorsAndSummary(); + }; + + const handleCancel = () => { + setSelectedMonitor(null); + }; + const isLoading = monitorsLoading || monitorsWithChecksLoading; return ( @@ -164,6 +182,7 @@ const UptimeMonitorsPage = () => { { setPage(0); }} /> + ); }; From 1819df5117af50da5ed8b7c9b0604d87296544a4 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 20:27:11 +0000 Subject: [PATCH 387/400] priority fallback --- .../v2/design-elements/BasePage.tsx | 52 +++++++++++++++++++ client/src/Pages/PageSpeed/Monitors/index.tsx | 2 +- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/client/src/Components/v2/design-elements/BasePage.tsx b/client/src/Components/v2/design-elements/BasePage.tsx index c8618fa67..bbe19be07 100644 --- a/client/src/Components/v2/design-elements/BasePage.tsx +++ b/client/src/Components/v2/design-elements/BasePage.tsx @@ -1,8 +1,11 @@ import Stack from "@mui/material/Stack"; +import Alert from "@mui/material/Alert"; +import Link from "@mui/material/Link"; import { ErrorFallback, EmptyFallback, EmptyMonitorFallback, + BaseFallback, } from "@/Components/v2/design-elements/Fallback"; import { Breadcrumb } from "@/Components/v2/design-elements/Breadcrumb"; import CircularProgress from "@mui/material/CircularProgress"; @@ -10,6 +13,41 @@ import CircularProgress from "@mui/material/CircularProgress"; import type { StackProps } from "@mui/material/Stack"; import { useTheme } from "@mui/material/styles"; import { useTranslation } from "react-i18next"; +import { Link as RouterLink } from "react-router-dom"; + +export const PageSpeedKeyPriorityFallback = () => { + const theme = useTheme(); + const { t } = useTranslation(); + + return ( + + + {t("pageSpeedWarning")}{" "} + + {t("pageSpeedLearnMoreLink")} + {" "} + {t("pageSpeedAddApiKey")} + + + ); +}; + interface BasePageProps extends StackProps { loading?: boolean; error?: boolean; @@ -114,6 +152,7 @@ interface MonitorBasePageWithStatesProps extends StackProps { page: string; actionLink?: string; children: React.ReactNode; + priorityFallback?: React.ReactNode; } const isEmpty = (items: any[]) => { @@ -129,12 +168,25 @@ export const MonitorBasePageWithStates = ({ page, actionLink, children, + priorityFallback, ...props }: MonitorBasePageWithStatesProps) => { const { t } = useTranslation(); const showLoading = loading && (!items || items.length === 0); + if (priorityFallback) { + return ( + + {priorityFallback} + + ); + } + if (!loading && isEmpty(items)) { return ( { isLoading: monitorsIsLoading, error: monitorsError, refetch, - } = useGet(monitorsUrl); + } = useGet(monitorsUrl, {}, { refreshInterval: 30000 }); const settingsUrl = "/settings"; const { From 90e9b41973f4dc6f650b639558c42b8292f80236 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 20:47:10 +0000 Subject: [PATCH 388/400] pagespeed key alert --- .../v2/design-elements/BasePage.tsx | 40 +++++++++---------- client/src/Pages/PageSpeed/Monitors/index.tsx | 5 +++ client/src/locales/en.json | 5 +++ 3 files changed, 29 insertions(+), 21 deletions(-) diff --git a/client/src/Components/v2/design-elements/BasePage.tsx b/client/src/Components/v2/design-elements/BasePage.tsx index bbe19be07..de20fc005 100644 --- a/client/src/Components/v2/design-elements/BasePage.tsx +++ b/client/src/Components/v2/design-elements/BasePage.tsx @@ -12,13 +12,11 @@ import CircularProgress from "@mui/material/CircularProgress"; import type { StackProps } from "@mui/material/Stack"; import { useTheme } from "@mui/material/styles"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { Link as RouterLink } from "react-router-dom"; +import { Typography } from "@mui/material"; export const PageSpeedKeyPriorityFallback = () => { - const theme = useTheme(); - const { t } = useTranslation(); - return ( { sx={{ width: "100%", maxWidth: 600, - backgroundColor: theme.palette.warning.light, - color: theme.palette.warning.contrastText, - "& .MuiAlert-icon": { - color: theme.palette.warning.contrastText, - }, }} > - {t("pageSpeedWarning")}{" "} - - {t("pageSpeedLearnMoreLink")} - {" "} - {t("pageSpeedAddApiKey")} + + + ), + }} + /> + ); @@ -178,8 +176,8 @@ export const MonitorBasePageWithStates = ({ if (priorityFallback) { return ( {priorityFallback} diff --git a/client/src/Pages/PageSpeed/Monitors/index.tsx b/client/src/Pages/PageSpeed/Monitors/index.tsx index 811859d41..d15e09e70 100644 --- a/client/src/Pages/PageSpeed/Monitors/index.tsx +++ b/client/src/Pages/PageSpeed/Monitors/index.tsx @@ -3,6 +3,7 @@ import { UpStatusBox, DownStatusBox, PausedStatusBox, + PageSpeedKeyPriorityFallback, } from "@/Components/v2/design-elements"; import { Dialog } from "@/Components/v2/inputs"; import { HeaderCreate } from "@/Components/v2/common"; @@ -52,6 +53,9 @@ const PageSpeedMonitorsPage = () => { const isLoading = monitorsIsLoading || settingsIsLoading; + const showApiKeyWarning = + isAdmin && settingsData && !settingsData.pagespeedKeySet; + const handleConfirm = async () => { if (!selectedMonitor) return; await deleteFn(`/monitors/${selectedMonitor.id}`); @@ -70,6 +74,7 @@ const PageSpeedMonitorsPage = () => { items={monitors || []} page="pageSpeed" actionLink="/pagespeed/create" + priorityFallback={showApiKeyWarning ? : undefined} > Settings to add one. Without it, the PageSpeed monitor won't function." + } + }, "dialogs": { "delete": { "description": "This action cannot be undone.", From a07e41d8f15b800c63ebc6c6ec876685b759eeb2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 20:47:24 +0000 Subject: [PATCH 389/400] format --- client/src/Pages/PageSpeed/Monitors/index.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/Pages/PageSpeed/Monitors/index.tsx b/client/src/Pages/PageSpeed/Monitors/index.tsx index d15e09e70..887ec02c0 100644 --- a/client/src/Pages/PageSpeed/Monitors/index.tsx +++ b/client/src/Pages/PageSpeed/Monitors/index.tsx @@ -53,8 +53,7 @@ const PageSpeedMonitorsPage = () => { const isLoading = monitorsIsLoading || settingsIsLoading; - const showApiKeyWarning = - isAdmin && settingsData && !settingsData.pagespeedKeySet; + const showApiKeyWarning = isAdmin && settingsData && !settingsData.pagespeedKeySet; const handleConfirm = async () => { if (!selectedMonitor) return; From 451e3524589304db873087c5c44a30b50bf8047d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 20:47:29 +0000 Subject: [PATCH 390/400] types --- server/src/service/business/monitorService.ts | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/server/src/service/business/monitorService.ts b/server/src/service/business/monitorService.ts index 513fc2a82..38fcaef78 100644 --- a/server/src/service/business/monitorService.ts +++ b/server/src/service/business/monitorService.ts @@ -342,7 +342,7 @@ export class MonitorService implements IMonitorService { order?: "asc" | "desc"; explain?: boolean; }): Promise => { - const summary = await this.monitorsRepository.findMonitorsSummaryByTeamId(teamId); + const summary = await this.monitorsRepository.findMonitorsSummaryByTeamId(teamId, { type }); const count = await this.monitorsRepository.findMonitorCountByTeamIdAndType(teamId, { type, filter }); const monitors = await this.monitorsRepository.findByTeamId(teamId, { limit, @@ -402,9 +402,27 @@ export class MonitorService implements IMonitorService { deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { const monitor = await this.monitorsRepository.deleteById(monitorId, teamId); - await this.monitorStatsRepository.deleteByMonitorId(monitor.id); - await this.checksRepository.deleteByMonitorId(monitor.id); - await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id); + await this.monitorStatsRepository.deleteByMonitorId(monitor.id).catch((err: any) => { + this.logger.warn({ + message: `Error deleting monitor stats for monitor ${monitor.id} with name ${monitor.name}`, + service: SERVICE_NAME, + stack: err.stack, + }); + }); + await this.checksRepository.deleteByMonitorId(monitor.id).catch((err: any) => { + this.logger.warn({ + message: `Error deleting checks for monitor ${monitor.id} with name ${monitor.name}`, + service: SERVICE_NAME, + stack: err.stack, + }); + }); + await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id).catch((err: any) => { + this.logger.warn({ + message: `Error removing monitor ${monitor.id} with name ${monitor.name} from status pages`, + service: SERVICE_NAME, + stack: err.stack, + }); + }); await this.jobQueue.deleteJob(monitor); return monitor; }; From b61077bd3f997ab610d120137d8a8b904070d748 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 21:14:04 +0000 Subject: [PATCH 391/400] remove unsued strings, initial commit --- .../v2/monitors/charts/PiePagespeed.tsx | 159 ++++++++++++++++++ client/src/Components/v2/monitors/index.tsx | 1 + client/src/Pages/PageSpeed/Details/index.tsx | 7 + .../PageSpeed/Details/{index.jsx => old.jsx} | 0 client/src/Routes/index.jsx | 2 +- client/src/locales/en.json | 40 ----- 6 files changed, 168 insertions(+), 41 deletions(-) create mode 100644 client/src/Components/v2/monitors/charts/PiePagespeed.tsx create mode 100644 client/src/Pages/PageSpeed/Details/index.tsx rename client/src/Pages/PageSpeed/Details/{index.jsx => old.jsx} (100%) diff --git a/client/src/Components/v2/monitors/charts/PiePagespeed.tsx b/client/src/Components/v2/monitors/charts/PiePagespeed.tsx new file mode 100644 index 000000000..cbc6b3106 --- /dev/null +++ b/client/src/Components/v2/monitors/charts/PiePagespeed.tsx @@ -0,0 +1,159 @@ +import { BaseChart } from "@/Components/v2/design-elements"; +import { FileText } from "lucide-react"; +import { Pie, PieChart, ResponsiveContainer, Label } from "recharts"; +import Typography from "@mui/material/Typography"; +import Box from "@mui/material/Box"; +import { getPageSpeedPalette } from "@/Utils/MonitorUtils"; +import { useTheme } from "@mui/material/styles"; +import { alpha } from "@mui/material/styles"; +import { useState } from "react"; +import Tooltip from "@mui/material/Tooltip"; +import { useTranslation } from "react-i18next"; + +const CenterLabel = ({ viewBox, value }: any) => { + const { cx, cy } = viewBox; + return ( + + + {value} + + + ); +}; + +export const PiePageSpeed = ({ latestCheck }: { latestCheck: any }) => { + const { t } = useTranslation(); + const theme = useTheme(); + const [hoverTitle, setHoverTitle] = useState(null); + + const LABELS: Record = { + FCP: t("common.charts.pageSpeed.fcp"), + SI: t("common.charts.pageSpeed.si"), + LCP: t("common.charts.pageSpeed.lcp"), + TBT: t("common.charts.pageSpeed.tbt"), + CLS: t("common.charts.pageSpeed.cls"), + }; + const metrics = [ + { key: "fcp", color: alpha("#1DE9B6", 0.6), weight: 0.1 }, + { key: "si", color: alpha("#7C4DFF", 0.6), weight: 0.1 }, + { key: "lcp", color: alpha("#FFB200", 0.6), weight: 0.25 }, + { key: "tbt", color: alpha("#00AFFE", 0.6), weight: 0.3 }, + { key: "cls", color: alpha("#FF4181", 0.6), weight: 0.25 }, + ]; + + const scores = metrics.flatMap(({ key, color, weight }) => { + const val = Math.floor((latestCheck?.[key] ?? 0) * 100); + const inverse = 100 - val; + return [ + { + name: `${key.toUpperCase()} Inverse`, + value: inverse * weight, + fill: "transparent", + stroke: color, + weight, + }, + { + name: key.toUpperCase(), + value: val * weight, + fill: color, + stroke: color, + weight, + }, + ]; + }); + + const totalScore = + (latestCheck?.fcp || 0) * 0.1 + + (latestCheck?.si || 0) * 0.1 + + (latestCheck?.lcp || 0) * 0.25 + + (latestCheck?.tbt || 0) * 0.3 + + (latestCheck?.cls || 0) * 0.25; + + const pageSpeedPalette = getPageSpeedPalette(Math.floor(totalScore * 100)); + + const score = [ + { + value: 100, + fill: alpha(theme.palette[pageSpeedPalette].light || "#ffffff", 0.6), + stroke: "none", + }, + ]; + + return ( + + } + title={t("common.charts.pageSpeed.title")} + > + + + + + + + `${name}: ${Math.round(value)}`} + dataKey="value" + stroke="none" + onMouseEnter={(_, index: number) => { + const d = scores[index]; + if (!d) return; + const isInverse = String(d.name).includes("Inverse"); + const pair = isInverse && scores[index + 1] ? scores[index + 1] : d; + const base = String(pair.name).replace(" Inverse", ""); + const full = LABELS[base] ?? base; + const displayVal = Math.round(Number(pair.value) || 0); + setHoverTitle(`${full}: ${displayVal}`); + }} + onMouseLeave={() => setHoverTitle(null)} + /> + + + + + + ); +}; diff --git a/client/src/Components/v2/monitors/index.tsx b/client/src/Components/v2/monitors/index.tsx index ca0aaa15e..f8e59270d 100644 --- a/client/src/Components/v2/monitors/index.tsx +++ b/client/src/Components/v2/monitors/index.tsx @@ -5,3 +5,4 @@ export * from "./charts/RadialAvgResponse"; export * from "./charts/HistogramDetails"; export * from "./charts/HistogramPageSpeed"; export * from "./charts/HistogramPageSpeedTooltip"; +export * from "./charts/PiePagespeed"; diff --git a/client/src/Pages/PageSpeed/Details/index.tsx b/client/src/Pages/PageSpeed/Details/index.tsx new file mode 100644 index 000000000..ce40d9b2f --- /dev/null +++ b/client/src/Pages/PageSpeed/Details/index.tsx @@ -0,0 +1,7 @@ +import { BasePage } from "@/Components/v2/design-elements"; + +const PageSpeedDetails = () => { + return PageSpeed Details - Work in Progress; +}; + +export default PageSpeedDetails; diff --git a/client/src/Pages/PageSpeed/Details/index.jsx b/client/src/Pages/PageSpeed/Details/old.jsx similarity index 100% rename from client/src/Pages/PageSpeed/Details/index.jsx rename to client/src/Pages/PageSpeed/Details/old.jsx diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index c9a1084d6..3c2873ffa 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -21,7 +21,7 @@ import UptimeCreate from "../Pages/Uptime/Create/index.jsx"; // PageSpeed import PageSpeed from "../Pages/PageSpeed/Monitors/index"; -import PageSpeedDetails from "../Pages/PageSpeed/Details/index.jsx"; +import PageSpeedDetails from "../Pages/PageSpeed/Details/"; import PageSpeedCreate from "../Pages/PageSpeed/Create/index.jsx"; // Infrastructure diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 928ace54c..78d05ff0e 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -53,8 +53,6 @@ }, "submit": "Submit", "title": "Title", - "distributedStatusHeaderText": "Real-time, real-device coverage", - "distributedStatusSubHeaderText": "Powered by millions devices worldwide, view a system performance by global region, country or city", "settingsDisabled": "Disabled", "settingsSuccessSaved": "Settings saved successfully", "settingsFailedToSave": "Failed to save settings", @@ -90,18 +88,11 @@ "webhookSendSuccess": "Webhook notification sent successfully", "webhookSendError": "Error sending webhook notification to {platform}", "webhookUnsupportedPlatform": "Unsupported platform: {platform}", - "distributedRightCategoryTitle": "Monitor", - "distributedStatusServerMonitors": "Server Monitors", - "distributedStatusServerMonitorsDescription": "Monitor status of related servers", "distributedUptimeCreateSelectURL": "Here you can select the URL of the host, together with the type of monitor.", "distributedUptimeCreateChecks": "Checks to perform", "distributedUptimeCreateChecksDescription": "You can always add or remove checks after adding your site.", - "distributedUptimeCreateIncidentNotification": "Incident notifications", - "distributedUptimeCreateIncidentDescription": "When there is an incident, notify users.", "distributedUptimeCreateAdvancedSettings": "Advanced settings", "distributedUptimeDetailsNoMonitorHistory": "There is no check history for this monitor yet.", - "distributedUptimeDetailsStatusHeaderUptime": "Uptime:", - "distributedUptimeDetailsStatusHeaderLastUpdate": "Last updated", "notifications": { "enableNotifications": "Enable {{platform}} notifications", "testNotification": "Test notification", @@ -174,7 +165,6 @@ "failed": "Failed to send test notification" } }, - "testLocale": "testLocale", "add": "Add", "monitors": "monitors", "pages": { @@ -241,34 +231,6 @@ } } }, - "distributedUptimeStatusCreateStatusPage": "status page", - "distributedUptimeStatusCreateStatusPageAccess": "Access", - "distributedUptimeStatusCreateStatusPageReady": "If your status page is ready, you can mark it as published.", - "distributedUptimeStatusBasicInfoHeader": "Basic Information", - "distributedUptimeStatusBasicInfoDescription": "Define company name and the subdomain that your status page points to.", - "distributedUptimeStatusLogoHeader": "Logo", - "distributedUptimeStatusLogoDescription": "Upload a logo for your status page", - "distributedUptimeStatusLogoUploadButton": "Upload logo", - "distributedUptimeStatusStandardMonitorsHeader": "Standard Monitors", - "distributedUptimeStatusStandardMonitorsDescription": "Attach standard monitors to your status page.", - "distributedUptimeStatusCreateYour": "Create your", - "distributedUptimeStatusEditYour": "Edit your", - "distributedUptimeStatusPublishedLabel": "Published and visible to the public", - "distributedUptimeStatusCompanyNameLabel": "Company name", - "distributedUptimeStatusPageAddressLabel": "Your status page address", - "distributedUptimeStatus30Days": "30 days", - "distributedUptimeStatus60Days": "60 days", - "distributedUptimeStatus90Days": "90 days", - "distributedUptimeStatusPageNotSetUp": "A status page is not set up.", - "distributedUptimeStatusContactAdmin": "Please contact your administrator", - "distributedUptimeStatusPageNotPublic": "This status page is not public.", - "distributedUptimeStatusPageDeleteDialog": "Do you want to delete this status page?", - "distributedUptimeStatusPageDeleteConfirm": "Yes, delete status page", - "distributedUptimeStatusPageDeleteDescription": "Once deleted, your status page cannot be retrieved.", - "distributedUptimeStatusDevices": "Devices", - "distributedUptimeStatusUpt": "UPT", - "distributedUptimeStatusUptBurned": "UPT Burned", - "distributedUptimeStatusUptLogo": "Upt Logo", "incidentsTableNoIncidents": "No incidents recorded", "incidentsTablePaginationLabel": "incidents", "incidentsTableMonitorName": "Monitor Name", @@ -279,7 +241,6 @@ "incidentsOptionsHeader": "Incidents for:", "incidentsOptionsHeaderFilterBy": "Filter by:", "incidentsOptionsHeaderFilterAll": "All", - "incidentsOptionsHeaderFilterActive": "Active", "incidentsOptionsHeaderFilterDown": "Down", "incidentsOptionsHeaderFilterCannotResolve": "Cannot Resolve", "incidentsOptionsHeaderShow": "Show:", @@ -625,7 +586,6 @@ "game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.", "https": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard." }, - "auth": { "common": { "navigation": { From 1bcb2ddf9164a28266af9134873d437ecf1f35ea Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 22:28:02 +0000 Subject: [PATCH 392/400] pagespeed v2 refactor --- client/package-lock.json | 30 +--- client/package.json | 1 - .../v2/design-elements/MonitorStatus.tsx | 1 - .../charts/HistogramPageSpeedDetails.tsx | 144 ++++++++++++++++++ .../HistogramPageSpeedDetailsTooltip.tsx | 78 ++++++++++ .../{PiePagespeed.tsx => PiePageSpeed.tsx} | 32 ++-- .../v2/monitors/charts/PiePageSpeedLegend.tsx | 105 +++++++++++++ client/src/Components/v2/monitors/index.tsx | 5 +- client/src/Pages/PageSpeed/Details/index.tsx | 59 ++++++- client/src/Routes/index.jsx | 8 +- client/src/Types/Monitor.ts | 5 + client/src/locales/en.json | 17 ++- 12 files changed, 434 insertions(+), 51 deletions(-) create mode 100644 client/src/Components/v2/monitors/charts/HistogramPageSpeedDetails.tsx create mode 100644 client/src/Components/v2/monitors/charts/HistogramPageSpeedDetailsTooltip.tsx rename client/src/Components/v2/monitors/charts/{PiePagespeed.tsx => PiePageSpeed.tsx} (80%) create mode 100644 client/src/Components/v2/monitors/charts/PiePageSpeedLegend.tsx diff --git a/client/package-lock.json b/client/package-lock.json index 7f5397b7f..a378fa5c2 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -26,7 +26,6 @@ "joi": "17.13.3", "lucide-react": "^0.562.0", "mui-color-input": "^6.0.0", - "pretty-ms": "9.3.0", "react": "18.3.1", "react-dom": "^18.2.0", "react-hook-form": "7.63.0", @@ -38,7 +37,7 @@ "react-toastify": "^10.0.5", "recharts": "2.15.2", "redux-persist": "6.0.0", - "swr": "2.3.6", + "swr": "^2.3.6", "vite-plugin-svgr": "^4.2.0", "zod": "4.1.11" }, @@ -5780,18 +5779,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parse-ms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", - "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5919,21 +5906,6 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "node_modules/pretty-ms": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", - "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", - "license": "MIT", - "dependencies": { - "parse-ms": "^4.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/client/package.json b/client/package.json index 7c5acb441..360e4911d 100644 --- a/client/package.json +++ b/client/package.json @@ -31,7 +31,6 @@ "joi": "17.13.3", "lucide-react": "^0.562.0", "mui-color-input": "^6.0.0", - "pretty-ms": "9.3.0", "react": "18.3.1", "react-dom": "^18.2.0", "react-hook-form": "7.63.0", diff --git a/client/src/Components/v2/design-elements/MonitorStatus.tsx b/client/src/Components/v2/design-elements/MonitorStatus.tsx index 0ecb77df8..3d5e81845 100644 --- a/client/src/Components/v2/design-elements/MonitorStatus.tsx +++ b/client/src/Components/v2/design-elements/MonitorStatus.tsx @@ -34,7 +34,6 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => { > ; + +export interface ConfigItem { + id: string; + text: string; + palette: PaletteColorKey; +} + +const config: Record = { + seo: { + id: "seo", + text: "SEO", + palette: "primary", + }, + performance: { + id: "performance", + text: "performance", + palette: "success", + }, + bestPractices: { + id: "bestPractices", + text: "best practices", + palette: "warning", + }, + accessibility: { + id: "accessibility", + text: "accessibility", + palette: "error", + }, +}; + +export const HistogramPageSpeedDetails = ({ + checks, + range, +}: { + checks: Check[]; + range: string; +}) => { + const theme = useTheme(); + return ( + + } + title="Score history" + > + + + ( + + )} + /> + + } + /> + + {Object.values(config).map(({ id, palette }) => { + const startColor = theme.palette[palette].main; + const endColor = theme.palette[palette].light; + + return ( + + + + + ); + })} + + {Object.keys(config).map((key) => { + const { palette } = config[key]; + const strokeColor = theme.palette[palette].main; + const bgColor = theme.palette.primary.main; + + return ( + + ); + })} + + + + ); +}; diff --git a/client/src/Components/v2/monitors/charts/HistogramPageSpeedDetailsTooltip.tsx b/client/src/Components/v2/monitors/charts/HistogramPageSpeedDetailsTooltip.tsx new file mode 100644 index 000000000..7219ba821 --- /dev/null +++ b/client/src/Components/v2/monitors/charts/HistogramPageSpeedDetailsTooltip.tsx @@ -0,0 +1,78 @@ +import Box from "@mui/material/Box"; +import Stack from "@mui/material/Stack"; +import Typography from "@mui/material/Typography"; +import { useTheme } from "@mui/material/styles"; +import { formatDateWithTz } from "@/Utils/TimeUtils"; +import type { ConfigItem } from "@/Components/v2/monitors"; +import type { TooltipProps } from "recharts"; +import { useSelector } from "react-redux"; +import type { RootState } from "@/Types/state"; + +interface HistogramPageSpeedScoresTooltipProps + extends Partial> { + config: Record; +} +export const HistogramPageSpeedScoresTooltip = ({ + active, + payload, + label, + config, +}: HistogramPageSpeedScoresTooltipProps) => { + const theme = useTheme(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + + if (active && payload && payload.length) { + return ( + + + {formatDateWithTz(label, "ddd, MMMM D, YYYY, h:mm A", uiTimezone)} + + {Object.keys(config) + .reverse() + .map((key) => { + const { palette } = config[key]; + const dotColor = theme.palette[palette].main; + + return ( + + + + {config[key].text} + + {Math.floor(payload[0].payload[key])} + + ); + })} + + ); + } + return null; +}; diff --git a/client/src/Components/v2/monitors/charts/PiePagespeed.tsx b/client/src/Components/v2/monitors/charts/PiePageSpeed.tsx similarity index 80% rename from client/src/Components/v2/monitors/charts/PiePagespeed.tsx rename to client/src/Components/v2/monitors/charts/PiePageSpeed.tsx index cbc6b3106..1c58bb3f7 100644 --- a/client/src/Components/v2/monitors/charts/PiePagespeed.tsx +++ b/client/src/Components/v2/monitors/charts/PiePageSpeed.tsx @@ -1,5 +1,6 @@ import { BaseChart } from "@/Components/v2/design-elements"; import { FileText } from "lucide-react"; +import type { Check, CheckAudits } from "@/Types/Check"; import { Pie, PieChart, ResponsiveContainer, Label } from "recharts"; import Typography from "@mui/material/Typography"; import Box from "@mui/material/Box"; @@ -29,19 +30,20 @@ const CenterLabel = ({ viewBox, value }: any) => { ); }; -export const PiePageSpeed = ({ latestCheck }: { latestCheck: any }) => { +export const PiePageSpeed = ({ latestCheck }: { latestCheck?: Check }) => { const { t } = useTranslation(); const theme = useTheme(); const [hoverTitle, setHoverTitle] = useState(null); + if (!latestCheck) return null; const LABELS: Record = { - FCP: t("common.charts.pageSpeed.fcp"), - SI: t("common.charts.pageSpeed.si"), - LCP: t("common.charts.pageSpeed.lcp"), - TBT: t("common.charts.pageSpeed.tbt"), - CLS: t("common.charts.pageSpeed.cls"), + FCP: t("pages.pageSpeed.charts.common.fcp"), + SI: t("pages.pageSpeed.charts.common.si"), + LCP: t("pages.pageSpeed.charts.common.lcp"), + TBT: t("pages.pageSpeed.charts.common.tbt"), + CLS: t("pages.pageSpeed.charts.common.cls"), }; - const metrics = [ + const metrics: { key: keyof CheckAudits; color: string; weight: number }[] = [ { key: "fcp", color: alpha("#1DE9B6", 0.6), weight: 0.1 }, { key: "si", color: alpha("#7C4DFF", 0.6), weight: 0.1 }, { key: "lcp", color: alpha("#FFB200", 0.6), weight: 0.25 }, @@ -50,7 +52,8 @@ export const PiePageSpeed = ({ latestCheck }: { latestCheck: any }) => { ]; const scores = metrics.flatMap(({ key, color, weight }) => { - const val = Math.floor((latestCheck?.[key] ?? 0) * 100); + const audit = latestCheck?.audits?.[key]; + const val = Math.floor((audit?.score ?? 0) * 100); const inverse = 100 - val; return [ { @@ -71,12 +74,11 @@ export const PiePageSpeed = ({ latestCheck }: { latestCheck: any }) => { }); const totalScore = - (latestCheck?.fcp || 0) * 0.1 + - (latestCheck?.si || 0) * 0.1 + - (latestCheck?.lcp || 0) * 0.25 + - (latestCheck?.tbt || 0) * 0.3 + - (latestCheck?.cls || 0) * 0.25; - + (latestCheck.audits?.fcp?.score || 0) * 0.1 + + (latestCheck.audits?.si?.score || 0) * 0.1 + + (latestCheck.audits?.lcp?.score || 0) * 0.25 + + (latestCheck.audits?.tbt?.score || 0) * 0.3 + + (latestCheck.audits?.cls?.score || 0) * 0.25; const pageSpeedPalette = getPageSpeedPalette(Math.floor(totalScore * 100)); const score = [ @@ -95,7 +97,7 @@ export const PiePageSpeed = ({ latestCheck }: { latestCheck: any }) => { strokeWidth={1.5} /> } - title={t("common.charts.pageSpeed.title")} + title={t("pages.pageSpeed.charts.pie.title")} > { + const { t } = useTranslation(); + const theme = useTheme(); + const palette = getPageSpeedPalette(value); + return ( + + + {label} + + {`${value}%`} + {`${t("pages.pageSpeed.charts.legend.weight")}: ${weight}%`} + + + + + ); +}; + +export const PiePageSpeedLegend = ({ latestCheck }: { latestCheck?: Check }) => { + const theme = useTheme(); + const { t } = useTranslation(); + + if (!latestCheck) { + return null; + } + return ( + + } + title={t("pages.pageSpeed.charts.legend.title")} + > + + + + + + + + + ); +}; diff --git a/client/src/Components/v2/monitors/index.tsx b/client/src/Components/v2/monitors/index.tsx index f8e59270d..4efaae65c 100644 --- a/client/src/Components/v2/monitors/index.tsx +++ b/client/src/Components/v2/monitors/index.tsx @@ -5,4 +5,7 @@ export * from "./charts/RadialAvgResponse"; export * from "./charts/HistogramDetails"; export * from "./charts/HistogramPageSpeed"; export * from "./charts/HistogramPageSpeedTooltip"; -export * from "./charts/PiePagespeed"; +export * from "./charts/PiePageSpeed"; +export * from "./charts/PiePageSpeedLegend"; +export * from "./charts/HistogramPageSpeedDetails"; +export * from "./charts/HistogramPageSpeedDetailsTooltip"; diff --git a/client/src/Pages/PageSpeed/Details/index.tsx b/client/src/Pages/PageSpeed/Details/index.tsx index ce40d9b2f..f6c672e77 100644 --- a/client/src/Pages/PageSpeed/Details/index.tsx +++ b/client/src/Pages/PageSpeed/Details/index.tsx @@ -1,7 +1,62 @@ import { BasePage } from "@/Components/v2/design-elements"; +import type { PageSpeedDetailsResponse } from "@/Types/Monitor"; +import { HeaderMonitorControls } from "@/Components/v2/common"; +import Stack from "@mui/material/Stack"; +import { + HistogramPageSpeedDetails, + PiePageSpeed, + PiePageSpeedLegend, + MonitorStatBoxes, +} from "@/Components/v2/monitors"; + +import { useIsAdmin } from "@/Hooks/useIsAdmin"; +import { useGet } from "@/Hooks/UseApi"; +import { useParams } from "react-router-dom"; +import { useTheme } from "@mui/material"; const PageSpeedDetails = () => { - return PageSpeed Details - Work in Progress; -}; + const { monitorId } = useParams(); + const isAdmin = useIsAdmin(); + const theme = useTheme(); + const { + data: monitorData, + isLoading, + error, + refetch, + } = useGet( + monitorId ? `/monitors/pagespeed/details/${monitorId}?dateRange=day` : null + ); + const monitor = monitorData?.monitor; + const monitorStats = monitorData?.monitorStats || null; + + return ( + + + + + + + + + + ); +}; export default PageSpeedDetails; diff --git a/client/src/Routes/index.jsx b/client/src/Routes/index.jsx index 3c2873ffa..45715a488 100644 --- a/client/src/Routes/index.jsx +++ b/client/src/Routes/index.jsx @@ -133,7 +133,13 @@ const Routes = () => { /> } + element={ + <> + + + + + } /> Date: Mon, 26 Jan 2026 22:29:28 +0000 Subject: [PATCH 393/400] pretty-ms --- client/package-lock.json | 28 ++++++++++++++++++++++++++++ client/package.json | 1 + 2 files changed, 29 insertions(+) diff --git a/client/package-lock.json b/client/package-lock.json index a378fa5c2..b4ad9f73c 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -26,6 +26,7 @@ "joi": "17.13.3", "lucide-react": "^0.562.0", "mui-color-input": "^6.0.0", + "pretty-ms": "^9.3.0", "react": "18.3.1", "react-dom": "^18.2.0", "react-hook-form": "7.63.0", @@ -5779,6 +5780,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -5906,6 +5919,21 @@ "url": "https://github.com/prettier/prettier?sponsor=1" } }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", diff --git a/client/package.json b/client/package.json index 360e4911d..a5c17e343 100644 --- a/client/package.json +++ b/client/package.json @@ -31,6 +31,7 @@ "joi": "17.13.3", "lucide-react": "^0.562.0", "mui-color-input": "^6.0.0", + "pretty-ms": "^9.3.0", "react": "18.3.1", "react-dom": "^18.2.0", "react-hook-form": "7.63.0", From d71d16ecd0718953faa6f02713ee176d607e48c9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 22:32:08 +0000 Subject: [PATCH 394/400] refresh interval --- client/src/Pages/PageSpeed/Details/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/client/src/Pages/PageSpeed/Details/index.tsx b/client/src/Pages/PageSpeed/Details/index.tsx index f6c672e77..2028f47c0 100644 --- a/client/src/Pages/PageSpeed/Details/index.tsx +++ b/client/src/Pages/PageSpeed/Details/index.tsx @@ -24,7 +24,9 @@ const PageSpeedDetails = () => { error, refetch, } = useGet( - monitorId ? `/monitors/pagespeed/details/${monitorId}?dateRange=day` : null + monitorId ? `/monitors/pagespeed/details/${monitorId}?dateRange=day` : null, + {}, + { keepPreviousData: true, refreshInterval: 30000 } ); const monitor = monitorData?.monitor; From fd5f4933458d1cdc9a7b620137787d850e9b3a1f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 23:02:30 +0000 Subject: [PATCH 395/400] improve migration --- .../0002_convertChecksToTimeSeries.ts | 179 +++++++++++++----- 1 file changed, 135 insertions(+), 44 deletions(-) diff --git a/server/src/db/migration/0002_convertChecksToTimeSeries.ts b/server/src/db/migration/0002_convertChecksToTimeSeries.ts index a6c503804..03fcf712d 100644 --- a/server/src/db/migration/0002_convertChecksToTimeSeries.ts +++ b/server/src/db/migration/0002_convertChecksToTimeSeries.ts @@ -1,9 +1,17 @@ import mongoose from "mongoose"; +import { BulkWriteResult } from "mongodb"; const CHECKS_COLLECTION = "checks"; const BACKUP_COLLECTION = "checks_backup"; +const FAILED_DOCS_COLLECTION = "checks_migration_failed"; const BATCH_SIZE = 1000; +interface MigrationStats { + totalSource: number; + totalMigrated: number; + totalFailed: number; +} + const getDb = () => { const db = mongoose.connection.db; if (!db) { @@ -12,12 +20,34 @@ const getDb = () => { return db; }; +const restoreFromBackup = async () => { + const db = getDb(); + const backupExists = await db.listCollections({ name: BACKUP_COLLECTION }).toArray(); + if (backupExists.length === 0) { + throw new Error("Cannot restore: backup collection does not exist"); + } + + const checksExists = await db.listCollections({ name: CHECKS_COLLECTION }).toArray(); + if (checksExists.length > 0) { + await db.collection(CHECKS_COLLECTION).drop(); + } + + await db.collection(BACKUP_COLLECTION).rename(CHECKS_COLLECTION); +}; + const backupAndDropExistingCollection = async () => { const db = getDb(); const collections = await db.listCollections({ name: CHECKS_COLLECTION }).toArray(); if (collections.length === 0) { return false; } + const backupExists = await db.listCollections({ name: BACKUP_COLLECTION }).toArray(); + if (backupExists.length > 0) { + throw new Error( + `Backup collection "${BACKUP_COLLECTION}" already exists. ` + + `Please remove it manually before running migration.` + ); + } await db .collection(CHECKS_COLLECTION) @@ -27,69 +57,130 @@ const backupAndDropExistingCollection = async () => { return true; }; -const createTimeSeriesCollection = async () => { +const createTimeSeriesCollection = async (backedUp: boolean) => { const db = getDb(); - const existing = await db.listCollections({ name: CHECKS_COLLECTION }).toArray(); - if (existing.length === 0) { - await db.createCollection(CHECKS_COLLECTION, { - timeseries: { - timeField: "createdAt", - metaField: "metadata", - granularity: "seconds", - }, - }); - } + try { + const existing = await db.listCollections({ name: CHECKS_COLLECTION }).toArray(); + if (existing.length === 0) { + await db.createCollection(CHECKS_COLLECTION, { + timeseries: { + timeField: "createdAt", + metaField: "metadata", + granularity: "seconds", + }, + }); + } - await db - .collection(CHECKS_COLLECTION) - .createIndexes([ - { key: { updatedAt: 1 } }, - { key: { "metadata.monitorId": 1, updatedAt: 1 } }, - { key: { "metadata.monitorId": 1, updatedAt: -1 } }, - { key: { "metadata.teamId": 1, updatedAt: -1 } }, - ]); + await db + .collection(CHECKS_COLLECTION) + .createIndexes([ + { key: { createdAt: 1 } }, + { key: { "metadata.monitorId": 1, createdAt: 1 } }, + { key: { "metadata.monitorId": 1, createdAt: -1 } }, + { key: { "metadata.teamId": 1, createdAt: -1 } }, + ]); + } catch (error) { + if (backedUp) { + await restoreFromBackup(); + } + throw error; + } }; -const migrateBackupData = async () => { +const validateDocument = (doc: any): boolean => { + if (!doc.createdAt) { + return false; + } + if (!doc.monitorId || !doc.teamId || !doc.type) { + return false; + } + return true; +}; + +const storeFailedDocument = async (doc: any, reason: string) => { + const db = getDb(); + await db.collection(FAILED_DOCS_COLLECTION).insertOne({ + originalDoc: doc, + reason, + failedAt: new Date(), + }); +}; + +const migrateBackupData = async (backedUp: boolean): Promise => { const db = getDb(); const backupExists = await db.listCollections({ name: BACKUP_COLLECTION }).toArray(); if (backupExists.length === 0) { - return; + return { totalSource: 0, totalMigrated: 0, totalFailed: 0 }; } - const source = db.collection(BACKUP_COLLECTION); - const target = db.collection(CHECKS_COLLECTION); - const cursor = source.find(); - const operations: any[] = []; + const stats: MigrationStats = { + totalSource: 0, + totalMigrated: 0, + totalFailed: 0, + }; - while (await cursor.hasNext()) { - const doc = await cursor.next(); - if (!doc) { - continue; + try { + const source = db.collection(BACKUP_COLLECTION); + const target = db.collection(CHECKS_COLLECTION); + + stats.totalSource = await source.countDocuments(); + + const cursor = source.find().addCursorFlag("noCursorTimeout", true); + const operations: any[] = []; + const invalidDocs: any[] = []; + + while (await cursor.hasNext()) { + const doc = await cursor.next(); + if (!doc) { + continue; + } + + if (!validateDocument(doc)) { + invalidDocs.push(doc); + continue; + } + + + const { _id, monitorId, teamId, type, ...rest } = doc; + const metadata = { monitorId, teamId, type }; + operations.push({ insertOne: { document: { ...rest, metadata } } }); + + if (operations.length >= BATCH_SIZE) { + const result: BulkWriteResult = await target.bulkWrite(operations, { ordered: false }); + stats.totalMigrated += result.insertedCount; + operations.length = 0; + } } - const metadata = { - monitorId: doc.monitorId, - teamId: doc.teamId, - type: doc.type, - }; - operations.push({ insertOne: { document: { ...doc, metadata } } }); - - if (operations.length >= BATCH_SIZE) { - await target.bulkWrite(operations); - operations.length = 0; + if (operations.length) { + const result: BulkWriteResult = await target.bulkWrite(operations, { ordered: false }); + stats.totalMigrated += result.insertedCount; } - } - if (operations.length) { - await target.bulkWrite(operations); + for (const doc of invalidDocs) { + await storeFailedDocument(doc, "Missing required fields (createdAt, monitorId, teamId, or type)"); + stats.totalFailed++; + } + + await cursor.close(); + + return stats; + } catch (error) { + if (backedUp) { + await restoreFromBackup(); + } + throw error; } }; export const convertChecksToTimeSeries = async () => { const backedUp = await backupAndDropExistingCollection(); - await createTimeSeriesCollection(); + await createTimeSeriesCollection(backedUp); if (backedUp) { - await migrateBackupData(); + const stats = await migrateBackupData(backedUp); + console.log( + `Migration completed: ${stats.totalMigrated} documents migrated, ` + + `${stats.totalFailed} documents failed (stored in ${FAILED_DOCS_COLLECTION})` + ); } }; From e26f3facf1be8543a241dd3f1b9d81a5bbce3599 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 23:15:59 +0000 Subject: [PATCH 396/400] use creatdAt --- server/src/db/models/Check.ts | 6 ++--- .../checks/MongoChecksRepistory.ts | 22 +++++++++---------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/server/src/db/models/Check.ts b/server/src/db/models/Check.ts index 94dfa873c..aedbf2079 100644 --- a/server/src/db/models/Check.ts +++ b/server/src/db/models/Check.ts @@ -284,11 +284,9 @@ const CheckSchema = new Schema( } ); -CheckSchema.index({ updatedAt: 1 }); -CheckSchema.index({ "metadata.monitorId": 1, updatedAt: 1 }); -CheckSchema.index({ "metadata.monitorId": 1, updatedAt: -1 }); CheckSchema.index({ "metadata.monitorId": 1, createdAt: -1 }); -CheckSchema.index({ "metadata.teamId": 1, updatedAt: -1 }); +CheckSchema.index({ "metadata.monitorId": 1, createdAt: 1 }); +CheckSchema.index({ createdAt: 1 }); CheckSchema.index({ "metadata.teamId": 1, createdAt: -1 }); const CheckModel = model("Check", CheckSchema); diff --git a/server/src/repositories/checks/MongoChecksRepistory.ts b/server/src/repositories/checks/MongoChecksRepistory.ts index 0783c0040..802f6f0d0 100644 --- a/server/src/repositories/checks/MongoChecksRepistory.ts +++ b/server/src/repositories/checks/MongoChecksRepistory.ts @@ -440,11 +440,11 @@ class MongoChecksRepository implements IChecksRepository { ) => { const matchStage = { "metadata.monitorId": monitorObjectId, - updatedAt: { $gte: startDate, $lte: endDate }, + createdAt: { $gte: startDate, $lte: endDate }, }; const [result] = await CheckModel.aggregate([ { $match: matchStage }, - { $sort: { updatedAt: 1 } }, + { $sort: { createdAt: 1 } }, { $facet: { uptimePercentage: [ @@ -646,7 +646,7 @@ class MongoChecksRepository implements IChecksRepository { avgTemperatures: { $push: { $ifNull: ["$cpu.temperature", [0]] } }, disks: { $push: "$disk" }, net: { $push: "$net" }, - updatedAts: { $push: "$updatedAt" }, + createdAts: { $push: "$createdAt" }, sampleDoc: { $first: "$$ROOT" }, }, }, @@ -685,7 +685,7 @@ class MongoChecksRepository implements IChecksRepository { bytesSentPerSecond: { $let: { vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] }, l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] }, }, @@ -695,7 +695,7 @@ class MongoChecksRepository implements IChecksRepository { deltaBytesRecv: { $let: { vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] }, l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] }, }, @@ -705,7 +705,7 @@ class MongoChecksRepository implements IChecksRepository { deltaPacketsSent: { $let: { vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] }, l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] }, }, @@ -715,7 +715,7 @@ class MongoChecksRepository implements IChecksRepository { deltaPacketsRecv: { $let: { vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] }, l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] }, }, @@ -725,7 +725,7 @@ class MongoChecksRepository implements IChecksRepository { deltaErrIn: { $let: { vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] }, l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] }, }, @@ -735,7 +735,7 @@ class MongoChecksRepository implements IChecksRepository { deltaErrOut: { $let: { vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] }, l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] }, }, @@ -745,7 +745,7 @@ class MongoChecksRepository implements IChecksRepository { deltaDropIn: { $let: { vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] }, l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] }, }, @@ -755,7 +755,7 @@ class MongoChecksRepository implements IChecksRepository { deltaDropOut: { $let: { vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$updatedAts" }, { $first: "$updatedAts" }] }, 1000] }, + tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] }, l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] }, }, From f9c85e2d768542ba0dd84d5d847b3da5b4439f6c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 23:20:18 +0000 Subject: [PATCH 397/400] indexes --- server/src/db/migration/0002_convertChecksToTimeSeries.ts | 2 ++ server/src/db/models/Check.ts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/server/src/db/migration/0002_convertChecksToTimeSeries.ts b/server/src/db/migration/0002_convertChecksToTimeSeries.ts index 03fcf712d..0f32d60ee 100644 --- a/server/src/db/migration/0002_convertChecksToTimeSeries.ts +++ b/server/src/db/migration/0002_convertChecksToTimeSeries.ts @@ -78,6 +78,8 @@ const createTimeSeriesCollection = async (backedUp: boolean) => { { key: { "metadata.monitorId": 1, createdAt: 1 } }, { key: { "metadata.monitorId": 1, createdAt: -1 } }, { key: { "metadata.teamId": 1, createdAt: -1 } }, + { key: { "metadata.monitorId": 1, "metadata.type": 1, createdAt: -1 } }, + { key: { "metadata.teamId": 1, status: 1, createdAt: -1 } }, ]); } catch (error) { if (backedUp) { diff --git a/server/src/db/models/Check.ts b/server/src/db/models/Check.ts index aedbf2079..4d9c499de 100644 --- a/server/src/db/models/Check.ts +++ b/server/src/db/models/Check.ts @@ -288,6 +288,8 @@ CheckSchema.index({ "metadata.monitorId": 1, createdAt: -1 }); CheckSchema.index({ "metadata.monitorId": 1, createdAt: 1 }); CheckSchema.index({ createdAt: 1 }); CheckSchema.index({ "metadata.teamId": 1, createdAt: -1 }); +CheckSchema.index({ "metadata.monitorId": 1, "metadata.type": 1, createdAt: -1 }); +CheckSchema.index({ "metadata.teamId": 1, status: 1, createdAt: -1 }); const CheckModel = model("Check", CheckSchema); From 716479c5bb3295c69e76fa4fca10b70918e802a7 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 23:21:49 +0000 Subject: [PATCH 398/400] format --- server/src/db/migration/0002_convertChecksToTimeSeries.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/server/src/db/migration/0002_convertChecksToTimeSeries.ts b/server/src/db/migration/0002_convertChecksToTimeSeries.ts index 0f32d60ee..222d210b0 100644 --- a/server/src/db/migration/0002_convertChecksToTimeSeries.ts +++ b/server/src/db/migration/0002_convertChecksToTimeSeries.ts @@ -43,10 +43,7 @@ const backupAndDropExistingCollection = async () => { } const backupExists = await db.listCollections({ name: BACKUP_COLLECTION }).toArray(); if (backupExists.length > 0) { - throw new Error( - `Backup collection "${BACKUP_COLLECTION}" already exists. ` + - `Please remove it manually before running migration.` - ); + throw new Error(`Backup collection "${BACKUP_COLLECTION}" already exists. ` + `Please remove it manually before running migration.`); } await db @@ -142,7 +139,6 @@ const migrateBackupData = async (backedUp: boolean): Promise => continue; } - const { _id, monitorId, teamId, type, ...rest } = doc; const metadata = { monitorId, teamId, type }; operations.push({ insertOne: { document: { ...rest, metadata } } }); @@ -182,7 +178,7 @@ export const convertChecksToTimeSeries = async () => { const stats = await migrateBackupData(backedUp); console.log( `Migration completed: ${stats.totalMigrated} documents migrated, ` + - `${stats.totalFailed} documents failed (stored in ${FAILED_DOCS_COLLECTION})` + `${stats.totalFailed} documents failed (stored in ${FAILED_DOCS_COLLECTION})` ); } }; From f51bf36dddd46c497d92c5f6ca7097860bf71707 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 23:44:40 +0000 Subject: [PATCH 399/400] fix tooltip styling --- .../charts/HeatmapResponseTimeTooltip.tsx | 11 +++++--- .../v2/monitors/charts/HistogramPageSpeed.tsx | 3 +-- .../charts/HistogramPageSpeedTooltip.tsx | 27 ++++++++++--------- client/src/locales/en.json | 3 +++ 4 files changed, 26 insertions(+), 18 deletions(-) diff --git a/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx b/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx index 448a60e49..7e9ae4470 100644 --- a/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx +++ b/client/src/Components/v2/common/charts/HeatmapResponseTimeTooltip.tsx @@ -6,7 +6,7 @@ import type { Check } from "@/Types/Check"; import { useTheme } from "@mui/material/styles"; import { useSelector } from "react-redux"; import type { RootState } from "@/Types/state"; - +import { useTranslation } from "react-i18next"; type HeatmapCheck = Check | { status: "placeholder"; responseTime: 0; createdAt: "" }; export const HeatmapResponseTimeTooltip = ({ @@ -16,6 +16,7 @@ export const HeatmapResponseTimeTooltip = ({ children: React.ReactElement; check: HeatmapCheck; }) => { + const { t } = useTranslation(); const uiTimezone = useSelector((state: RootState) => state.ui.timezone); const theme = useTheme(); @@ -54,13 +55,17 @@ export const HeatmapResponseTimeTooltip = ({ {check?.originalResponseTime && ( - Response Time: {check.originalResponseTime.toFixed()} ms + {t("common.labels.responseTime")}: {check.originalResponseTime.toFixed()} ms )} Status:{" "} - {check?.status} + + {check?.status === true + ? t("pages.common.monitors.status.up") + : t("pages.common.monitors.status.down")} + } diff --git a/client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx b/client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx index d8d3b9829..1bcceebd1 100644 --- a/client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx +++ b/client/src/Components/v2/monitors/charts/HistogramPageSpeed.tsx @@ -1,7 +1,6 @@ import Stack from "@mui/material/Stack"; import { Area, AreaChart, CartesianGrid, ResponsiveContainer, Tooltip } from "recharts"; import { HistogramPageSpeedTooltip } from "@/Components/v2/monitors/charts/HistogramPageSpeedTooltip"; - import { useTheme } from "@mui/material/styles"; import type { Check } from "@/Types/Check"; import type { MonitorStatus } from "@/Types/Monitor"; @@ -17,7 +16,7 @@ const processData = (checks: Check[]) => { const bestPractices = entry?.bestPractices || 0; const performance = entry?.performance || 0; const seo = entry?.seo || 0; - return Math.floor(((accessibility + bestPractices + performance + seo) / 4) * 100); + return Math.floor((accessibility + bestPractices + performance + seo) / 4); }; checks.forEach((entry) => { diff --git a/client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx b/client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx index 6fd18251c..1cb47243c 100644 --- a/client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx +++ b/client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx @@ -7,16 +7,21 @@ import { useSelector } from "react-redux"; import type { RootState } from "@/Types/state"; export const HistogramPageSpeedTooltip = ({ + active, payload, -}: TooltipProps) => { +}: TooltipProps) => { const uiTimezone = useSelector((state: RootState) => state.ui.timezone); const theme = useTheme(); + + if (!active || !payload || payload.length === 0) { + return null; + } + + const data = payload[0]?.payload; + return ( - - {formatDateWithTz( - payload?.[0]?.payload?.date ?? 0, - "MMMM D, YYYY, HH:mm A", - uiTimezone - )} + + {formatDateWithTz(data?.date, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)} - - Score: {payload?.[0]?.payload?.score} + + Score: {data?.score} ); diff --git a/client/src/locales/en.json b/client/src/locales/en.json index d8d5c84a1..7386ced7d 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -2,6 +2,9 @@ "common": { "appName": "Checkmate", "monitoringAgentName": "Capture", + "labels": { + "responseTime": "Response time" + }, "buttons": { "toggleTheme": "Toggles light & dark", "testNotifications": "Test notifications", From bbdfc00eb9e874ddf04e4a821ca7c1b3c1c10582 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 26 Jan 2026 23:45:28 +0000 Subject: [PATCH 400/400] format --- .../v2/monitors/charts/HistogramPageSpeedTooltip.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx b/client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx index 1cb47243c..28d43a66e 100644 --- a/client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx +++ b/client/src/Components/v2/monitors/charts/HistogramPageSpeedTooltip.tsx @@ -33,9 +33,7 @@ export const HistogramPageSpeedTooltip = ({ {formatDateWithTz(data?.date, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)} - - Score: {data?.score} - + Score: {data?.score} ); };