Merge pull request #2521 from bluewave-labs/develop

develop -> demo
This commit is contained in:
Alexander Holliday authored and GitHub committed 2025-06-24 10:11:15 +08:00
commit 4ef0ef53ed
50 files changed
+1019 -440

No files matched your search

+246 -153
View File
@@ -45,7 +45,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6",
"prettier": "^3.3.3",
"vite": "^5.4.19"
"vite": "6.3.5"
},
"optionalDependencies": {
"@rollup/rollup-linux-arm64-musl": "4.41.0"
@@ -492,9 +492,9 @@
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
"integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
"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"
],
@@ -504,13 +504,13 @@
"aix"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
"integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
"cpu": [
"arm"
],
@@ -520,13 +520,13 @@
"android"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
"integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
"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"
],
@@ -536,13 +536,13 @@
"android"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
"integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
"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"
],
@@ -552,13 +552,13 @@
"android"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
"integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
"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"
],
@@ -568,13 +568,13 @@
"darwin"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
"integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
"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"
],
@@ -584,13 +584,13 @@
"darwin"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
"integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
"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"
],
@@ -600,13 +600,13 @@
"freebsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
"integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
"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"
],
@@ -616,13 +616,13 @@
"freebsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
"integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
"cpu": [
"arm"
],
@@ -632,13 +632,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
"integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
"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"
],
@@ -648,13 +648,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
"integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
"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"
],
@@ -664,13 +664,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
"integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
"cpu": [
"loong64"
],
@@ -680,13 +680,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
"integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
"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"
],
@@ -696,13 +696,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
"integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
"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"
],
@@ -712,13 +712,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
"integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
"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"
],
@@ -728,13 +728,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
"integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
"cpu": [
"s390x"
],
@@ -744,13 +744,13 @@
"linux"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
"integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
"integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==",
"cpu": [
"x64"
],
@@ -760,13 +760,29 @@
"linux"
],
"engines": {
"node": ">=12"
"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.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
"integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
"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"
],
@@ -776,13 +792,29 @@
"netbsd"
],
"engines": {
"node": ">=12"
"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.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
"integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
"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"
],
@@ -792,13 +824,13 @@
"openbsd"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
"integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
"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"
],
@@ -808,13 +840,13 @@
"sunos"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
"integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
"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"
],
@@ -824,13 +856,13 @@
"win32"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
"integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
"cpu": [
"ia32"
],
@@ -840,13 +872,13 @@
"win32"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
"integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
"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"
],
@@ -856,7 +888,7 @@
"win32"
],
"engines": {
"node": ">=12"
"node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
@@ -3040,9 +3072,9 @@
}
},
"node_modules/brace-expansion": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
"integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==",
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -3172,23 +3204,6 @@
],
"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",
@@ -3259,6 +3274,15 @@
"node": ">=10"
}
},
"node_modules/cosmiconfig/node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"license": "ISC",
"engines": {
"node": ">= 6"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -3820,41 +3844,43 @@
}
},
"node_modules/esbuild": {
"version": "0.21.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
"integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz",
"integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.21.5",
"@esbuild/android-arm": "0.21.5",
"@esbuild/android-arm64": "0.21.5",
"@esbuild/android-x64": "0.21.5",
"@esbuild/darwin-arm64": "0.21.5",
"@esbuild/darwin-x64": "0.21.5",
"@esbuild/freebsd-arm64": "0.21.5",
"@esbuild/freebsd-x64": "0.21.5",
"@esbuild/linux-arm": "0.21.5",
"@esbuild/linux-arm64": "0.21.5",
"@esbuild/linux-ia32": "0.21.5",
"@esbuild/linux-loong64": "0.21.5",
"@esbuild/linux-mips64el": "0.21.5",
"@esbuild/linux-ppc64": "0.21.5",
"@esbuild/linux-riscv64": "0.21.5",
"@esbuild/linux-s390x": "0.21.5",
"@esbuild/linux-x64": "0.21.5",
"@esbuild/netbsd-x64": "0.21.5",
"@esbuild/openbsd-x64": "0.21.5",
"@esbuild/sunos-x64": "0.21.5",
"@esbuild/win32-arm64": "0.21.5",
"@esbuild/win32-ia32": "0.21.5",
"@esbuild/win32-x64": "0.21.5"
"@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"
}
},
"node_modules/escalade": {
@@ -4052,6 +4078,23 @@
"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",
@@ -4184,6 +4227,20 @@
"reusify": "^1.0.4"
}
},
"node_modules/fdir": {
"version": "6.4.6",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz",
"integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==",
"license": "MIT",
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/file-entry-cache": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
@@ -6713,6 +6770,22 @@
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"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==",
"license": "MIT",
"dependencies": {
"fdir": "^6.4.4",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
@@ -6923,20 +6996,23 @@
}
},
"node_modules/vite": {
"version": "5.4.19",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.19.tgz",
"integrity": "sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA==",
"version": "6.3.5",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
"integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==",
"license": "MIT",
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
"rollup": "^4.20.0"
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
"picomatch": "^4.0.2",
"postcss": "^8.5.3",
"rollup": "^4.34.9",
"tinyglobby": "^0.2.13"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^18.0.0 || >=20.0.0"
"node": "^18.0.0 || ^20.0.0 || >=22.0.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
@@ -6945,19 +7021,25 @@
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^18.0.0 || >=20.0.0",
"@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0",
"jiti": ">=1.21.0",
"less": "*",
"lightningcss": "^1.21.0",
"sass": "*",
"sass-embedded": "*",
"stylus": "*",
"sugarss": "*",
"terser": "^5.4.0"
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
@@ -6978,6 +7060,12 @@
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
},
@@ -7133,12 +7221,17 @@
"license": "ISC"
},
"node_modules/yaml": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz",
"integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==",
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 6"
"node": ">= 14.6"
}
},
"node_modules/yocto-queue": {
+1 -1
View File
@@ -58,7 +58,7 @@
"eslint-plugin-react-hooks": "^5.0.0",
"eslint-plugin-react-refresh": "^0.4.6",
"prettier": "^3.3.3",
"vite": "^5.4.19"
"vite": "6.3.5"
},
"optionalDependencies": {
"@rollup/rollup-linux-arm64-musl": "4.41.0"
@@ -2,16 +2,17 @@ import Box from "@mui/material/Box";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import BackgroundSVG from "../../../assets/Images/background.svg";
import { useSelector } from "react-redux";
const AppLayout = ({ children }) => {
const theme = useTheme();
const ui = useSelector((state) => state.ui);
return (
<Box
sx={{
minHeight: "100vh",
backgroundColor: theme.palette.primaryBackground.main,
backgroundImage: `url("${BackgroundSVG}")`,
backgroundImage: ui?.mode === "dark" ? `url("${BackgroundSVG}")` : "none",
backgroundSize: "100% 100%",
backgroundPosition: "center",
backgroundRepeat: "no-repeat",
+2 -1
View File
@@ -8,7 +8,6 @@ import {
ListItemButton,
ListItemIcon,
ListItemText,
ListSubheader,
Menu,
MenuItem,
Stack,
@@ -39,6 +38,7 @@ 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 "./index.css";
@@ -64,6 +64,7 @@ const getMenu = (t) => [
{ name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
{ name: t("menu.maintenance"), path: "maintenance", icon: <Maintenance /> },
{ name: t("menu.logs"), path: "logs", icon: <Logs /> },
{
name: t("menu.settings"),
+1 -1
View File
@@ -4,7 +4,7 @@ import SkeletonLayout from "./skeleton";
// Utils
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
const StatusBoxes = ({ shouldRender, flexWrap = "nowrap", children }) => {
const StatusBoxes = ({ shouldRender = true, flexWrap = "nowrap", children }) => {
const theme = useTheme();
if (!shouldRender) {
return (
+3 -1
View File
@@ -5,7 +5,7 @@ import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Link as RouterLink } from "react-router-dom";
const TextLink = ({ text, linkText, href }) => {
const TextLink = ({ text, linkText, href, target = "_self" }) => {
const theme = useTheme();
return (
@@ -18,6 +18,7 @@ const TextLink = ({ text, linkText, href }) => {
color="accent"
to={href}
component={RouterLink}
target={target}
>
{linkText}
</Link>
@@ -29,6 +30,7 @@ TextLink.propTypes = {
text: PropTypes.string,
linkText: PropTypes.string,
href: PropTypes.string,
target: PropTypes.string,
};
export default TextLink;
+35
View File
@@ -0,0 +1,35 @@
import { useState, useEffect } from "react";
import { networkService } from "../main";
import { createToast } from "../Utils/toastUtils";
import { useTranslation } from "react-i18next";
const useFetchLogs = () => {
const { t } = useTranslation();
const [logs, setLogs] = useState(undefined);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(undefined);
useEffect(() => {
const fetchLogs = async () => {
try {
setIsLoading(true);
const response = await networkService.getLogs();
setLogs(response.data.data);
createToast({
body: t("logsPage.toast.fetchLogsSuccess"),
});
} catch (error) {
setError(error);
createToast({
message: error.message,
});
} finally {
setIsLoading(false);
}
};
fetchLogs();
}, [t]);
return [logs, isLoading, error];
};
export { useFetchLogs };
+97
View File
@@ -0,0 +1,97 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Select from "../../../Components/Inputs/Select";
import Typography from "@mui/material/Typography";
import { useFetchLogs } from "../../../Hooks/logHooks";
import { useTheme } from "@emotion/react";
import { useState } from "react";
import { useTranslation } from "react-i18next";
const formatLog = (theme, log, idx) => {
const levelColors = {
info: theme.palette.success.main,
warn: theme.palette.warning.main,
error: theme.palette.error.main,
debug: theme.palette.accent.main,
};
const color = levelColors[log.level] || theme.palette.primary.contrastText;
return (
<span key={idx}>
<span>[{log.timestamp}]</span>{" "}
<span style={{ color, fontWeight: "bold" }}>{log.level.toUpperCase()}</span>
{": "}
{`(${log.service})`}
{`(${log.method})`}
{": "}
{log.message}
<br />
</span>
);
};
const Logs = () => {
// Local state
const [logLevel, setLogLevel] = useState("all");
// 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") },
{ _id: "warn", name: t("logsPage.logLevelSelect.values.warn") },
{ _id: "error", name: t("logsPage.logLevelSelect.values.error") },
{ _id: "debug", name: t("logsPage.logLevelSelect.values.debug") },
];
return (
<Stack gap={theme.spacing(4)}>
<Box>
<Typography variant="body">{t("logsPage.description")}</Typography>
</Box>
<Stack
direction="row"
alignItems="center"
gap={theme.spacing(4)}
>
<Typography>{t("logsPage.logLevelSelect.title")}</Typography>
<Select
items={LOG_LEVELS}
value={logLevel}
onChange={(e) => {
setLogLevel(e.target.value);
}}
/>
</Stack>
<Box
component="pre"
sx={{
fontFamily: "monospace",
color: theme.palette.primary.contrastText,
padding: 2,
borderRadius: 1,
overflowX: "auto",
whiteSpace: "pre-wrap",
wordWrap: "break-word",
}}
>
<code>
{logs
?.filter((log) => {
if (logLevel === "all") return true;
return log.level === logLevel;
})
.reverse()
.map((log, idx) => formatLog(theme, log, idx))}
</code>
</Box>
</Stack>
);
};
export default Logs;
@@ -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";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@emotion/react";
import { TypeToPathMap } from "../../../../Utils/monitorUtils";
import { TypeToPathMap } from "../../../../../Utils/monitorUtils";
import PropTypes from "prop-types";
import { useTranslation } from "react-i18next";
@@ -1,13 +1,13 @@
import Stack from "@mui/material/Stack";
import DataTable from "../../../../Components/Table";
import DataTable from "../../../../../Components/Table";
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";
import { useTranslation } from "react-i18next";
import { createHeaderFactory } from "../../../../Components/Table/TableUtils";
import { createHeaderFactory } from "../../../../../Components/Table/TableUtils";
const JobTable = ({ jobs = [] }) => {
const theme = useTheme();
@@ -0,0 +1,43 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import StatBox from "../../../../../Components/StatBox";
import StatusBoxes from "../../../../../Components/StatusBoxes";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
const camelToTitle = (str) => {
return str
.replace(/([A-Z])/g, " $1")
.toLowerCase()
.replace(/^./, (m) => m.toUpperCase());
};
const Metrics = ({ metrics = {} }) => {
const { t } = useTranslation();
const theme = useTheme();
const data = Object.keys(metrics)
.filter((key) => key !== "jobsWithFailures")
.map((key) => {
return { key, title: camelToTitle(key), value: metrics[key] };
});
return (
<Stack gap={theme.spacing(2)}>
<Typography variant="h2">{t("queuePage.metricsTable.title")}</Typography>
<StatusBoxes flexWrap="wrap">
{data.map((metric) => {
return (
<StatBox
key={metric.key}
heading={metric.title}
subHeading={metric.value}
/>
);
})}
</StatusBoxes>
</Stack>
);
};
export default Metrics;
@@ -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";
// Utils
import { useTranslation } from "react-i18next";
@@ -1,15 +1,14 @@
// Components
import Stack from "@mui/material/Stack";
import Breadcrumbs from "../../Components/Breadcrumbs";
import JobTable from "./components/JobTable";
import MetricsTable from "./components/MetricsTable";
import Metrics from "./components/Metrics";
import FailedJobTable from "./components/FailedJobTable";
import ButtonGroup from "@mui/material/ButtonGroup";
import Button from "@mui/material/Button";
// Utils
import { useState } from "react";
import { useFetchQueueData, useFlushQueue } from "../../Hooks/queueHooks";
import { useFetchQueueData, useFlushQueue } from "../../../Hooks/queueHooks";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
@@ -23,15 +22,13 @@ const QueueDetails = () => {
const [jobs, metrics, isLoading, error] = useFetchQueueData(trigger);
const [flushQueue, isFlushing, flushError] = useFlushQueue();
const BREADCRUMBS = [{ name: t("queuePage.title"), path: "/queue" }];
if (isLoading) return <div>Loading...</div>;
if (error || flushError) return <div>Error: {error.message}</div>;
return (
<Stack gap={theme.spacing(20)}>
<Breadcrumbs list={BREADCRUMBS} />
<Metrics metrics={metrics} />
<JobTable jobs={jobs} />
<MetricsTable metrics={metrics} />
<FailedJobTable metrics={metrics} />
<ButtonGroup
+41
View File
@@ -0,0 +1,41 @@
import Stack from "@mui/material/Stack";
import Breadcrumbs from "../../Components/Breadcrumbs";
import Tabs from "@mui/material/Tabs";
import Tab from "@mui/material/Tab";
import Queue from "./Queue";
import LogsComponent from "./Logs";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useState } from "react";
const Logs = () => {
const { t } = useTranslation();
const theme = useTheme();
// Local state
const [value, setValue] = useState(0);
// Handlers
const handleChange = (event, newValue) => {
setValue(newValue);
};
const BREADCRUMBS = [{ name: t("logsPage.title"), path: "/logs" }];
return (
<Stack gap={theme.spacing(20)}>
<Breadcrumbs list={BREADCRUMBS} />
<Tabs
value={value}
onChange={handleChange}
>
<Tab label={t("logsPage.tabs.logs")} />
<Tab label={t("logsPage.tabs.queue")} />
</Tabs>
{value === 0 && <LogsComponent />}
{value === 1 && <Queue />}
</Stack>
);
};
export default Logs;
@@ -11,6 +11,8 @@ import { formatDurationRounded } from "../../../Utils/timeUtils";
import { StatusLabel } from "../../../Components/Label";
import { setRowsPerPage } from "../../../Features/UI/uiSlice";
import { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react";
import { useNavigate } from "react-router-dom";
import dayjs from "dayjs";
/**
* Component for pagination actions (first, previous, next, last).
@@ -36,7 +38,8 @@ const MaintenanceTable = ({
}) => {
const rowsPerPage = useSelector((state) => state?.ui?.maintenance?.rowsPerPage ?? 5);
const dispatch = useDispatch();
const theme = useTheme();
const navigate = useNavigate();
const handleChangePage = (event, newPage) => {
setPage(newPage);
};
@@ -180,6 +183,18 @@ const MaintenanceTable = ({
return (
<>
<DataTable
config={{
rowSX: {
cursor: "pointer",
"&:hover td": {
backgroundColor: theme.palette.tertiary.main,
transition: "background-color .3s ease",
},
},
onRowClick: (row) => {
navigate(`/maintenance/create/${row._id}`);
},
}}
headers={headers}
data={maintenanceWindows}
/>
-37
View File
@@ -1,37 +0,0 @@
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import ConfigBox from "../../Components/ConfigBox";
// Utils
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom";
import Link from "../../Components/Link";
const SettingsDev = ({ isAdmin, HEADER_SX }) => {
const { t } = useTranslation();
if (!isAdmin) return null;
return (
<ConfigBox>
<Box>
<Typography
component="h1"
variant="h2"
>
{t("settingsDev")}
</Typography>
<Typography sx={HEADER_SX}>{t("settingsDevDescription")}</Typography>
</Box>
<Box>
<Link
level="secondary"
label={t("settingsDevViewJobQueueDetails")}
url="/queue"
external={false}
/>
</Box>
</ConfigBox>
);
};
export default SettingsDev;
+42 -1
View File
@@ -4,6 +4,8 @@ import ConfigBox from "../../Components/ConfigBox";
import TextInput from "../../Components/Inputs/TextInput";
import Button from "@mui/material/Button";
import Stack from "@mui/material/Stack";
import { Switch } from "@mui/material";
import TextLink from "../../Components/TextLink";
// Utils
import { useTheme } from "@emotion/react";
import { PropTypes } from "prop-types";
@@ -12,7 +14,6 @@ import { useTranslation } from "react-i18next";
import { PasswordEndAdornment } from "../../Components/Inputs/TextInput/Adornments";
import { useSendTestEmail } from "../../Hooks/useSendTestEmail";
import { createToast } from "../../Utils/toastUtils";
import { Switch } from "@mui/material";
const SettingsEmail = ({
isAdmin,
@@ -239,6 +240,46 @@ const SettingsEmail = ({
/>
</Box>
))}
<TextLink
text={t("settingsEmailTransportLinkDescription")}
linkText={t("settingsEmailTransportLinkText")}
href="https://nodemailer.com/smtp"
target="_blank"
/>
<Box
component={"pre"}
sx={{
fontFamily: "monospace",
p: 2,
borderRadius: 1,
overflow: "auto",
}}
>
<code>
{JSON.stringify(
{
host: systemEmailHost,
port: systemEmailPort,
secure: systemEmailSecure,
auth: {
user: systemEmailUser || systemEmailAddress,
pass: "<your_password>",
},
name: systemEmailConnectionHost || "localhost",
pool: systemEmailPool,
tls: {
rejectUnauthorized: systemEmailRejectUnauthorized,
ignoreTLS: systemEmailIgnoreTLS,
requireTLS: systemEmailRequireTLS,
servername: systemEmailTLSServername,
},
},
null,
2
)}
</code>
</Box>
</Box>
<Box>
+1 -5
View File
@@ -8,7 +8,6 @@ import SettingsPagespeed from "./SettingsPagespeed";
import SettingsDemoMonitors from "./SettingsDemoMonitors";
import SettingsAbout from "./SettingsAbout";
import SettingsEmail from "./SettingsEmail";
import SettingsDev from "./SettingsDev";
import Button from "@mui/material/Button";
// Utils
import { settingsValidation } from "../../Validation/validation";
@@ -209,10 +208,7 @@ const Settings = () => {
emailPasswordHasBeenReset={emailPasswordHasBeenReset}
setEmailPasswordHasBeenReset={setEmailPasswordHasBeenReset}
/>
<SettingsDev
isAdmin={isAdmin}
HEADER_SX={HEADING_SX}
/>
<SettingsAbout />
<Stack
direction="row"
@@ -90,6 +90,13 @@ const Content = ({
isChecked={form.showUptimePercentage}
onChange={handleFormChange}
/>
<Checkbox
id="showAdminLoginLink"
name="showAdminLoginLink"
label={t("showAdminLoginLink")}
isChecked={form.showAdminLoginLink}
onChange={handleFormChange}
/>
</Stack>
</ConfigStack>
</Stack>
+3 -1
View File
@@ -20,7 +20,7 @@ const TAB_LIST = ["General settings", "Contents"];
const ERROR_TAB_MAPPING = [
["companyName", "url", "timezone", "color", "isPublished", "logo"],
["monitors", "showUptimePercentage", "showCharts"],
["monitors", "showUptimePercentage", "showCharts", "showAdminLoginLink"],
];
const CreateStatusPage = () => {
@@ -39,6 +39,7 @@ const CreateStatusPage = () => {
monitors: [],
showCharts: true,
showUptimePercentage: true,
showAdminLoginLink: false,
});
const [errors, setErrors] = useState({});
const [selectedMonitors, setSelectedMonitors] = useState([]);
@@ -195,6 +196,7 @@ const CreateStatusPage = () => {
logo: newLogo,
showCharts: statusPage?.showCharts ?? true,
showUptimePercentage: statusPage?.showUptimePercentage ?? true,
showAdminLoginLink: statusPage?.showAdminLoginLink ?? false,
};
});
setSelectedMonitors(statusPageMonitors);
+1 -1
View File
@@ -48,7 +48,7 @@ const PublicStatus = () => {
let link = undefined;
const isPublic = location.pathname.startsWith("/status/uptime/public");
// Public status page
if (isPublic) {
if (isPublic && statusPage && statusPage.showAdminLoginLink === true) {
sx = {
paddingTop: theme.spacing(20),
paddingLeft: "20vw",
+4 -3
View File
@@ -51,7 +51,7 @@ import ProtectedRoute from "../Components/ProtectedRoute";
import CreateNewMaintenanceWindow from "../Pages/Maintenance/CreateMaintenance";
import withAdminCheck from "../Components/HOC/withAdminCheck";
import BulkImport from "../Pages/Uptime/BulkImport";
import Queue from "../Pages/Queue";
import Logs from "../Pages/Logs";
const Routes = () => {
const AdminCheckedRegister = withAdminCheck(AuthRegister);
@@ -187,9 +187,10 @@ const Routes = () => {
path="account/team"
element={<Account open={"team"} />}
/>
<Route
path="queue"
element={<Queue />}
path="logs"
element={<Logs />}
/>
</Route>
+11
View File
@@ -871,6 +871,9 @@ class NetworkService {
if (form.showUptimePercentage !== undefined) {
fd.append("showUptimePercentage", String(form.showUptimePercentage));
}
if (form.showAdminLoginLink !== undefined) {
fd.append("showAdminLoginLink", String(form.showAdminLoginLink));
}
form.monitors &&
form.monitors.forEach((monitorId) => {
fd.append("monitors[]", monitorId);
@@ -1041,6 +1044,14 @@ class NetworkService {
});
return response;
}
async getLogs() {
return this.axiosInstance.get(`/logs`, {
headers: {
"Content-Type": "application/json",
},
});
}
}
export default NetworkService;
+3
View File
@@ -407,6 +407,7 @@ const baseTheme = (palette) => ({
}),
},
},
MuiTab: {
styleOverrides: {
root: ({ theme }) => ({
@@ -475,6 +476,8 @@ const baseTheme = (palette) => ({
MuiTabs: {
styleOverrides: {
root: ({ theme }) => ({
height: "34px",
minHeight: "34px",
display: "inline-flex",
borderRadius: 0,
"& .MuiTabs-indicator": {
+1
View File
@@ -277,6 +277,7 @@ const statusPageValidation = joi.object({
logo: logoImageValidation,
showUptimePercentage: joi.boolean(),
showCharts: joi.boolean(),
showAdminLoginLink: joi.boolean(),
});
const settingsValidation = joi.object({
+5
View File
@@ -0,0 +1,5 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M21 5C21 6.65685 16.9706 8 12 8C7.02944 8 3 6.65685 3 5M21 5C21 3.34315 16.9706 2 12 2C7.02944 2 3 3.34315 3 5M21 5V19C21 20.66 17 22 12 22C7 22 3 20.66 3 19V5M21 12C21 13.66 17 15 12 15C7 15 3 13.66 3 12"
stroke="black" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>

After

Width:  |  Height:  |  Size: 421 B

+11 -3
View File
@@ -583,6 +583,13 @@
},
"errors": {
"validation": ""
},
"fields": {
"password": {
"errors": {
"incorrect": ""
}
}
}
},
"login": {
@@ -709,13 +716,13 @@
"integrationKeyLabel": "",
"integrationKeyPlaceholder": ""
},
"genericWebhookSettings": {
"discordSettings": {
"title": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
},
"discordSettings": {
"webhookSettings": {
"title": "",
"description": "",
"webhookLabel": "",
@@ -740,5 +747,6 @@
"settingsEmailRejectUnauthorized": "",
"settingsEmailSecure": "",
"settingsEmailPool": "",
"sendTestNotifications": ""
"sendTestNotifications": "",
"selectAll": ""
}
+11 -3
View File
@@ -583,6 +583,13 @@
},
"errors": {
"validation": "Nastala chyba při ověřování dat."
},
"fields": {
"password": {
"errors": {
"incorrect": ""
}
}
}
},
"login": {
@@ -709,13 +716,13 @@
"integrationKeyLabel": "",
"integrationKeyPlaceholder": ""
},
"genericWebhookSettings": {
"discordSettings": {
"title": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
},
"discordSettings": {
"webhookSettings": {
"title": "",
"description": "",
"webhookLabel": "",
@@ -740,5 +747,6 @@
"settingsEmailRejectUnauthorized": "",
"settingsEmailSecure": "",
"settingsEmailPool": "",
"sendTestNotifications": ""
"sendTestNotifications": "",
"selectAll": ""
}
+11 -3
View File
@@ -583,6 +583,13 @@
},
"errors": {
"validation": ""
},
"fields": {
"password": {
"errors": {
"incorrect": ""
}
}
}
},
"login": {
@@ -709,13 +716,13 @@
"integrationKeyLabel": "",
"integrationKeyPlaceholder": ""
},
"genericWebhookSettings": {
"discordSettings": {
"title": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
},
"discordSettings": {
"webhookSettings": {
"title": "",
"description": "",
"webhookLabel": "",
@@ -740,5 +747,6 @@
"settingsEmailRejectUnauthorized": "",
"settingsEmailSecure": "",
"settingsEmailPool": "",
"sendTestNotifications": ""
"sendTestNotifications": "",
"selectAll": ""
}
+54 -32
View File
@@ -30,9 +30,6 @@
"settingsRemoveAllMonitorsDialogTitle": "Do you want to remove all monitors?",
"settingsRemoveAllMonitorsDialogConfirm": "Yes, remove all monitors",
"settingsAbout": "About",
"settingsDev": "Developer",
"settingsDevDescription": "Developer settings",
"settingsDevViewJobQueueDetails": "View job queue details",
"settingsDevelopedBy": "Developed by Bluewave Labs.",
"settingsSave": "Save",
"settingsSuccessSaved": "Settings saved successfully",
@@ -319,6 +316,7 @@
"statusPageCreateTabsContentFeaturesDescription": "Show more details on the status page",
"showCharts": "Show charts",
"showUptimePercentage": "Show uptime percentage",
"showAdminLoginLink": "Show \"Administrator? Login Here\" link on the status page",
"removeLogo": "Remove Logo",
"statusPageStatus": "A public status page is not set up.",
"statusPageStatusContactAdmin": "Please contact to your administrator",
@@ -431,6 +429,7 @@
"statusPages": "Status pages",
"maintenance": "Maintenance",
"integrations": "Integrations",
"logs": "Logs",
"settings": "Settings",
"support": "Support",
"discussions": "Discussions",
@@ -450,6 +449,8 @@
"settingsEmailPassword": "Email password - Password for authentication",
"settingsEmailUser": "Email user - Username for authentication, overrides email address if specified",
"settingsEmailFieldResetLabel": "Password is set. Click Reset to change it.",
"settingsEmailTransportLinkDescription": "This builds an SMTP transport for NodeMailer",
"settingsEmailTransportLinkText": "See specifications here",
"state": "State",
"statusBreadCrumbsStatusPages": "Status Pages",
"statusBreadCrumbsDetails": "Details",
@@ -510,13 +511,6 @@
},
"auth": {
"common": {
"fields": {
"password": {
"errors": {
"incorrect": "The password you provided does not match our records"
}
}
},
"navigation": {
"continue": "Continue",
"back": "Back"
@@ -597,6 +591,13 @@
},
"errors": {
"validation": "Error validating data."
},
"fields": {
"password": {
"errors": {
"incorrect": "The password you provided does not match our records"
}
}
}
},
"login": {
@@ -717,23 +718,23 @@
"webhookLabel": "Slack webhook URL",
"webhookPlaceholder": "https://hooks.slack.com/services/..."
},
"pagerDutySettings": {
"pagerdutySettings": {
"title": "PagerDuty",
"description": "Configure your PagerDuty integration here",
"integrationKeyLabel": "Integration key",
"integrationKeyPlaceholder": "1234567890"
},
"webhookSettings": {
"title": "Webhook",
"description": "Configure your webhook here",
"webhookLabel": "Webhook URL",
"webhookPlaceholder": "https://your-server.com/webhook"
},
"discordSettings": {
"title": "Discord",
"description": "Configure your Discord webhook here",
"webhookLabel": "Discord Webhook URL",
"webhookPlaceholder": "https://your-server.com/webhook"
},
"webhookSettings": {
"title": "Webhook",
"description": "Configure your webhook here",
"webhookLabel": "Webhook URL",
"webhookPlaceholder": "https://your-server.com/webhook"
}
},
"notificationConfig": {
@@ -749,13 +750,34 @@
},
"advancedMatching": "Advanced matching",
"settingsEmailTLSServername": "TLS Servername - Optional Hostname for TLS Validation when host is an IP",
"settingsEmailIgnoreTLS": "Disable STARTTLS: Don't use TLS even if the server supports it",
"settingsEmailRequireTLS": "Force STARTTLS: Require TLS upgrade, fail if not supported",
"settingsEmailRejectUnauthorized": "Reject invalid certificates: Reject connections with self-signed or untrusted certificates",
"settingsEmailSecure": "Use SSL (recommended): Encrypt the connection using SSL/TLS",
"settingsEmailPool": "Enable connection pooling: Reuse existing connections to improve performance",
"settingsEmailIgnoreTLS": "Ignore TLS - Disable STARTTLS",
"settingsEmailRequireTLS": "Require TLS - Force STARTTLS",
"settingsEmailRejectUnauthorized": "Reject Unauthorized",
"settingsEmailSecure": "Secure - Use SSL",
"settingsEmailPool": "Pool - Enable connection pooling",
"sendTestNotifications": "Send test notifications",
"selectAll": "Select all",
"logsPage": {
"title": "Logs",
"description": "This page shows the latest 1000 lines of logs from the Checkmate server",
"tabs": {
"queue": "Job queue",
"logs": "Server logs"
},
"toast": {
"fetchLogsSuccess": "Logs fetched successfully"
},
"logLevelSelect": {
"title": "Log level",
"values": {
"all": "All",
"info": "Info",
"warn": "Warn",
"error": "Error",
"debug": "Debug"
}
}
},
"queuePage": {
"title": "Queue",
"refreshButton": "Refresh",
@@ -785,16 +807,16 @@
"failCountHeader": "Fail count",
"failedAtHeader": "Last failed at",
"failReasonHeader": "Fail reason"
},
"export": {
"title": "Export Monitors",
"success": "Monitors exported successfully!",
"failed": "Failed to export monitors"
},
"monitorActions": {
"title": "Export/Import",
"import": "Import Monitors",
"export": "Export Monitors"
}
},
"export": {
"title": "Export Monitors",
"success": "Monitors exported successfully!",
"failed": "Failed to export monitors"
},
"monitorActions": {
"title": "Export/Import",
"import": "Import Monitors",
"export": "Export Monitors"
}
}
+11 -3
View File
@@ -583,6 +583,13 @@
},
"errors": {
"validation": ""
},
"fields": {
"password": {
"errors": {
"incorrect": ""
}
}
}
},
"login": {
@@ -709,13 +716,13 @@
"integrationKeyLabel": "",
"integrationKeyPlaceholder": ""
},
"genericWebhookSettings": {
"discordSettings": {
"title": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
},
"discordSettings": {
"webhookSettings": {
"title": "",
"description": "",
"webhookLabel": "",
@@ -740,5 +747,6 @@
"settingsEmailRejectUnauthorized": "",
"settingsEmailSecure": "",
"settingsEmailPool": "",
"sendTestNotifications": ""
"sendTestNotifications": "",
"selectAll": ""
}
+11 -3
View File
@@ -583,6 +583,13 @@
},
"errors": {
"validation": ""
},
"fields": {
"password": {
"errors": {
"incorrect": ""
}
}
}
},
"login": {
@@ -709,13 +716,13 @@
"integrationKeyLabel": "",
"integrationKeyPlaceholder": ""
},
"genericWebhookSettings": {
"discordSettings": {
"title": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
},
"discordSettings": {
"webhookSettings": {
"title": "",
"description": "",
"webhookLabel": "",
@@ -740,5 +747,6 @@
"settingsEmailRejectUnauthorized": "",
"settingsEmailSecure": "",
"settingsEmailPool": "",
"sendTestNotifications": ""
"sendTestNotifications": "",
"selectAll": ""
}
+11 -3
View File
@@ -583,6 +583,13 @@
},
"errors": {
"validation": ""
},
"fields": {
"password": {
"errors": {
"incorrect": ""
}
}
}
},
"login": {
@@ -709,13 +716,13 @@
"integrationKeyLabel": "",
"integrationKeyPlaceholder": ""
},
"genericWebhookSettings": {
"discordSettings": {
"title": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
},
"discordSettings": {
"webhookSettings": {
"title": "",
"description": "",
"webhookLabel": "",
@@ -740,5 +747,6 @@
"settingsEmailRejectUnauthorized": "",
"settingsEmailSecure": "",
"settingsEmailPool": "",
"sendTestNotifications": ""
"sendTestNotifications": "",
"selectAll": ""
}
+12 -3
View File
@@ -312,6 +312,7 @@
"statusPageCreateTabsContentFeaturesDescription": "Mostrar mais detalhes na página de status",
"showCharts": "Mostrar gráficos",
"showUptimePercentage": "Mostrar porcentagem de Uptime",
"showAdminLoginLink": "Mostrar o link \"Administrador? Efetue login aqui\" na página de status",
"removeLogo": "Remover logo",
"statusPageStatus": "Uma página de status pública não está configurada.",
"statusPageStatusContactAdmin": "Entre em contato com seu administrador",
@@ -583,6 +584,13 @@
},
"errors": {
"validation": ""
},
"fields": {
"password": {
"errors": {
"incorrect": ""
}
}
}
},
"login": {
@@ -709,13 +717,13 @@
"integrationKeyLabel": "",
"integrationKeyPlaceholder": ""
},
"genericWebhookSettings": {
"discordSettings": {
"title": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
},
"discordSettings": {
"webhookSettings": {
"title": "",
"description": "",
"webhookLabel": "",
@@ -740,5 +748,6 @@
"settingsEmailRejectUnauthorized": "",
"settingsEmailSecure": "",
"settingsEmailPool": "",
"sendTestNotifications": ""
"sendTestNotifications": "",
"selectAll": ""
}
+11 -3
View File
@@ -583,6 +583,13 @@
},
"errors": {
"validation": ""
},
"fields": {
"password": {
"errors": {
"incorrect": ""
}
}
}
},
"login": {
@@ -709,13 +716,13 @@
"integrationKeyLabel": "",
"integrationKeyPlaceholder": ""
},
"genericWebhookSettings": {
"discordSettings": {
"title": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
},
"discordSettings": {
"webhookSettings": {
"title": "",
"description": "",
"webhookLabel": "",
@@ -740,5 +747,6 @@
"settingsEmailRejectUnauthorized": "",
"settingsEmailSecure": "",
"settingsEmailPool": "",
"sendTestNotifications": ""
"sendTestNotifications": "",
"selectAll": ""
}
+131 -123
View File
@@ -117,11 +117,11 @@
"title": "",
"checks": [""]
},
"createButton": "",
"createTitle": "",
"createButton": "Bildirim kanalı oluştur",
"createTitle": "Bildirim kanalı",
"create": {
"success": "",
"failed": ""
"failed": "Bildirim kanalı oluşturulamadı"
},
"fetch": {
"success": "",
@@ -136,7 +136,7 @@
"failed": ""
},
"test": {
"success": "",
"success": "Test bildirimi başarıyla gönderildi",
"failed": ""
}
},
@@ -373,33 +373,33 @@
"pageSpeedApiKeyFieldDescription": "Sayfa hızı izlemeyi etkinleştirmek için Google PageSpeed API anahtarınızı girin. Anahtarı güncellemek için Sıfırla'ya tıklayın.",
"pageSpeedApiKeyFieldResetLabel": "API anahtarı ayarlandı. Değiştirmek için Sıfırla'ya tıklayın.",
"reset": "Sıfırla",
"ignoreTLSError": "",
"tlsErrorIgnored": "",
"ignoreTLSError": "TLS/SSL hatalarını gözardı et",
"tlsErrorIgnored": "TLS/SSL hataları gözardı ediliyor",
"ignoreTLSErrorDescription": "",
"createNew": "",
"createNew": "Yeni oluştur",
"greeting": {
"prepend": "",
"prepend": "Merhaba",
"append": "",
"overview": ""
},
"roles": {
"superAdmin": "",
"admin": "",
"teamMember": "",
"demoUser": ""
"superAdmin": "Süper yönetici",
"admin": "Yönetici",
"teamMember": "Ekip elemanı",
"demoUser": "Demo kullanıcısı"
},
"teamPanel": {
"teamMembers": "",
"teamMembers": "Ekip elemanları",
"filter": {
"all": "",
"member": ""
"all": "Tümü",
"member": "Eleman"
},
"inviteTeamMember": "",
"inviteNewTeamMember": "",
"inviteTeamMember": "Ekip elemanı davet et",
"inviteNewTeamMember": "Yeni ekip elemanı davet et",
"inviteDescription": "",
"email": "",
"selectRole": "",
"inviteLink": "",
"email": "Eposta",
"selectRole": "Rolü seçin",
"inviteLink": "Davet linki",
"cancel": "",
"noMembers": "",
"getToken": "",
@@ -422,21 +422,21 @@
"infrastructure": "",
"incidents": "",
"statusPages": "",
"maintenance": "",
"integrations": "",
"settings": "",
"support": "",
"discussions": "",
"docs": "",
"changelog": "",
"profile": "",
"password": "",
"team": "",
"maintenance": "Bakım",
"integrations": "Entegrasyonlar",
"settings": "Ayarlar",
"support": "Destek",
"discussions": "Soru-cevap",
"docs": "Dokümanlar",
"changelog": "Değişiklik günlüğü",
"profile": "Profil",
"password": "Parola",
"team": "Ekip",
"logOut": "Çıkış yap",
"notifications": ""
"notifications": "Bildirimler"
},
"settingsEmail": "",
"settingsEmailDescription": "",
"settingsEmail": "Eposta",
"settingsEmailDescription": "Sistem için eposta ayarlarını düzenleyin. Bu işlem eposta üzerinden bildirim ve alarm almak için gereklidir.",
"settingsEmailHost": "",
"settingsEmailPort": "",
"settingsEmailAddress": "",
@@ -490,73 +490,73 @@
"port": ""
},
"common": {
"appName": "",
"appName": "Checkmate",
"monitoringAgentName": "",
"buttons": {
"toggleTheme": ""
},
"toasts": {
"networkError": "",
"checkConnection": "",
"unknownError": ""
"checkConnection": "Lütfen bağlantınızı kontrol edin",
"unknownError": "Bilinmeyen hata"
}
},
"auth": {
"common": {
"navigation": {
"continue": "",
"back": ""
"continue": "Devam et",
"back": "Geri"
},
"inputs": {
"email": {
"label": "",
"label": "Eposta",
"placeholder": "",
"errors": {
"empty": "",
"empty": "Devam etmek için eposta adresinizi girin",
"invalid": ""
}
},
"password": {
"label": "",
"label": "Parola",
"rules": {
"length": {
"beginning": "",
"highlighted": ""
"highlighted": "8 karakter uzunluğunda"
},
"special": {
"beginning": "",
"highlighted": ""
"beginning": "En az içermelidir: ",
"highlighted": "bir özel karakter"
},
"number": {
"beginning": "",
"highlighted": ""
"beginning": "En az içermelidir: ",
"highlighted": "bir sayı"
},
"uppercase": {
"beginning": "",
"highlighted": ""
"beginning": "En az içermelidir: ",
"highlighted": "Bir büyük harf"
},
"lowercase": {
"beginning": "",
"highlighted": ""
"beginning": "En az içermelidir: ",
"highlighted": "Bir küçük harf"
},
"match": {
"beginning": "",
"highlighted": ""
"highlighted": "eşleşmelidir"
}
},
"errors": {
"empty": "",
"length": "",
"uppercase": "",
"lowercase": "",
"number": "",
"special": "",
"incorrect": ""
"empty": "Lütfen parolanızı girin",
"length": "Parolanız en az 8 karakter uzunluğunda olmalıdır",
"uppercase": "Parola en az 1 büyük harf içermelidir",
"lowercase": "Parola en az 1 küçük harf içermelidir",
"number": "Parola en az 1 sayı içermelidir",
"special": "Parola en az 1 özel karakter içermelidir",
"incorrect": "Parolanız sistemdeki ile uyuşmuyor"
}
},
"passwordConfirm": {
"label": "",
"placeholder": "",
"label": "Parolayı onayla",
"placeholder": "Onaylamak için parolayı yeniden gir",
"errors": {
"empty": "",
"different": ""
@@ -572,78 +572,85 @@
}
},
"lastName": {
"label": "",
"placeholder": "",
"label": "Soyadı",
"placeholder": "Ellis",
"errors": {
"empty": "",
"length": "",
"empty": "Lütfen soyadınızı girin",
"length": "Soyadı en çok 50 karakter olmalıdır",
"pattern": ""
}
}
},
"errors": {
"validation": ""
},
"fields": {
"password": {
"errors": {
"incorrect": ""
}
}
}
},
"login": {
"heading": "",
"subheadings": {
"stepOne": "",
"stepTwo": ""
"stepOne": "Eposta adresinizi girin",
"stepTwo": "Parolanızı girin"
},
"links": {
"forgotPassword": "",
"register": "",
"forgotPasswordLink": "",
"registerLink": ""
"forgotPassword": "Parolanızı mı unuttunuz?",
"register": "Hesabınız yok mu?",
"forgotPasswordLink": "Parolayı sıfırla",
"registerLink": "Buradan kayıt olun"
},
"toasts": {
"success": "",
"incorrectPassword": ""
"success": "Tekrar hoşgeldiniz! Başarıyla sisteme giriş yaptınız.",
"incorrectPassword": "Hatalı parola"
},
"errors": {
"password": {
"incorrect": ""
"incorrect": "Giriş yaptığınız parola sistemdekiyle uyuşmuyor"
}
}
},
"registration": {
"heading": {
"superAdmin": "",
"user": ""
"superAdmin": "Süper yönetici oluştur",
"user": "Kayıt ol"
},
"subheadings": {
"stepOne": "",
"stepTwo": "",
"stepThree": ""
"stepOne": "Kişisel bilgilerinizi girin",
"stepTwo": "Eposta adresinizi girin",
"stepThree": "Parolanızı oluturun"
},
"description": {
"superAdmin": "",
"superAdmin": "Başlamak için süper yönetici hesabı oluşturun",
"user": ""
},
"gettingStartedButton": {
"superAdmin": "",
"superAdmin": "Süper yönetici hesabı oluşturun",
"user": ""
},
"termsAndPolicies": "",
"links": {
"login": ""
"login": "Bir hesabınız mı var? <a>Giriş yapın</a>"
},
"toasts": {
"success": ""
"success": "Hoşgeldiniz! Hesabınız başarıyla oluşturuldu."
}
},
"forgotPassword": {
"heading": "",
"heading": "Parolanızı mı kaybettiniz?",
"subheadings": {
"stepOne": "",
"stepTwo": "",
"stepOne": "Endişelenmeyin, sıfırlamak için gerekli bilgileri göndereceğiz.",
"stepTwo": "Parola sıfırlama için gerekli bilgileri <email/> hesabına ilettik.",
"stepThree": "",
"stepFour": ""
},
"buttons": {
"openEmail": "",
"resetPassword": ""
"openEmail": "Eposta uygulamasını aç",
"resetPassword": "Parolayı sıfırla"
},
"imageAlts": {
"passwordKey": "",
@@ -670,75 +677,76 @@
"reconnected": "",
"stillUnreachable": ""
},
"alertBox": "",
"alertBox": "Sunucu bağlantı hatası",
"description": "",
"retryButton": {
"default": "",
"processing": ""
"default": "Bağlantıyı tekrarla",
"processing": "Bağlanıyor..."
}
}
},
"createNotifications": {
"title": "",
"title": "Bildirim kanalları oluştur",
"nameSettings": {
"title": "",
"description": "",
"nameLabel": "",
"namePlaceholder": ""
"title": "İsim",
"description": "Entegrasyon için açıklama",
"nameLabel": "İsim",
"namePlaceholder": "örn. Slack bildirimleri"
},
"typeSettings": {
"title": "",
"title": "Tür",
"description": "",
"typeLabel": ""
"typeLabel": "Tür"
},
"emailSettings": {
"title": "",
"description": "",
"emailLabel": "",
"emailPlaceholder": ""
"title": "Eposta",
"description": "Hedef eposta adresi",
"emailLabel": "Eposta adresi",
"emailPlaceholder": "örn. john@example.com"
},
"slackSettings": {
"title": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
"title": "Slack",
"description": "Slack webhook'u burada tanımlayın",
"webhookLabel": "Slack webhook adresi",
"webhookPlaceholder": "https://hooks.slack.com/services/...\n"
},
"pagerdutySettings": {
"title": "",
"description": "",
"integrationKeyLabel": "",
"integrationKeyPlaceholder": ""
},
"genericWebhookSettings": {
"title": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
"title": "PagerDuty",
"description": "PagerDuty entegrasyonunu burada yapın",
"integrationKeyLabel": "Entegrasyon anahtarı",
"integrationKeyPlaceholder": "1234567890"
},
"discordSettings": {
"title": "",
"title": "Discord",
"description": "Discord webhook'u buradan ayarlayın",
"webhookLabel": "Discord Webhook adresi",
"webhookPlaceholder": "https://sunucu-adı.com/webhook\n"
},
"webhookSettings": {
"title": "Webhook",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
"webhookPlaceholder": "https://sunucu-adı.com/webhook\n"
}
},
"notificationConfig": {
"title": "",
"title": "Bildirimler",
"description": ""
},
"monitorStatus": {
"checkingEvery": "",
"withCaptureAgent": "",
"up": "",
"down": "",
"paused": ""
"up": "ayakta",
"down": "çalışmıyor",
"paused": "beklemede"
},
"advancedMatching": "",
"settingsEmailTLSServername": "",
"settingsEmailIgnoreTLS": "",
"settingsEmailRequireTLS": "",
"settingsEmailRejectUnauthorized": "",
"settingsEmailSecure": "",
"settingsEmailSecure": "Güvenli - SSL kullan",
"settingsEmailPool": "",
"sendTestNotifications": ""
"sendTestNotifications": "Bildirimleri dene",
"selectAll": "Tümünü seç"
}
+11 -3
View File
@@ -583,6 +583,13 @@
},
"errors": {
"validation": ""
},
"fields": {
"password": {
"errors": {
"incorrect": ""
}
}
}
},
"login": {
@@ -709,13 +716,13 @@
"integrationKeyLabel": "",
"integrationKeyPlaceholder": ""
},
"genericWebhookSettings": {
"discordSettings": {
"title": "",
"description": "",
"webhookLabel": "",
"webhookPlaceholder": ""
},
"discordSettings": {
"webhookSettings": {
"title": "",
"description": "",
"webhookLabel": "",
@@ -740,5 +747,6 @@
"settingsEmailRejectUnauthorized": "",
"settingsEmailSecure": "",
"settingsEmailPool": "",
"sendTestNotifications": ""
"sendTestNotifications": "",
"selectAll": ""
}
+2 -12
View File
@@ -21,6 +21,8 @@ services:
volumes:
- ./certbot/www/:/var/www/certbot/:rw
- ./certbot/conf/:/etc/letsencrypt/:rw
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot -w /var/www/certbot; sleep 12h & wait $${!}; done;'"
server:
image: ghcr.io/bluewave-labs/checkmate:backend-demo
restart: always
@@ -29,21 +31,9 @@ services:
env_file:
- server.env
depends_on:
- redis
- mongodb
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
redis:
image: ghcr.io/bluewave-labs/checkmate:redis-demo
restart: always
volumes:
- ./redis/data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 10s
retries: 5
start_period: 5s
mongodb:
image: ghcr.io/bluewave-labs/checkmate:mongo-demo
restart: always
+23
View File
@@ -0,0 +1,23 @@
import { handleError } from "./controllerUtils.js";
const SERVICE_NAME = "JobQueueController";
class LogController {
constructor(logger) {
this.logger = logger;
}
getLogs = async (req, res, next) => {
try {
const logs = await this.logger.getLogs();
res.success({
msg: "Logs fetched successfully",
data: logs,
});
} catch (error) {
next(handleError(error, SERVICE_NAME, "getLogs"));
return;
}
};
}
export default LogController;
+4
View File
@@ -70,6 +70,10 @@ const StatusPageSchema = mongoose.Schema(
type: Boolean,
default: true,
},
showAdminLoginLink: {
type: Boolean,
default: false,
},
},
{ timestamps: true }
);
@@ -153,6 +153,7 @@ const getStatusPage = async (url) => {
showCharts: 1,
showUptimePercentage: 1,
timezone: 1,
showAdminLoginLink: 1,
url: 1,
},
monitors: {
+7
View File
@@ -36,6 +36,9 @@ import StatusPageController from "./controllers/statusPageController.js";
import QueueRoutes from "./routes/queueRoute.js";
import QueueController from "./controllers/queueController.js";
import LogRoutes from "./routes/logRoutes.js";
import LogController from "./controllers/logController.js";
import NotificationRoutes from "./routes/notificationRoute.js";
import NotificationController from "./controllers/notificationController.js";
@@ -294,6 +297,8 @@ const startApp = async () => {
ServiceRegistry.get(StringService.SERVICE_NAME)
);
const logController = new LogController(logger);
const statusPageController = new StatusPageController(
ServiceRegistry.get(MongoDB.SERVICE_NAME),
ServiceRegistry.get(StringService.SERVICE_NAME)
@@ -320,6 +325,7 @@ const startApp = async () => {
maintenanceWindowController
);
const queueRoutes = new QueueRoutes(queueController);
const logRoutes = new LogRoutes(logController);
const statusPageRoutes = new StatusPageRoutes(statusPageController);
const notificationRoutes = new NotificationRoutes(notificationController);
@@ -372,6 +378,7 @@ const startApp = async () => {
app.use("/api/v1/checks", verifyJWT, checkRoutes.getRouter());
app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRoutes.getRouter());
app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter());
app.use("/api/v1/logs", verifyJWT, logRoutes.getRouter());
app.use("/api/v1/status-page", statusPageRoutes.getRouter());
app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter());
app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter());
+2 -1
View File
@@ -8,6 +8,7 @@ const verifyOwnership = (Model, paramName) => {
return async (req, res, next) => {
const userId = req.user._id;
const documentId = req.params[paramName];
try {
const doc = await Model.findById(documentId);
//If the document is not found, return a 404 error
@@ -35,7 +36,7 @@ const verifyOwnership = (Model, paramName) => {
// If the userID does not match the document's userID, return a 403 error
if (userId.toString() !== doc.userId.toString()) {
const error = new Error(stringService.verifyOwnerUnauthorized);
const error = new Error("Unauthorized");
error.status = 403;
throw error;
}
+38
View File
@@ -0,0 +1,38 @@
const SERVICE_NAME = "verifyTeamAccess";
const verifyTeamAccess = (Model, paramName) => {
return async (req, res, next) => {
try {
const documentId = req.params[paramName];
const doc = await Model.findById(documentId);
if (!doc) {
const error = new Error("Document not found");
error.status = 404;
throw error;
}
if (!req?.user?.teamId || !doc.teamId) {
const error = new Error("Missing team information");
error.status = 400;
throw error;
}
if (req.user.teamId.toString() === doc.teamId.toString()) {
next();
return;
}
const error = new Error("Unauthorized");
error.status = 403;
throw error;
} catch (error) {
error.service = SERVICE_NAME;
error.method = "verifyTeamAccess";
next(error);
return;
}
};
};
export { verifyTeamAccess };
+18
View File
@@ -0,0 +1,18 @@
import { Router } from "express";
import { isAllowed } from "../middleware/isAllowed.js";
class LogRoutes {
constructor(logController) {
this.router = Router();
this.logController = logController;
this.initRoutes();
}
initRoutes() {
this.router.get("/", isAllowed(["admin", "superadmin"]), this.logController.getLogs);
}
getRouter() {
return this.router;
}
}
export default LogRoutes;
+12 -3
View File
@@ -1,7 +1,8 @@
import { Router } from "express";
import { verifyOwnership } from "../middleware/verifyOwnership.js";
import { verifyTeamAccess } from "../middleware/verifyTeamAccess.js";
import Monitor from "../db/models/Monitor.js";
import MaintenanceWindow from "../db/models/MaintenanceWindow.js";
class MaintenanceWindowRoutes {
constructor(maintenanceWindowController) {
this.router = Router();
@@ -24,9 +25,17 @@ class MaintenanceWindowRoutes {
this.router.get("/:id", this.maintenanceWindowController.getMaintenanceWindowById);
this.router.put("/:id", this.maintenanceWindowController.editMaintenanceWindow);
this.router.put(
"/:id",
verifyTeamAccess(MaintenanceWindow, "id"),
this.maintenanceWindowController.editMaintenanceWindow
);
this.router.delete("/:id", this.maintenanceWindowController.deleteMaintenanceWindow);
this.router.delete(
"/:id",
verifyTeamAccess(MaintenanceWindow, "id"),
this.maintenanceWindowController.deleteMaintenanceWindow
);
}
getRouter() {
+5 -1
View File
@@ -2,7 +2,9 @@ import { Router } from "express";
import { isAllowed } from "../middleware/isAllowed.js";
import multer from "multer";
import { fetchMonitorCertificate } from "../controllers/controllerUtils.js";
import Monitor from "../db/models/Monitor.js";
import { verifyOwnership } from "../middleware/verifyOwnership.js";
import { verifyTeamAccess } from "../middleware/verifyTeamAccess.js";
const upload = multer({
storage: multer.memoryStorage(), // Store file in memory as Buffer
});
@@ -62,6 +64,7 @@ class MonitorRoutes {
this.router.delete(
"/:monitorId",
verifyOwnership(Monitor, "monitorId"),
isAllowed(["admin", "superadmin"]),
this.monitorController.deleteMonitor
);
@@ -74,6 +77,7 @@ class MonitorRoutes {
this.router.put(
"/:monitorId",
verifyTeamAccess(Monitor, "monitorId"),
isAllowed(["admin", "superadmin"]),
this.monitorController.editMonitor
);
+13 -3
View File
@@ -1,6 +1,8 @@
import { Router } from "express";
import { verifyJWT } from "../middleware/verifyJWT.js";
import { verifyOwnership } from "../middleware/verifyOwnership.js";
import { verifyTeamAccess } from "../middleware/verifyTeamAccess.js";
import Notification from "../db/models/Notification.js";
class NotificationRoutes {
constructor(notificationController) {
this.router = Router();
@@ -18,10 +20,18 @@ class NotificationRoutes {
this.router.get("/team", this.notificationController.getNotificationsByTeamId);
this.router.delete("/:id", this.notificationController.deleteNotification);
this.router.delete(
"/:id",
verifyOwnership(Notification, "id"),
this.notificationController.deleteNotification
);
this.router.get("/:id", this.notificationController.getNotificationById);
this.router.put("/:id", this.notificationController.editNotification);
this.router.put(
"/:id",
verifyTeamAccess(Notification, "id"),
this.notificationController.editNotification
);
}
getRouter() {
+33 -18
View File
@@ -4,6 +4,8 @@ dotenv.config();
class Logger {
constructor() {
this.logCache = [];
this.maxCacheSize = 1000;
const consoleFormat = format.printf(
({ level, message, service, method, details, timestamp, stack }) => {
if (message instanceof Object) {
@@ -72,11 +74,9 @@ class Logger {
* @param {Object} config.details - Additional details.
*/
info(config) {
this.logger.info(config.message, {
service: config.service,
method: config.method,
details: config.details,
});
const logEntry = this.buildLogEntry("info", config);
this.cacheLog(logEntry);
this.logger.info(config.message, logEntry);
}
/**
@@ -88,11 +88,9 @@ class Logger {
* @param {Object} config.details - Additional details.
*/
warn(config) {
this.logger.warn(config.message, {
service: config.service,
method: config.method,
details: config.details,
});
const logEntry = this.buildLogEntry("warn", config);
this.cacheLog(logEntry);
this.logger.warn(config.message, logEntry);
}
/**
@@ -104,12 +102,9 @@ class Logger {
* @param {Object} config.details - Additional details.
*/
error(config) {
this.logger.error(config.message, {
service: config.service,
method: config.method,
details: config.details,
stack: config.stack,
});
const logEntry = this.buildLogEntry("error", config);
this.cacheLog(logEntry);
this.logger.error(config.message, logEntry);
}
/**
* Logs a debug message.
@@ -120,12 +115,32 @@ class Logger {
* @param {Object} config.details - Additional details.
*/
debug(config) {
this.logger.debug(config.message, {
const logEntry = this.buildLogEntry("debug", config);
this.cacheLog(logEntry);
this.logger.debug(config.message, logEntry);
}
cacheLog(entry) {
this.logCache.push(entry);
if (this.logCache.length > this.maxCacheSize) {
this.logCache.shift();
}
}
getLogs() {
return this.logCache;
}
buildLogEntry(level, config) {
return {
level,
message: config.message,
service: config.service,
method: config.method,
details: config.details,
stack: config.stack,
});
timestamp: new Date().toISOString(),
};
}
}
+1
View File
@@ -463,6 +463,7 @@ const createStatusPageBodyValidation = joi.object({
isPublished: joi.boolean(),
showCharts: joi.boolean().optional(),
showUptimePercentage: joi.boolean(),
showAdminLoginLink: joi.boolean().optional(),
});
const imageValidation = joi