Compare commits

..
Author SHA1 Message Date
Alex Holliday 69e07975e3 bump dependencies 2026-03-05 13:57:45 -08:00
Alex Holliday c5c9a1a88b fix missing await 2026-03-05 13:57:39 -08:00
Alexander Holliday 31efa791bd Merge pull request #3377 from bluewave-labs/fix/providers
fix/providers
2026-03-05 13:23:27 -08:00
Alex Holliday f8c0b57d21 remove unused imports 2026-03-05 19:37:32 +00:00
Alex Holliday 605c969d2f merge commit 2026-03-05 19:37:03 +00:00
Alex Holliday ad7d4e54c3 cleanup 2026-03-05 19:34:57 +00:00
Alex Holliday c7689cc64a grpc 2026-03-05 19:18:29 +00:00
Alex Holliday 27c44a0fbd config 2026-03-05 18:48:19 +00:00
Alex Holliday 190d9dd2b1 gameProvider 2026-03-05 18:45:27 +00:00
Alex Holliday 49672db563 port provider 2026-03-05 18:35:19 +00:00
Alex Holliday 80603c9a42 port provider 2026-03-05 18:30:06 +00:00
Alex Holliday f3b156f2d0 docker provider 2026-03-05 18:19:03 +00:00
Alex Holliday ec82efebb4 pagespeed provider, hardware provider 2026-03-05 17:54:16 +00:00
Alex Holliday 8dd846d6cd harden http provider 2026-03-04 23:36:36 +00:00
Alex Holliday 82175f166f pingProvider, httpProvider 2026-03-04 23:24:48 +00:00
Alex Holliday 3c99809e8e rix reveresed checks 2026-03-04 23:24:05 +00:00
Alexander Holliday 26db2b3a41 Merge pull request #3371 from bluewave-labs/fix/infrastructure-threshold-migration
Fix infrastructure threshold defaults via migration (#3327)
2026-03-04 14:28:49 -08:00
Alexander Holliday 232a71c502 Merge pull request #3369 from bluewave-labs/fix/swagger-dynamic-server-url
fix: dynamically set Swagger UI server URL from request
2026-03-04 14:28:06 -08:00
gorkem-bwl 8df263fd93 fix: also migrate threshold value of 0 to 100, fix formatting
Addresses review feedback — thresholds set to 0 also caused issues
and should be migrated to 100 (disabled) alongside the value of 5.
2026-03-04 16:44:20 -05:00
gorkem-bwl d2f3335b1a fix: add type annotations to Swagger middleware parameters
Fixes TypeScript build error TS7006 for implicit 'any' types on req, res, next.
2026-03-04 16:32:01 -05:00
Alexander Holliday 7bfd68e374 Merge pull request #3372 from bluewave-labs/fix/ipv6-support
Fix IPv6 address handling in ping monitor (#3053)
2026-03-04 13:00:15 -08:00
gorkem-bwl b2e2b11ef2 fix: support IPv6 addresses in ping monitor
## Changes
- Fixed ping host sanitization regex that stripped everything after
  the first colon, which destroyed IPv6 addresses like 2001:db8::1
- New regex properly handles bracketed IPv6 ([::1]:port), bare IPv6,
  and IPv4/hostname:port formats separately

## Closes
Fixes #3053
2026-03-04 15:41:01 -05:00
gorkem-bwl 93b9abd508 fix: add migration to reset infrastructure thresholds from 5 to 100
## Changes
- Added migration 0004_fixInfrastructureThresholds that finds hardware
  monitors with threshold values of 5 (the old incorrect default) and
  updates them to 100 (effectively disabled)
- Runs automatically on server startup like other migrations

## Closes
Fixes #3327
2026-03-04 15:38:34 -05:00
gorkem-bwl 888b399a4f fix: dynamically set Swagger UI server URL from request
## Changes
- Swagger UI now prepends the current server URL (protocol + host) to
  the servers list, so it works correctly in remote/Docker environments
  instead of always pointing to localhost.

## Closes
Fixes #3339
2026-03-04 15:29:44 -05:00
34 changed files with 1922 additions and 1703 deletions

No files matched your search

+138 -170
View File
@@ -58,7 +58,7 @@
"lint-staged": "^16.2.7",
"prettier": "^3.3.3",
"typescript": "5.9.3",
"vite": "6.3.6"
"vite": "6.4.1"
}
},
"node_modules/@babel/code-frame": {
@@ -83,6 +83,7 @@
"node_modules/@babel/core": {
"version": "7.29.0",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -353,6 +354,7 @@
"node_modules/@emotion/react": {
"version": "11.14.0",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -390,6 +392,7 @@
"node_modules/@emotion/styled": {
"version": "11.14.1",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -434,7 +437,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -451,7 +453,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -468,7 +469,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -485,7 +485,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -502,7 +501,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -519,7 +517,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -536,7 +533,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -553,7 +549,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -570,7 +565,6 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -587,7 +581,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -604,7 +597,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -621,7 +613,6 @@
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -638,7 +629,6 @@
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -655,7 +645,6 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -672,7 +661,6 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -689,7 +677,6 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -704,7 +691,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -721,7 +707,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -738,7 +723,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -755,7 +739,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -772,7 +755,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -789,7 +771,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -806,7 +787,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -823,7 +803,6 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -840,7 +819,6 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -857,7 +835,6 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1185,6 +1162,7 @@
"node_modules/@mui/material": {
"version": "7.3.7",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4",
"@mui/core-downloads-tracker": "^7.3.7",
@@ -1289,6 +1267,7 @@
"node_modules/@mui/system": {
"version": "7.3.7",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.28.4",
"@mui/private-theming": "^7.3.7",
@@ -1660,13 +1639,12 @@
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz",
"integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
"integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1674,13 +1652,12 @@
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz",
"integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
"integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1688,13 +1665,12 @@
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz",
"integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
"integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1702,13 +1678,12 @@
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz",
"integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
"integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1716,13 +1691,12 @@
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz",
"integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
"integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1730,13 +1704,12 @@
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz",
"integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
"integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1744,13 +1717,12 @@
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz",
"integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
"integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1758,13 +1730,12 @@
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz",
"integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
"integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1772,13 +1743,12 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz",
"integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
"integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1786,13 +1756,12 @@
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz",
"integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
"integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1800,13 +1769,12 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz",
"integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
"integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1814,13 +1782,12 @@
]
},
"node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz",
"integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
"integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1828,13 +1795,12 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz",
"integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
"integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1842,13 +1808,12 @@
]
},
"node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz",
"integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
"integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1856,13 +1821,12 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz",
"integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
"integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1870,13 +1834,12 @@
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz",
"integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
"integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1884,13 +1847,12 @@
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz",
"integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
"integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1898,11 +1860,12 @@
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.57.1",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz",
"integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1910,13 +1873,12 @@
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz",
"integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
"integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1924,13 +1886,12 @@
]
},
"node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz",
"integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
"integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1938,13 +1899,12 @@
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz",
"integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
"integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1952,13 +1912,12 @@
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz",
"integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
"integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1966,13 +1925,12 @@
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz",
"integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
"integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1980,13 +1938,12 @@
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz",
"integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
"integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -1994,13 +1951,12 @@
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.57.1",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz",
"integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==",
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
"integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -2350,8 +2306,9 @@
},
"node_modules/@types/node": {
"version": "24.5.2",
"dev": true,
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.12.0"
}
@@ -2366,8 +2323,8 @@
},
"node_modules/@types/react": {
"version": "18.3.27",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.2.2"
@@ -2489,6 +2446,7 @@
"version": "8.15.0",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2505,7 +2463,9 @@
}
},
"node_modules/ajv": {
"version": "6.12.6",
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz",
"integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2811,6 +2771,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -3242,7 +3203,8 @@
},
"node_modules/dayjs": {
"version": "1.11.13",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/debug": {
"version": "4.4.3",
@@ -3566,7 +3528,6 @@
},
"node_modules/esbuild": {
"version": "0.25.12",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
@@ -3625,6 +3586,7 @@
"version": "8.57.1",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -3884,7 +3846,6 @@
},
"node_modules/fdir": {
"version": "6.5.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
@@ -4018,7 +3979,6 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -4371,6 +4331,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6"
},
@@ -5154,6 +5115,7 @@
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz",
"integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==",
"license": "BSD-3-Clause",
"peer": true,
"dependencies": {
"@mapbox/geojson-rewind": "^0.5.2",
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
@@ -5251,7 +5213,9 @@
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz",
"integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==",
"dev": true,
"license": "ISC",
"dependencies": {
@@ -5317,7 +5281,6 @@
},
"node_modules/nanoid": {
"version": "3.3.11",
"dev": true,
"funding": [
{
"type": "github",
@@ -5650,7 +5613,6 @@
},
"node_modules/postcss": {
"version": "8.5.6",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -5801,6 +5763,7 @@
"node_modules/react-dom": {
"version": "18.3.1",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -5812,6 +5775,7 @@
"node_modules/react-hook-form": {
"version": "7.71.1",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -6011,7 +5975,8 @@
},
"node_modules/redux": {
"version": "5.0.1",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/redux-persist": {
"version": "6.0.0",
@@ -6157,9 +6122,11 @@
"license": "Unlicense"
},
"node_modules/rollup": {
"version": "4.57.1",
"dev": true,
"version": "4.59.0",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.8"
},
@@ -6171,31 +6138,31 @@
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.57.1",
"@rollup/rollup-android-arm64": "4.57.1",
"@rollup/rollup-darwin-arm64": "4.57.1",
"@rollup/rollup-darwin-x64": "4.57.1",
"@rollup/rollup-freebsd-arm64": "4.57.1",
"@rollup/rollup-freebsd-x64": "4.57.1",
"@rollup/rollup-linux-arm-gnueabihf": "4.57.1",
"@rollup/rollup-linux-arm-musleabihf": "4.57.1",
"@rollup/rollup-linux-arm64-gnu": "4.57.1",
"@rollup/rollup-linux-arm64-musl": "4.57.1",
"@rollup/rollup-linux-loong64-gnu": "4.57.1",
"@rollup/rollup-linux-loong64-musl": "4.57.1",
"@rollup/rollup-linux-ppc64-gnu": "4.57.1",
"@rollup/rollup-linux-ppc64-musl": "4.57.1",
"@rollup/rollup-linux-riscv64-gnu": "4.57.1",
"@rollup/rollup-linux-riscv64-musl": "4.57.1",
"@rollup/rollup-linux-s390x-gnu": "4.57.1",
"@rollup/rollup-linux-x64-gnu": "4.57.1",
"@rollup/rollup-linux-x64-musl": "4.57.1",
"@rollup/rollup-openbsd-x64": "4.57.1",
"@rollup/rollup-openharmony-arm64": "4.57.1",
"@rollup/rollup-win32-arm64-msvc": "4.57.1",
"@rollup/rollup-win32-ia32-msvc": "4.57.1",
"@rollup/rollup-win32-x64-gnu": "4.57.1",
"@rollup/rollup-win32-x64-msvc": "4.57.1",
"@rollup/rollup-android-arm-eabi": "4.59.0",
"@rollup/rollup-android-arm64": "4.59.0",
"@rollup/rollup-darwin-arm64": "4.59.0",
"@rollup/rollup-darwin-x64": "4.59.0",
"@rollup/rollup-freebsd-arm64": "4.59.0",
"@rollup/rollup-freebsd-x64": "4.59.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
"@rollup/rollup-linux-arm-musleabihf": "4.59.0",
"@rollup/rollup-linux-arm64-gnu": "4.59.0",
"@rollup/rollup-linux-arm64-musl": "4.59.0",
"@rollup/rollup-linux-loong64-gnu": "4.59.0",
"@rollup/rollup-linux-loong64-musl": "4.59.0",
"@rollup/rollup-linux-ppc64-gnu": "4.59.0",
"@rollup/rollup-linux-ppc64-musl": "4.59.0",
"@rollup/rollup-linux-riscv64-gnu": "4.59.0",
"@rollup/rollup-linux-riscv64-musl": "4.59.0",
"@rollup/rollup-linux-s390x-gnu": "4.59.0",
"@rollup/rollup-linux-x64-gnu": "4.59.0",
"@rollup/rollup-linux-x64-musl": "4.59.0",
"@rollup/rollup-openbsd-x64": "4.59.0",
"@rollup/rollup-openharmony-arm64": "4.59.0",
"@rollup/rollup-win32-arm64-msvc": "4.59.0",
"@rollup/rollup-win32-ia32-msvc": "4.59.0",
"@rollup/rollup-win32-x64-gnu": "4.59.0",
"@rollup/rollup-win32-x64-msvc": "4.59.0",
"fsevents": "~2.3.2"
}
},
@@ -6530,7 +6497,6 @@
},
"node_modules/source-map-js": {
"version": "1.2.1",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -6818,7 +6784,6 @@
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
@@ -6948,8 +6913,9 @@
},
"node_modules/typescript": {
"version": "5.9.3",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -6992,7 +6958,7 @@
},
"node_modules/undici-types": {
"version": "7.12.0",
"dev": true,
"devOptional": true,
"license": "MIT"
},
"node_modules/union-value": {
@@ -7081,9 +7047,11 @@
}
},
"node_modules/vite": {
"version": "6.3.6",
"dev": true,
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -7366,7 +7334,7 @@
"version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"dev": true,
"devOptional": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"
+1 -1
View File
@@ -66,6 +66,6 @@
"lint-staged": "^16.2.7",
"prettier": "^3.3.3",
"typescript": "5.9.3",
"vite": "6.3.6"
"vite": "6.4.1"
}
}
@@ -32,7 +32,7 @@ export const HeatmapResponseTime = ({
if (!checks || checks.length === 0) return null;
const latestChecks = checks.slice(-25).reverse();
const latestChecks = checks.slice(-25);
let data: Array<CheckSnapshot | PlaceholderCheck>;
if (latestChecks.length !== 25) {
-4
View File
@@ -40,8 +40,6 @@ export const useMonitorForm = ({
matchMethod: data?.matchMethod || "",
expectedValue: data?.expectedValue || "",
jsonPath: data?.jsonPath || "",
basicAuthUser: data?.basicAuthUser || "",
basicAuthPassword: data?.basicAuthPassword || "",
};
break;
case "ping":
@@ -115,8 +113,6 @@ export const useMonitorForm = ({
matchMethod: "",
expectedValue: "",
jsonPath: "",
basicAuthUser: "",
basicAuthPassword: "",
};
}
-44
View File
@@ -770,50 +770,6 @@ const CreateMonitorPage = () => {
/>
)}
{watchedType === "http" && (
<ConfigBox
title={t("pages.createMonitor.form.basicAuth.title")}
subtitle={t("pages.createMonitor.form.basicAuth.description")}
rightContent={
<Stack spacing={theme.spacing(LAYOUT.MD)}>
<Controller
name="basicAuthUser"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
fieldLabel={t(
"pages.createMonitor.form.basicAuth.option.username.label"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
<Controller
name="basicAuthPassword"
control={control}
render={({ field, fieldState }) => (
<TextField
{...field}
value={field.value ?? ""}
type="password"
fieldLabel={t(
"pages.createMonitor.form.basicAuth.option.password.label"
)}
fullWidth
error={!!fieldState.error}
helperText={fieldState.error?.message ?? ""}
/>
)}
/>
</Stack>
}
/>
)}
{watchedType === "http" && (
<ConfigBox
title={t("pages.createMonitor.form.advanced.title")}
-2
View File
@@ -44,8 +44,6 @@ export interface Monitor {
jsonPath?: string;
expectedValue?: string;
matchMethod?: MonitorMatchMethod;
basicAuthUser?: string;
basicAuthPassword?: string;
url: string;
port?: number;
isActive: boolean;
-2
View File
@@ -38,8 +38,6 @@ const httpSchema = baseSchema.extend({
matchMethod: z.enum(["equal", "include", "regex", ""]).optional(),
expectedValue: z.string().optional(),
jsonPath: z.string().optional(),
basicAuthUser: z.string().optional(),
basicAuthPassword: z.string().optional(),
});
// Ping monitor schema
-12
View File
@@ -501,18 +501,6 @@
"hardware": "Monitor CPU, memory, disk usage, and temperature for your infrastructure."
}
},
"basicAuth": {
"description": "Configure HTTP basic authentication credentials for this monitor.",
"option": {
"username": {
"label": "Username"
},
"password": {
"label": "Password"
}
},
"title": "Basic authentication"
},
"ignoreTls": {
"description": "Configure TLS/SSL certificate validation for HTTPS connections.",
"option": {
+700 -687
View File
File diff suppressed because it is too large. Load diff
+2 -2
View File
@@ -38,7 +38,7 @@
"dotenv": "^16.4.5",
"express": "^4.19.2",
"express-rate-limit": "8.0.1",
"gamedig": "^5.3.1",
"gamedig": "5.2.0",
"got": "14.4.7",
"handlebars": "^4.7.8",
"helmet": "^8.0.0",
@@ -51,7 +51,7 @@
"mjml": "^5.0.0-alpha.4",
"mongoose": "^8.3.3",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.14",
"nodemailer": "8.0.1",
"ping": "0.4.4",
"sharp": "0.33.5",
"ssl-checker": "2.0.10",
+16 -2
View File
@@ -74,8 +74,22 @@ export const createApp = ({
},
})
);
// Swagger UI
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec));
// Swagger UI — dynamically set server URL from request
app.use("/api-docs", swaggerUi.serve, (req: express.Request, res: express.Response, next: express.NextFunction) => {
const protocol = req.protocol;
const host = req.get("host");
const dynamicSpec = {
...openApiSpec,
servers: [
{
url: `${protocol}://${host}/api/v1`,
description: "Current Server",
},
...openApiSpec.servers,
],
};
swaggerUi.setup(dynamicSpec)(req, res, next);
});
app.use("/api/v1/health", (req, res) => {
res.json({
+28 -13
View File
@@ -29,7 +29,6 @@ import { NotificationMessageBuilder, INotificationMessageBuilder } from "../serv
import axios from "axios";
import got from "got";
import ping from "ping";
import https from "https";
import Docker from "dockerode";
import net from "net";
import fs from "fs";
@@ -75,6 +74,15 @@ import {
} from "@/repositories/index.js";
import { ILogger } from "@/utils/logger.js";
import { EnvConfig } from "@/service/system/settingsService.js";
import { PingProvider } from "@/service/infrastructure/network/PingProvider.js";
import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js";
import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js";
import { PageSpeedProvider } from "@/service/infrastructure/network/PageSpeedProvider.js";
import { HardwareProvider } from "@/service/infrastructure/network/HardwareProvider.js";
import { DockerProvider } from "@/service/infrastructure/network/DockerProvider.js";
import { PortProvider } from "@/service/infrastructure/network/PortProvider.js";
import { GameProvider } from "@/service/infrastructure/network/GameProvider.js";
import { GrpcProvider } from "@/service/infrastructure/network/GrpcProvider.js";
export type InitializedServices = {
settingsService: any;
@@ -144,21 +152,28 @@ export const initializeServices = async ({
const teamsRepository = new MongoTeamsRepository();
const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
// Providers
const pingProvider = new PingProvider(ping);
const httpProvider = new HttpProvider(got, new AdvancedMatcher(jmespath));
const pageSpeedProvider = new PageSpeedProvider(httpProvider, settingsService, logger);
const hardwareProvider = new HardwareProvider(httpProvider);
const dockerProvider = new DockerProvider(logger, Docker);
const portProvider = new PortProvider(net);
const gameProvider = new GameProvider(logger, GameDig);
const grpcProvider = new GrpcProvider(grpc, protoLoader);
const networkService = new NetworkService(
axios,
got,
https,
jmespath,
GameDig as unknown as {
query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }>;
},
ping,
logger,
Docker,
net,
settingsService,
grpc,
protoLoader
pingProvider,
httpProvider,
pageSpeedProvider,
hardwareProvider,
dockerProvider,
portProvider,
gameProvider,
grpcProvider
);
const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger);
@@ -0,0 +1,60 @@
import { MonitorModel } from "../models/Monitor.js";
import { logger } from "@/utils/logger.js";
/**
* Fix infrastructure monitors that had thresholds incorrectly set to 0 or 5
* due to the old default value. Updates them to 100 (disabled).
*/
export async function fixInfrastructureThresholds(): Promise<void> {
const SERVICE_NAME = "Migration:FixInfrastructureThresholds";
try {
logger.info({ service: SERVICE_NAME, message: "Starting infrastructure threshold fix" });
const thresholdFields = ["cpuAlertThreshold", "memoryAlertThreshold", "diskAlertThreshold", "tempAlertThreshold"];
// Find hardware monitors with any threshold set to 0 or 5
const filter = {
type: "hardware",
$or: thresholdFields.flatMap((field) => [{ [field]: 0 }, { [field]: 5 }]),
};
const monitors = await MonitorModel.find(filter);
if (monitors.length === 0) {
logger.info({ service: SERVICE_NAME, message: "No monitors with threshold value of 0 or 5 found" });
return;
}
logger.info({
service: SERVICE_NAME,
message: `Found ${monitors.length} monitors with threshold value of 0 or 5`,
});
let updatedCount = 0;
for (const monitor of monitors) {
const update: Record<string, number> = {};
for (const field of thresholdFields) {
if ((monitor as any)[field] === 0 || (monitor as any)[field] === 5) {
update[field] = 100;
}
}
if (Object.keys(update).length > 0) {
await MonitorModel.updateOne({ _id: monitor._id }, { $set: update });
updatedCount++;
}
}
logger.info({
service: SERVICE_NAME,
message: `Fixed ${updatedCount} monitors — thresholds updated from 0/5 to 100`,
});
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
logger.error({ service: SERVICE_NAME, message: `Error fixing thresholds: ${errorMessage}` });
throw error;
}
}
+2
View File
@@ -1,6 +1,7 @@
import { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js";
import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js";
import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js";
import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js";
import MigrationModel from "../models/Migration.js";
type MigrationEntry = {
@@ -12,6 +13,7 @@ const migrations: MigrationEntry[] = [
{ name: "0001_migrateStatusWindowThreshold", execute: migrateStatusWindowThreshold },
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
{ name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats },
{ name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds },
];
const runMigrations = async (logger?: { info: Function; error: Function }) => {
-6
View File
@@ -93,12 +93,6 @@ const MonitorSchema = new Schema<MonitorDocument>(
type: String,
enum: ["equal", "include", "regex", ""],
},
basicAuthUser: {
type: String,
},
basicAuthPassword: {
type: String,
},
url: {
type: String,
required: true,
@@ -20,12 +20,19 @@ export interface IChecksRepository {
monitorId: string,
sortOrder: string,
dateRange: string,
filter: string,
filter: string | undefined,
page: number,
rowsPerPage: number,
status: boolean | undefined
): Promise<ChecksQueryResult>;
findByTeamId(sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string): Promise<ChecksQueryResult>;
findByTeamId(
sortOrder: string,
dateRange: string,
filter: string | undefined,
page: number,
rowsPerPage: number,
teamId: string
): Promise<ChecksQueryResult>;
findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap>;
findByDateRangeAndMonitorId(
monitorId: string,
@@ -231,7 +231,7 @@ class MongoChecksRepository implements IChecksRepository {
monitorId: string,
sortOrder: string,
dateRange: string,
filter: string,
filter: string | undefined,
page: number,
rowsPerPage: number,
status: boolean | undefined
+8 -3
View File
@@ -124,8 +124,13 @@ class CheckService implements ICheckService {
await this.monitorsRepository.findById(monitorId, teamId);
const { sortOrder, dateRange, filter, page, rowsPerPage, status } = query;
if (!sortOrder || !dateRange || !filter || !page || !rowsPerPage) {
throw new AppError({ message: "Missing required query parameters", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 });
if (!sortOrder || !dateRange || !page || !rowsPerPage) {
throw new AppError({
message: `Missing required query parameters sortOrder: ${sortOrder}, dateRange: ${dateRange}, filter: ${filter}, page: ${page}, rowsPerPage: ${rowsPerPage}`,
service: SERVICE_NAME,
method: "getChecksByMonitor",
status: 400,
});
}
const parsedStatus = typeof status === "undefined" ? status : ParseBoolean(status);
const parsedPage = page ? parseInt(page) : 0;
@@ -138,7 +143,7 @@ class CheckService implements ICheckService {
getChecksByTeam = async ({ teamId, query }: { teamId: string; query: Record<string, string> }) => {
const { sortOrder, dateRange, filter, page, rowsPerPage } = query;
if (!sortOrder || !dateRange || !filter || !page || !rowsPerPage) {
if (!sortOrder || !dateRange || !page || !rowsPerPage) {
throw new AppError({ message: "Missing required query parameters", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 });
}
@@ -94,7 +94,7 @@ class EmailService implements IEmailService {
if (!mjml) {
throw new Error(`Template ${template} not found`);
}
const html = this.mjml2html(mjml);
const html = await this.mjml2html(mjml);
return html.html;
} catch (error: unknown) {
this.logger.error({
@@ -0,0 +1,45 @@
import { Monitor } from "@/types/monitor.js";
import jmespath from "jmespath";
type JmesPath = typeof jmespath;
export class AdvancedMatcher {
constructor(private jmespath: JmesPath) {}
private compare(actual: unknown, expected: string, method?: string): boolean {
if (method === "equal") return String(actual) === expected;
if (method === "include") return String(actual).includes(expected);
if (method === "regex") return new RegExp(expected).test(String(actual));
return String(actual) === expected; // Default
}
validate<T>(payload: T, monitor: Monitor): { ok: boolean; message: string; extracted?: any } {
const { useAdvancedMatching, jsonPath, matchMethod, expectedValue } = monitor;
if (!useAdvancedMatching) return { ok: true, message: "Success" };
let dataToValidate = payload;
if (jsonPath) {
try {
dataToValidate = this.jmespath.search(payload, jsonPath);
} catch {
return { ok: false, message: "Error evaluating JSON path" };
}
}
if (expectedValue) {
const ok = this.compare(dataToValidate, expectedValue, matchMethod);
return {
ok,
message: ok ? "Success" : "Expected value did not match",
extracted: dataToValidate,
};
}
const isFalsey = dataToValidate === false || dataToValidate === "false" || dataToValidate === undefined || dataToValidate === null;
return {
ok: !isFalsey,
message: !isFalsey ? "Success" : "Extracted value is falsey",
extracted: dataToValidate,
};
}
}
@@ -0,0 +1,160 @@
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
import { DockerStatusPayload, MonitorStatusResponse } from "@/types/network.js";
import { Monitor, MonitorType } from "@/types/monitor.js";
import { ILogger } from "@/utils/logger.js";
import { AppError } from "@/utils/AppError.js";
import Dockerode from "dockerode";
import { NETWORK_ERROR, timeRequest } from "@/service/infrastructure/network/utils.js";
type DockerodeType = typeof Dockerode;
const SERVICE_NAME = "DockerProvider";
export interface DockerError extends Error {
statusCode?: number;
reason?: string;
json?: { message?: string };
}
export class DockerProvider implements IStatusProvider<DockerStatusPayload> {
readonly type = "docker";
private docker: Dockerode;
constructor(
private logger: ILogger,
private DockerLib: DockerodeType
) {
this.docker = new this.DockerLib();
}
supports(type: MonitorType): boolean {
return type === "docker";
}
private isDockerError(error: unknown): error is DockerError {
return error instanceof Error && ("statusCode" in error || "reason" in error || "json" in error);
}
async handle(monitor: Monitor): Promise<MonitorStatusResponse<DockerStatusPayload>> {
const { url: containerInput } = monitor;
try {
if (!containerInput) {
throw new Error("Container name or ID is required for Docker monitor");
}
const containers = await this.docker.listContainers({ all: true });
const normalizedInput = containerInput.replace(/^\/+/, "").toLowerCase();
// Priority-based matching to avoid ambiguity:
// 1. Exact full ID match (64-char)
const exactIdMatch = containers.find((c) => c.Id.toLowerCase() === normalizedInput);
// 2. Exact container name match (case-insensitive)
const exactNameMatch = containers.find((c) =>
c.Names.some((name: string) => {
const cleanName = name.replace(/^\/+/, "").toLowerCase();
return cleanName === normalizedInput;
})
);
// 3. Partial ID match (fallback for backwards compatibility)
const partialIdMatch = containers.find((c) => c.Id.toLowerCase().startsWith(normalizedInput));
// Select container based on priority
const targetContainer = exactIdMatch || exactNameMatch || partialIdMatch;
// Handle no match
if (!targetContainer) {
this.logger.warn({
message: `No container found for "${monitor.url}".`,
service: SERVICE_NAME,
method: "handle",
details: { url: monitor.url },
});
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: false,
code: 404,
message: "Docker container not found",
responseTime: 0,
payload: null,
};
}
// 5. Handle Ambiguity Check
const matchTypes: string[] = [];
if (exactIdMatch) matchTypes.push("exact ID");
if (exactNameMatch) matchTypes.push("exact name");
if (partialIdMatch && !exactIdMatch) matchTypes.push("partial ID");
if (matchTypes.length > 1) {
const message = `Ambiguous container match for "${containerInput}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`;
this.logger.warn({
message,
service: SERVICE_NAME,
method: "handle",
details: { url: containerInput },
});
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: false,
code: NETWORK_ERROR,
message,
responseTime: 0,
payload: null,
};
}
// 6. Inspect Container Status
const container = this.docker.getContainer(targetContainer.Id);
const { response, responseTime, error } = await timeRequest(() => container.inspect());
if (error) {
let message = "Failed to fetch Docker container information";
let code = NETWORK_ERROR;
if (this.isDockerError(error)) {
code = error.statusCode ?? NETWORK_ERROR;
message = error.json?.message ?? error.reason ?? error.message;
} else if (error instanceof Error) {
message = error.message;
}
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: false,
code: code,
message: message,
responseTime,
payload: null,
};
}
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: response?.State?.Status === "running",
code: 200,
message: "Docker container status fetched successfully",
responseTime,
payload: response as unknown as DockerStatusPayload,
};
} catch (err: unknown) {
throw new AppError({
message: err instanceof Error ? err.message : "Error performing Docker request",
service: SERVICE_NAME,
method: "handle",
details: { url: containerInput },
});
}
}
}
@@ -0,0 +1,78 @@
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
import { GameStatusPayload, MonitorStatusResponse } from "@/types/network.js";
import { Monitor, MonitorType } from "@/types/monitor.js";
import { GameDig } from "gamedig";
import { AppError } from "@/utils/AppError.js";
import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js";
import { ILogger } from "@/utils/logger.js";
type GameDigType = typeof GameDig;
const SERVICE_NAME = "GameProvider";
export class GameProvider implements IStatusProvider<GameStatusPayload> {
readonly type = "game";
constructor(
private logger: ILogger,
private gameDig: GameDigType
) {}
supports(type: MonitorType): boolean {
return type === "game";
}
async handle(monitor: Monitor): Promise<MonitorStatusResponse<GameStatusPayload>> {
const { url, port, gameId, id, teamId, type } = monitor;
// Clean the host in case a full URL was passed
const host = url?.replace(/^https?:\/\//, "").split(/[/?#:]/)[0] || "";
try {
const state = await this.gameDig
.query({
type: gameId ?? "unknown",
host: host,
port: port ?? 0,
})
.catch((error: unknown) => {
this.logger.warn({
message: error instanceof Error ? error.message : String(error),
service: SERVICE_NAME,
method: "handle",
details: { host, port, gameId },
});
return undefined;
});
if (!state) {
return {
monitorId: id,
teamId: teamId,
type: type,
status: false,
code: NETWORK_ERROR,
message: "No response from game server",
responseTime: 0,
payload: null,
};
}
return {
monitorId: id,
teamId: teamId,
type: type,
status: true,
code: 200,
message: "Success",
responseTime: state.ping ?? 0,
payload: state,
};
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing game server check",
service: SERVICE_NAME,
method: "handle",
details: { url: monitor.url, port: monitor.port, gameId: monitor.gameId },
});
}
}
}
@@ -0,0 +1,180 @@
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
import { GrpcStatusPayload, MonitorStatusResponse } from "@/types/network.js";
import { Monitor, MonitorType } from "@/types/monitor.js";
import { AppError } from "@/utils/AppError.js";
import { timeRequest } from "@/service/infrastructure/network/utils.js";
import * as grpc from "@grpc/grpc-js";
import * as protoLoader from "@grpc/proto-loader";
import path from "path";
import { fileURLToPath } from "url";
type GrpcType = typeof grpc;
type ProtoLoaderType = typeof protoLoader;
const SERVICE_NAME = "GrpcProvider";
export class GrpcProvider implements IStatusProvider<GrpcStatusPayload> {
readonly type = "grpc";
constructor(
private grpc: GrpcType,
private protoLoader: ProtoLoaderType
) {}
supports(type: MonitorType): boolean {
return type === "grpc";
}
private getGrpcStatusName(code: number): string {
const statusNames: Record<number, string> = {
0: "OK",
1: "CANCELLED",
2: "UNKNOWN",
3: "INVALID_ARGUMENT",
4: "DEADLINE_EXCEEDED",
5: "NOT_FOUND",
6: "ALREADY_EXISTS",
7: "PERMISSION_DENIED",
8: "RESOURCE_EXHAUSTED",
9: "FAILED_PRECONDITION",
10: "ABORTED",
11: "OUT_OF_RANGE",
12: "UNIMPLEMENTED",
13: "INTERNAL",
14: "UNAVAILABLE",
15: "DATA_LOSS",
16: "UNAUTHENTICATED",
};
return statusNames[code] || "UNKNOWN";
}
async handle(monitor: Monitor): Promise<MonitorStatusResponse<GrpcStatusPayload>> {
try {
const { id, teamId, type, url, port, ignoreTlsErrors } = monitor;
const grpcServiceName = monitor.grpcServiceName || "";
if (!url) {
throw new AppError({ message: "Monitor host is required", service: SERVICE_NAME, method: "handle" });
}
if (!port) {
throw new AppError({ message: "Monitor port is required", service: SERVICE_NAME, method: "handle" });
}
const host = url?.replace(/^https?:\/\//, "").split(/[/?#:]/)[0];
const target = `${host}:${port}`;
const currentFilePath = fileURLToPath(import.meta.url);
const protoPath = path.join(path.dirname(currentFilePath), "protos", "health.proto");
const packageDefinition = this.protoLoader.loadSync(protoPath, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const grpcObject = this.grpc.loadPackageDefinition(packageDefinition) as unknown as {
grpc: {
health: {
v1: {
Health: new (
target: string,
credentials: unknown
) => {
Check: (request: { service: string }, options: { deadline: Date }, callback: (err: unknown, response: unknown) => void) => void;
close: () => void;
};
};
};
};
};
const healthService = grpcObject.grpc.health.v1.Health;
let credentials;
if (ignoreTlsErrors) {
credentials = this.grpc.credentials.createSsl(null, null, null, {
checkServerIdentity: () => undefined,
});
} else {
credentials = this.grpc.credentials.createInsecure();
}
const client = new healthService(target, credentials);
const TIMEOUT_MS = 10000;
const deadline = new Date(Date.now() + TIMEOUT_MS);
const { response, responseTime, error } = await timeRequest<GrpcStatusPayload>(() => {
return new Promise<GrpcStatusPayload>((resolve, reject) => {
client.Check({ service: grpcServiceName }, { deadline }, (err: unknown, response: unknown) => {
client.close();
if (err) {
const grpcErr = err as { code?: number; details?: string; message?: string };
const payload: GrpcStatusPayload = {
grpcStatusCode: grpcErr.code ?? -1,
grpcStatusName: this.getGrpcStatusName(grpcErr.code ?? -1),
serviceName: grpcServiceName,
servingStatus: "UNKNOWN",
};
const grpcError = new AppError({
message: grpcErr.details || grpcErr.message || "gRPC error",
service: SERVICE_NAME,
method: "handle",
}) as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number };
grpcError.grpcPayload = payload;
grpcError.grpcCode = grpcErr.code;
reject(grpcError);
return;
}
const resp = response as { status?: string } | undefined;
const servingStatus = resp?.status ?? "UNKNOWN";
resolve({
grpcStatusCode: 0,
grpcStatusName: "OK",
serviceName: grpcServiceName,
servingStatus,
});
});
});
});
if (error) {
const grpcError = error as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number };
const payload = grpcError.grpcPayload;
return {
monitorId: id,
teamId: teamId,
type: type,
status: false,
code: grpcError.grpcCode ?? 5000,
message: grpcError.message ?? "gRPC health check failed",
responseTime,
payload: payload ?? null,
};
}
const grpcPayload = response as GrpcStatusPayload;
const isServing = grpcPayload.servingStatus === "SERVING";
return {
monitorId: id,
teamId: teamId,
type: type,
status: isServing,
code: isServing ? 200 : 5000,
message: isServing ? `gRPC service healthy (${grpcPayload.servingStatus})` : `gRPC service unhealthy (${grpcPayload.servingStatus})`,
responseTime,
payload: grpcPayload,
};
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing gRPC health check",
service: SERVICE_NAME,
method: "handle",
details: { url: monitor.url, port: monitor.port, grpcServiceName: monitor.grpcServiceName },
});
}
}
}
@@ -0,0 +1,29 @@
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
import { HardwareStatusPayload, MonitorStatusResponse } from "@/types/network.js";
import { Monitor, MonitorType } from "@/types/monitor.js";
import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js";
import { AppError } from "@/utils/AppError.js";
export class HardwareProvider implements IStatusProvider<HardwareStatusPayload> {
readonly type = "hardware";
constructor(private httpProvider: HttpProvider) {}
supports(type: MonitorType) {
return type === "hardware";
}
async handle(monitor: Monitor): Promise<MonitorStatusResponse<HardwareStatusPayload>> {
const { url } = monitor;
try {
if (!url) throw new Error("URL is required for Hardware monitor");
return await this.httpProvider.handle<HardwareStatusPayload>(monitor);
} catch (err: unknown) {
throw new AppError({
message: err instanceof Error ? err.message : "Error performing Hardware request",
service: "HardwareProvider",
method: "handle",
details: { url: monitor.url },
});
}
}
}
@@ -0,0 +1,121 @@
import { type Got, HTTPError, RequestError } from "got";
import { AdvancedMatcher } from "@/service/infrastructure/network/AdvancedMatcher.js";
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
import { HttpStatusPayload } from "@/types/network.js";
import { MonitorStatusResponse } from "@/types/network.js";
import { Agent as HttpsAgent } from "https";
import { Monitor, MonitorType } from "@/types/monitor.js";
import { NETWORK_ERROR } from "@/service/infrastructure/network/utils.js";
import CacheableLookup from "cacheable-lookup";
export class HttpProvider implements IStatusProvider<HttpStatusPayload> {
readonly type = "http";
constructor(
private got: Got,
private advancedMatcher: AdvancedMatcher
) {
const cacheable = new CacheableLookup();
this.got = got.extend({
dnsCache: cacheable,
timeout: {
request: 30000,
},
retry: { limit: 1 },
});
}
supports(type: MonitorType) {
return type === "http";
}
private handleHttpError<T>(error: unknown, monitor: Monitor): MonitorStatusResponse<T> {
if (error instanceof HTTPError || error instanceof RequestError) {
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: false,
code: error.response?.statusCode ?? NETWORK_ERROR,
message: error.message,
responseTime: error.timings?.phases?.total ?? 0,
timings: error.timings,
payload: null as T,
};
}
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: false,
code: NETWORK_ERROR,
message: error instanceof Error ? error.message : String(error),
responseTime: 0,
payload: null as T,
};
}
async handle<T>(monitor: Monitor): Promise<MonitorStatusResponse<T>> {
const { url, secret, jsonPath, ignoreTlsErrors } = monitor;
if (!url) {
throw new Error("URL is required for HTTP monitor");
}
const options: Record<string, unknown> = {
headers: monitor.secret ? { Authorization: `Bearer ${secret}` } : undefined,
};
options.agent = {
https: new HttpsAgent({ rejectUnauthorized: !ignoreTlsErrors }),
};
try {
const response = await this.got<string>(url, options);
const contentType = response.headers["content-type"] || "";
const isJson = contentType.includes("application/json");
if (jsonPath && !isJson) {
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: false,
code: response.statusCode,
message: "Response is not JSON",
responseTime: response.timings.phases.total ?? 0,
timings: response.timings,
payload: response.body as unknown as T,
};
}
let payload: T;
if (isJson) {
try {
payload = JSON.parse(response.body) as T;
} catch {
payload = response.body as unknown as T;
}
} else {
payload = response.body as unknown as T;
}
const matchResult = this.advancedMatcher.validate<T>(payload, monitor);
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: response.ok && matchResult.ok,
code: response.statusCode,
message: matchResult.ok ? (response.statusMessage ?? "OK") : matchResult.message,
responseTime: response.timings.phases.total ?? 0,
timings: response.timings,
payload,
extracted: matchResult.extracted,
};
} catch (error: unknown) {
return this.handleHttpError(error, monitor);
}
}
}
@@ -0,0 +1,8 @@
import { Monitor, MonitorType } from "@/types/monitor.js";
import { MonitorStatusResponse } from "@/types/network.js";
export interface IStatusProvider<T> {
type: string;
supports: (type: MonitorType) => boolean;
handle(monitor: Monitor): Promise<MonitorStatusResponse<T>>;
}
@@ -0,0 +1,55 @@
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
import { MonitorStatusResponse, PageSpeedStatusPayload } from "@/types/network.js";
import { Monitor, MonitorType } from "@/types/monitor.js";
import { HttpProvider } from "@/service/infrastructure/network/HttpProvider.js";
import { ISettingsService } from "@/service/system/settingsService.js";
import { ILogger } from "@/utils/logger.js";
import { AppError } from "@/utils/AppError.js";
export class PageSpeedProvider implements IStatusProvider<PageSpeedStatusPayload> {
readonly type = "pagespeed";
constructor(
private httpProvider: HttpProvider,
private settingsService: ISettingsService,
private logger: ILogger
) {}
supports(type: MonitorType) {
return type === "pagespeed";
}
async handle(monitor: Monitor): Promise<MonitorStatusResponse<PageSpeedStatusPayload>> {
const { url } = monitor;
try {
if (!url) throw new Error("URL is required for PageSpeed monitor");
const dbSettings = await this.settingsService.getDBSettings();
const apiKey = dbSettings?.pagespeedApiKey;
let pageSpeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${encodeURIComponent(
url
)}&category=seo&category=accessibility&category=best-practices&category=performance`;
if (apiKey) {
pageSpeedUrl += `&key=${apiKey}`;
} else {
this.logger.warn({
message: "PageSpeed API key not found, performance may be throttled",
service: "PageSpeedProvider",
method: "handle",
details: { url },
});
}
return await this.httpProvider.handle<PageSpeedStatusPayload>({
...monitor,
url: pageSpeedUrl,
});
} catch (err: unknown) {
throw new AppError({
message: err instanceof Error ? err.message : "Error performing PageSpeed request",
service: "PageSpeedProvider",
method: "handle",
details: { url: monitor.url },
});
}
}
}
@@ -0,0 +1,64 @@
import { PingStatusPayload } from "@/types/network.js";
import { IStatusProvider } from "./IStatusProvider.js";
import { MonitorType, Monitor } from "@/types/monitor.js";
import { MonitorStatusResponse } from "@/types/network.js";
import { AppError } from "@/utils/AppError.js";
import ping from "ping";
import { buildStatusResponse, timeRequest } from "@/service/infrastructure/network/utils.js";
const SERVICE_NAME = "PingProvider";
type Ping = typeof ping;
export class PingProvider implements IStatusProvider<PingStatusPayload> {
readonly type = "ping";
constructor(private ping: Ping) {}
supports(type: MonitorType): boolean {
return type === "ping";
}
private sanitizeHost(url: string): string {
return url
.replace(/^https?:\/\//, "")
.replace(/\/.*$/, "")
.replace(/:.*/, "");
}
async handle(monitor: Monitor): Promise<MonitorStatusResponse<PingStatusPayload>> {
try {
if (!monitor.url) {
throw new Error("URL is required for ping monitor");
}
const sanitizedHost = this.sanitizeHost(monitor.url);
const { response, error } = await timeRequest<PingStatusPayload>(() => this.ping.promise.probe(sanitizedHost));
const safeTime = typeof response?.time === "number" ? response.time : parseFloat(String(response?.time)) || 0;
if (error) {
throw error;
}
if (!response) {
throw new Error(`No response from ping for host: ${sanitizedHost}`);
}
return buildStatusResponse<PingStatusPayload>({
monitor,
payload: response,
overrides: {
status: response.alive ?? false,
code: 200,
message: "Success",
responseTime: safeTime,
},
});
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
throw new AppError({
message,
service: SERVICE_NAME,
method: "handle",
});
}
}
}
@@ -0,0 +1,91 @@
import { IStatusProvider } from "@/service/infrastructure/network/IStatusProvider.js";
import { PortStatusPayload, MonitorStatusResponse } from "@/types/network.js";
import { Monitor, MonitorType } from "@/types/monitor.js";
import { AppError } from "@/utils/AppError.js";
import { NETWORK_ERROR, timeRequest } from "@/service/infrastructure/network/utils.js";
import * as net from "net";
type NetType = typeof net;
const SERVICE_NAME = "PortProvider";
export class PortProvider implements IStatusProvider<PortStatusPayload> {
readonly type = "port";
constructor(private net: NetType) {}
supports(type: MonitorType): boolean {
return type === "port";
}
async handle(monitor: Monitor): Promise<MonitorStatusResponse<PortStatusPayload>> {
try {
const { url, port } = monitor;
if (!url || !port) {
throw new Error("URL and port are required for port monitoring");
}
const { responseTime, error } = await timeRequest(async () => {
return new Promise((resolve, reject) => {
const socket = this.net.createConnection(
{
host: url,
port,
},
() => {
socket.end();
socket.destroy();
resolve({ success: true });
}
);
socket.setTimeout(5000);
socket.on("timeout", () => {
socket.destroy();
reject(new Error("Connection timeout"));
});
socket.on("error", (err: unknown) => {
socket.destroy();
reject(err);
});
});
});
if (error) {
console.log(error);
const errorMessage = error instanceof Error ? error.message : "Port check failed";
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: false,
code: NETWORK_ERROR,
message: errorMessage,
responseTime: responseTime,
timings: undefined,
payload: { success: false },
};
}
return {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: true,
code: 200,
message: "Port check successful",
responseTime: responseTime,
timings: undefined,
payload: { success: true },
};
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing port check",
status: 500,
service: SERVICE_NAME,
method: "handle",
details: { url: monitor.url, port: monitor.port },
});
}
}
}
@@ -0,0 +1,85 @@
import { HTTPError, RequestError } from "got";
import type { Response } from "got";
import type { Monitor } from "@/types/monitor.js";
import { MonitorStatusResponse, MonitorStatusResponseOverrides } from "@/types/network.js";
export const timeRequest = async <T>(operation: () => Promise<T>): 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, error: null };
} catch (error) {
const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000);
return { response: null, responseTime: elapsedMs, error };
}
};
export const NETWORK_ERROR = 5000;
export const PING_ERROR = 5001;
interface BuildStatusResponseArgs<T> {
monitor: Monitor;
response?: Response<T> | null;
error?: Error | RequestError | HTTPError | null;
payload?: T | null;
jsonPath?: string;
matchMethod?: MonitorStatusResponse["matchMethod"];
expectedValue?: string;
extracted?: unknown;
overrides?: MonitorStatusResponseOverrides<T>;
}
export const buildStatusResponse = <T>({
monitor,
response,
error,
payload,
jsonPath,
matchMethod,
expectedValue,
extracted,
overrides,
}: BuildStatusResponseArgs<T>): MonitorStatusResponse<T> => {
if (error) {
const statusResponse: MonitorStatusResponse<T> = {
monitorId: monitor.id,
teamId: monitor.teamId,
type: monitor.type,
status: false,
code: 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 ?? 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 ?? NETWORK_ERROR,
message: response?.statusMessage ?? "",
responseTime: response?.timings?.phases?.total ?? 0,
timings: response?.timings,
payload: payload ?? response?.body,
jsonPath,
matchMethod,
expectedValue,
extracted,
};
};
@@ -1,5 +1,3 @@
import { HTTPError, RequestError } from "got";
import type { Got, Response } from "got";
import type {
Monitor,
MonitorStatusResponse,
@@ -14,28 +12,11 @@ import type {
} from "@/types/index.js";
import type { AxiosStatic } from "axios";
import { AppError } from "@/utils/AppError.js";
import path from "path";
import { fileURLToPath } from "url";
import CacheableLookup from "cacheable-lookup";
import { ISettingsService } from "../system/settingsService.js";
import { ILogger } from "@/utils/logger.js";
import { IStatusProvider } from "./network/IStatusProvider.js";
const SERVICE_NAME = "NetworkService";
type MonitorStatusResponseOverrides<T> = Partial<Omit<MonitorStatusResponse<T>, "monitorId" | "teamId" | "type">>;
interface BuildStatusResponseArgs<T> {
monitor: Monitor;
response?: Response<T> | null;
error?: Error | RequestError | HTTPError | null;
payload?: T | null;
jsonPath?: string;
matchMethod?: MonitorStatusResponse["matchMethod"];
expectedValue?: string;
extracted?: unknown;
overrides?: MonitorStatusResponseOverrides<T>;
}
export interface INetworkService {
readonly serviceName: string;
requestStatus(
@@ -82,85 +63,27 @@ class NetworkService implements INetworkService {
private PING_ERROR: number;
private axios: AxiosStatic;
private got: Got;
private https: typeof import("https");
private jmespath: { search: (data: unknown, expression: string) => unknown };
private GameDig: { query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }> };
private ping: typeof import("ping");
private logger: ILogger;
private Docker: typeof import("dockerode");
private net: typeof import("net");
private settingsService: ISettingsService;
private grpc: typeof import("@grpc/grpc-js");
private protoLoader: typeof import("@grpc/proto-loader");
private buildStatusResponse = <T>({
monitor,
response,
error,
payload,
jsonPath,
matchMethod,
expectedValue,
extracted,
overrides,
}: BuildStatusResponseArgs<T>): MonitorStatusResponse<T> => {
if (error) {
const statusResponse: MonitorStatusResponse<T> = {
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 ?? {}) } as MonitorStatusResponse<T>;
}
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 ?? {}),
} as MonitorStatusResponse<T>;
};
private pingProvider;
private httpProvider;
private pageSpeedProvider;
private hardwareProvider;
private dockerProvider;
private portProvider;
private gameProvider;
private grpcProvider;
constructor(
axios: AxiosStatic,
got: Got,
https: typeof import("https"),
jmespath: { search: (data: unknown, expression: string) => unknown },
GameDig: { query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }> },
ping: typeof import("ping"),
logger: ILogger,
Docker: typeof import("dockerode"),
net: typeof import("net"),
settingsService: ISettingsService,
grpc: typeof import("@grpc/grpc-js"),
protoLoader: typeof import("@grpc/proto-loader")
pingProvider: IStatusProvider<PingStatusPayload>,
httpProvider: IStatusProvider<HttpStatusPayload>,
pagespeedProvider: IStatusProvider<PageSpeedStatusPayload>,
hardwareProvider: IStatusProvider<HardwareStatusPayload>,
dockerProvider: IStatusProvider<DockerStatusPayload>,
portProvider: IStatusProvider<PortStatusPayload>,
gameProvider: IStatusProvider<GameStatusPayload>,
grpcProvider: IStatusProvider<GrpcStatusPayload>
) {
this.TYPE_PING = "ping";
this.TYPE_HTTP = "http";
@@ -174,45 +97,22 @@ class NetworkService implements INetworkService {
this.NETWORK_ERROR = 5000;
this.PING_ERROR = 5001;
this.axios = axios;
this.https = https;
this.jmespath = jmespath;
this.GameDig = GameDig;
this.ping = ping;
this.logger = logger;
this.Docker = Docker;
this.net = net;
this.settingsService = settingsService;
this.grpc = grpc;
this.protoLoader = protoLoader;
const cacheable = new CacheableLookup();
this.got = got.extend({
dnsCache: cacheable,
timeout: {
request: 30000,
},
retry: { limit: 1 },
});
this.pingProvider = pingProvider;
this.httpProvider = httpProvider;
this.pageSpeedProvider = pagespeedProvider;
this.hardwareProvider = hardwareProvider;
this.dockerProvider = dockerProvider;
this.portProvider = portProvider;
this.gameProvider = gameProvider;
this.grpcProvider = grpcProvider;
}
get serviceName(): string {
return NetworkService.SERVICE_NAME;
}
// Helper functions
private async timeRequest<T>(operation: () => Promise<T>): 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, error: null };
} catch (error) {
const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000);
return { response: null, responseTime: elapsedMs, error };
}
}
// Main entry point
async requestStatus(
monitor: Monitor
@@ -231,635 +131,26 @@ class NetworkService implements INetworkService {
const type = monitor?.type || "unknown";
switch (type) {
case this.TYPE_PING:
return await this.requestPing(monitor);
return await this.pingProvider.handle(monitor);
case this.TYPE_HTTP:
return await this.requestHttp<HttpStatusPayload>(monitor);
return await this.httpProvider.handle(monitor);
case this.TYPE_PAGESPEED:
return await this.requestPageSpeed(monitor);
return await this.pageSpeedProvider.handle(monitor);
case this.TYPE_HARDWARE:
return await this.requestHardware(monitor);
return await this.hardwareProvider.handle(monitor);
case this.TYPE_DOCKER:
return await this.requestDocker(monitor);
return await this.dockerProvider.handle(monitor);
case this.TYPE_PORT:
return await this.requestPort(monitor);
return await this.portProvider.handle(monitor);
case this.TYPE_GAME:
return await this.requestGame(monitor);
return await this.gameProvider.handle(monitor);
case this.TYPE_GRPC:
return await this.requestGrpc(monitor);
return await this.grpcProvider.handle(monitor);
default:
return this.handleUnsupportedType(type);
}
}
private async requestPing(monitor: Monitor): Promise<MonitorStatusResponse<PingStatusPayload>> {
try {
if (!monitor?.url) {
throw new Error("Monitor URL is required");
}
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) {
throw error;
}
throw new Error("Ping failed - no result returned");
}
const pingResponse = this.buildStatusResponse<PingStatusPayload>({
monitor,
payload: response as PingStatusPayload,
overrides: {
status: (response as { alive?: boolean })?.alive ?? false,
code: 200,
message: "Success",
responseTime: (response as { time?: number })?.time ?? 0,
payload: response as PingStatusPayload,
} as MonitorStatusResponseOverrides<PingStatusPayload>,
});
return pingResponse;
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing ping",
service: this.SERVICE_NAME,
method: "requestPing",
details: { url: monitor.url },
});
}
}
private async requestHttp<T = unknown>(monitor: Monitor): Promise<MonitorStatusResponse<T>> {
const { url, secret, ignoreTlsErrors, useAdvancedMatching, jsonPath, matchMethod, expectedValue, basicAuthUser, basicAuthPassword } = monitor;
const httpResponse = this.buildStatusResponse<T>({
monitor,
overrides: {
status: false,
code: this.NETWORK_ERROR,
message: "Request not executed",
},
});
try {
if (!url) {
throw new Error("Monitor URL is required");
}
const headers: Record<string, string> = {};
if (secret) {
headers["Authorization"] = `Bearer ${secret}`;
} else if (basicAuthUser) {
const credentials = Buffer.from(`${basicAuthUser}:${basicAuthPassword || ""}`).toString("base64");
headers["Authorization"] = `Basic ${credentials}`;
}
const config: Record<string, unknown> = {
headers: Object.keys(headers).length > 0 ? headers : undefined,
};
if (ignoreTlsErrors) {
config.agent = {
https: new this.https.Agent({
rejectUnauthorized: false,
}),
};
}
const response = await this.got(url, config);
let payload: T | string | undefined;
const contentType = response.headers["content-type"];
if (contentType && contentType.includes("application/json")) {
try {
payload = JSON.parse(response.body);
} catch {
payload = response.body;
}
} else {
payload = response.body;
}
Object.assign(httpResponse, {
code: response.statusCode,
status: response.ok,
message: response.statusMessage ?? "",
responseTime: response.timings.phases.total || 0,
payload,
timings: response.timings,
});
if (!useAdvancedMatching) {
return httpResponse;
}
if (expectedValue && !jsonPath) {
let ok = false;
if (matchMethod === "equal") ok = payload === expectedValue;
if (matchMethod === "include" && typeof payload === "string") ok = payload.includes(expectedValue);
if (matchMethod === "regex" && typeof payload === "string") ok = new RegExp(expectedValue).test(payload);
if (ok === true) {
return httpResponse;
} else {
httpResponse.code = 500;
httpResponse.status = false;
httpResponse.message = "Expected value did not match";
return httpResponse;
}
}
if (jsonPath) {
const contentType = response.headers["content-type"];
const isJson = contentType?.includes("application/json");
if (!isJson) {
httpResponse.status = false;
httpResponse.message = "Response is not JSON";
return httpResponse;
}
try {
const extracted = this.jmespath.search(payload, jsonPath);
if (expectedValue) {
let ok = false;
if (matchMethod === "equal") ok = extracted === expectedValue;
if (matchMethod === "include" && typeof extracted === "string") ok = extracted.includes(expectedValue);
if (matchMethod === "regex" && typeof extracted === "string") ok = new RegExp(expectedValue).test(extracted);
if (ok) {
httpResponse.extracted = extracted;
return httpResponse;
} else {
httpResponse.status = false;
httpResponse.code = 500;
httpResponse.message = "Expected value did not match";
httpResponse.extracted = extracted;
return httpResponse;
}
} else {
const isFalsey = extracted === false || extracted === "false" || extracted === undefined || extracted === null;
if (!isFalsey) {
httpResponse.extracted = extracted;
return httpResponse;
} else {
httpResponse.status = false;
httpResponse.code = 500;
httpResponse.message = "Expected value did not match";
httpResponse.extracted = extracted;
return httpResponse;
}
}
} catch {
httpResponse.status = false;
httpResponse.message = "Error evaluating JSON path";
return httpResponse;
}
}
return httpResponse;
} catch (err: unknown) {
if (err instanceof HTTPError || err instanceof RequestError) {
httpResponse.code = err.response?.statusCode || this.NETWORK_ERROR;
httpResponse.status = false;
httpResponse.message = String(err.response?.statusCode) || err.message;
httpResponse.responseTime = err.timings?.phases?.total || 0;
httpResponse.payload = null;
httpResponse.timings = err.timings || undefined;
return httpResponse;
}
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing HTTP request",
service: this.SERVICE_NAME,
method: "requestHttp",
details: { url: monitor.url },
});
}
}
private async requestPageSpeed(monitor: Monitor): Promise<MonitorStatusResponse<PageSpeedStatusPayload>> {
try {
const url = monitor.url;
if (!url) {
throw new Error("Monitor URL is required");
}
let pageSpeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`;
const dbSettings = await this.settingsService.getDBSettings();
if (dbSettings?.pagespeedApiKey) {
pageSpeedUrl += `&key=${dbSettings.pagespeedApiKey}`;
} else {
this.logger.warn({
message: "PageSpeed API key not found, job not executed",
service: this.SERVICE_NAME,
method: "requestPagespeed",
details: { url },
});
}
return await this.requestHttp<PageSpeedStatusPayload>({
...monitor,
url: pageSpeedUrl,
});
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing PageSpeed request",
service: this.SERVICE_NAME,
method: "requestPageSpeed",
details: { url: monitor.url },
});
}
}
private async requestHardware(monitor: Monitor): Promise<MonitorStatusResponse<HardwareStatusPayload>> {
try {
return await this.requestHttp<HardwareStatusPayload>(monitor);
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing hardware request",
service: this.SERVICE_NAME,
method: "requestHardware",
details: { url: monitor.url },
});
}
}
private async requestDocker(monitor: Monitor): Promise<MonitorStatusResponse<DockerStatusPayload>> {
try {
if (!monitor.url) {
throw new Error("Monitor URL is required");
}
const docker = new this.Docker({
socketPath: "/var/run/docker.sock",
});
const dockerResponse = this.buildStatusResponse<DockerStatusPayload>({
monitor,
overrides: {
status: false,
code: this.NETWORK_ERROR,
message: "No response",
},
});
const containers = await docker.listContainers({ all: true });
// Normalize input: strip leading slashes and convert to lowercase for comparison
const normalizedInput = monitor.url.replace(/^\/+/, "").toLowerCase();
// Priority-based matching to avoid ambiguity:
// 1. Exact full ID match (64-char)
const exactIdMatch = containers.find((c) => c.Id.toLowerCase() === normalizedInput);
// 2. Exact container name match (case-insensitive)
const exactNameMatch = containers.find((c) =>
c.Names.some((name: string) => {
const cleanName = name.replace(/^\/+/, "").toLowerCase();
return cleanName === normalizedInput;
})
);
// 3. Partial ID match (fallback for backwards compatibility)
const partialIdMatch = containers.find((c) => c.Id.toLowerCase().startsWith(normalizedInput));
// Select container based on priority
const targetContainer = exactIdMatch || exactNameMatch || partialIdMatch;
// Return negative response if no container
if (!targetContainer) {
this.logger.warn({
message: `No container found for "${monitor.url}".`,
service: this.SERVICE_NAME,
method: "requestDocker",
details: { url: monitor.url },
});
dockerResponse.code = 404;
dockerResponse.status = false;
dockerResponse.message = "Docker container not found";
return dockerResponse;
}
// Return negative response if ambiguous matches exist
const matchTypes = [];
if (exactIdMatch) matchTypes.push("exact ID");
if (exactNameMatch) matchTypes.push("exact name");
if (partialIdMatch && !exactIdMatch) matchTypes.push("partial ID");
if (matchTypes.length > 1) {
this.logger.warn({
message: `Ambiguous container match for "${monitor.url}". Matched by: ${matchTypes.join(", ")}. Using ${exactIdMatch ? "exact ID" : exactNameMatch ? "exact name" : "partial ID"} match.`,
service: this.SERVICE_NAME,
method: "requestDocker",
details: { url: monitor.url },
});
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;
}
const container = docker.getContainer(targetContainer.Id);
const { response, responseTime, error }: { response?: { State: { Status: string } } | null; responseTime: number; error: unknown } =
await this.timeRequest(() => container.inspect());
dockerResponse.responseTime = responseTime;
dockerResponse.status = response?.State?.Status === "running" ? true : false;
dockerResponse.code = 200;
dockerResponse.message = "Docker container status fetched successfully";
if (error) {
dockerResponse.status = false;
dockerResponse.code =
(error && typeof error === "object" && "statusCode" in error ? (error as { statusCode?: number }).statusCode : undefined) ||
this.NETWORK_ERROR;
dockerResponse.message =
(error && typeof error === "object" && "reason" in error ? (error as { reason?: string }).reason : undefined) ||
"Failed to fetch Docker container information";
return dockerResponse;
}
return dockerResponse;
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing Docker request",
service: this.SERVICE_NAME,
method: "requestDocker",
details: { url: monitor.url },
});
}
}
private async requestPort(monitor: Monitor): Promise<MonitorStatusResponse<PortStatusPayload>> {
try {
const { url, port } = monitor;
if (!port) {
throw new Error("Port is required for port monitor");
}
const { response, responseTime, error } = await this.timeRequest(async () => {
return new Promise((resolve, reject) => {
const socket = this.net.createConnection(
{
host: url,
port,
},
() => {
socket.end();
socket.destroy();
resolve({ success: true });
}
);
socket.setTimeout(5000);
socket.on("timeout", () => {
socket.destroy();
reject(new Error("Connection timeout"));
});
socket.on("error", (err: unknown) => {
socket.destroy();
reject(err);
});
});
});
const portResponse = this.buildStatusResponse<PortStatusPayload>({
monitor,
overrides: {
code: 200,
status: (response as { success?: boolean })?.success ?? false,
message: "Port check successful",
responseTime,
},
});
if (error) {
portResponse.code = this.NETWORK_ERROR;
portResponse.status = false;
portResponse.message = "Port check failed";
return portResponse;
}
return portResponse;
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing port check",
service: this.SERVICE_NAME,
method: "requestPort",
details: { url: monitor.url, port: monitor.port },
});
}
}
private async requestGame(monitor: Monitor): Promise<MonitorStatusResponse<GameStatusPayload>> {
try {
const { url, port, gameId } = monitor;
const gameResponse = this.buildStatusResponse<GameStatusPayload>({
monitor,
overrides: {
code: 200,
status: true,
message: "Success",
},
});
const state = await this.GameDig.query({
type: gameId ?? "unknown",
host: url,
port: port ?? 0,
}).catch((error: unknown): undefined => {
this.logger.warn({
message: error instanceof Error ? error.message : String(error),
service: this.SERVICE_NAME,
method: "requestGame",
details: { url, port, gameId },
});
return undefined;
});
if (!state) {
gameResponse.code = this.NETWORK_ERROR;
gameResponse.status = false;
gameResponse.message = "No response";
return gameResponse;
}
gameResponse.responseTime = state.ping ?? 0;
gameResponse.payload = state as GameStatusPayload;
return gameResponse;
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing game server check",
service: this.SERVICE_NAME,
method: "requestGame",
details: { url: monitor.url, port: monitor.port, gameId: monitor.gameId },
});
}
}
private async requestGrpc(monitor: Monitor): Promise<MonitorStatusResponse<GrpcStatusPayload>> {
try {
const { url, port, ignoreTlsErrors } = monitor;
const grpcServiceName = monitor.grpcServiceName || "";
if (!url) {
throw new AppError({ message: "Monitor host is required", service: this.SERVICE_NAME, method: "requestGrpc" });
}
if (!port) {
throw new AppError({ message: "Monitor port is required", service: this.SERVICE_NAME, method: "requestGrpc" });
}
const target = `${url}:${port}`;
const currentFilePath = fileURLToPath(import.meta.url);
const protoPath = path.join(path.dirname(currentFilePath), "protos", "health.proto");
const packageDefinition = this.protoLoader.loadSync(protoPath, {
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true,
});
const grpcObject = this.grpc.loadPackageDefinition(packageDefinition) as unknown as {
grpc: {
health: {
v1: {
Health: new (
target: string,
credentials: unknown
) => {
Check: (request: { service: string }, options: { deadline: Date }, callback: (err: unknown, response: unknown) => void) => void;
close: () => void;
};
};
};
};
};
const healthService = grpcObject.grpc.health.v1.Health;
let credentials;
if (ignoreTlsErrors) {
credentials = this.grpc.credentials.createSsl(null, null, null, {
checkServerIdentity: () => undefined,
});
} else {
credentials = this.grpc.credentials.createInsecure();
}
const client = new healthService(target, credentials);
const TIMEOUT_MS = 10000;
const deadline = new Date(Date.now() + TIMEOUT_MS);
const grpcResponse = this.buildStatusResponse<GrpcStatusPayload>({
monitor,
overrides: {
status: false,
code: this.NETWORK_ERROR,
message: "gRPC health check not executed",
},
});
const { response, responseTime, error } = await this.timeRequest<GrpcStatusPayload>(() => {
return new Promise<GrpcStatusPayload>((resolve, reject) => {
client.Check({ service: grpcServiceName }, { deadline }, (err: unknown, response: unknown) => {
client.close();
if (err) {
const grpcErr = err as { code?: number; details?: string; message?: string };
const payload: GrpcStatusPayload = {
grpcStatusCode: grpcErr.code ?? -1,
grpcStatusName: this.getGrpcStatusName(grpcErr.code ?? -1),
serviceName: grpcServiceName,
servingStatus: "UNKNOWN",
};
const grpcError = new AppError({
message: grpcErr.details || grpcErr.message || "gRPC error",
service: this.SERVICE_NAME,
method: "requestGrpc",
}) as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number };
grpcError.grpcPayload = payload;
grpcError.grpcCode = grpcErr.code;
reject(grpcError);
return;
}
const resp = response as { status?: string } | undefined;
const servingStatus = resp?.status ?? "UNKNOWN";
resolve({
grpcStatusCode: 0,
grpcStatusName: "OK",
serviceName: grpcServiceName,
servingStatus,
});
});
});
});
if (error) {
const grpcError = error as AppError & { grpcPayload?: GrpcStatusPayload; grpcCode?: number };
const payload = grpcError.grpcPayload;
grpcResponse.status = false;
grpcResponse.code = grpcError.grpcCode ?? this.NETWORK_ERROR;
grpcResponse.message = grpcError.message ?? "gRPC health check failed";
grpcResponse.responseTime = responseTime;
grpcResponse.payload = payload ?? null;
return grpcResponse;
}
const grpcPayload = response as GrpcStatusPayload;
const isServing = grpcPayload.servingStatus === "SERVING";
grpcResponse.status = isServing;
grpcResponse.code = isServing ? 200 : this.NETWORK_ERROR;
grpcResponse.message = isServing
? `gRPC service healthy (${grpcPayload.servingStatus})`
: `gRPC service unhealthy (${grpcPayload.servingStatus})`;
grpcResponse.responseTime = responseTime;
grpcResponse.payload = grpcPayload;
return grpcResponse;
} catch (err: unknown) {
const originalMessage = err instanceof Error ? err.message : String(err);
throw new AppError({
message: originalMessage || "Error performing gRPC health check",
service: this.SERVICE_NAME,
method: "requestGrpc",
details: { url: monitor.url, port: monitor.port, grpcServiceName: monitor.grpcServiceName },
});
}
}
private getGrpcStatusName(code: number): string {
const statusNames: Record<number, string> = {
0: "OK",
1: "CANCELLED",
2: "UNKNOWN",
3: "INVALID_ARGUMENT",
4: "DEADLINE_EXCEEDED",
5: "NOT_FOUND",
6: "ALREADY_EXISTS",
7: "PERMISSION_DENIED",
8: "RESOURCE_EXHAUSTED",
9: "FAILED_PRECONDITION",
10: "ABORTED",
11: "OUT_OF_RANGE",
12: "UNIMPLEMENTED",
13: "INTERNAL",
14: "UNAVAILABLE",
15: "DATA_LOSS",
16: "UNAUTHENTICATED",
};
return statusNames[code] || "UNKNOWN";
}
private async handleUnsupportedType(type: string): Promise<MonitorStatusResponse> {
return {
monitorId: "unknown",
-2
View File
@@ -27,8 +27,6 @@ export interface Monitor {
jsonPath?: string;
expectedValue?: string;
matchMethod?: MonitorMatchMethod;
basicAuthUser?: string;
basicAuthPassword?: string;
url: string;
port?: number;
isActive: boolean;
+6 -2
View File
@@ -14,6 +14,8 @@ import type {
MonitorStatus,
} from "@/types/index.js";
import type { QueryResult } from "gamedig";
export interface MonitorStatusResponse<
T =
| HttpStatusPayload
@@ -49,7 +51,7 @@ export interface PingStatusPayload {
host: string;
numeric_host?: string;
alive: boolean;
time: number;
time: number | unknown;
times?: number[];
output?: string;
min?: string;
@@ -100,7 +102,7 @@ export interface PortStatusPayload {
success: boolean;
}
export type GameStatusPayload = Record<string, unknown>;
export type GameStatusPayload = QueryResult;
export interface GrpcStatusPayload {
grpcStatusCode: number;
@@ -134,3 +136,5 @@ export type StatusChangeResult = {
temp: boolean;
};
};
export type MonitorStatusResponseOverrides<T> = Partial<Omit<MonitorStatusResponse<T>, "monitorId" | "teamId" | "type">>;
@@ -70,8 +70,6 @@ export const createMonitorBodyValidation = z.object({
jsonPath: z.union([z.string(), z.literal("")]).optional(),
expectedValue: z.union([z.string(), z.literal("")]).optional(),
matchMethod: z.union([z.string(), z.null(), z.literal("")]).optional(),
basicAuthUser: z.union([z.string(), z.literal("")]).optional(),
basicAuthPassword: z.union([z.string(), z.literal("")]).optional(),
gameId: z.union([z.string(), z.literal("")]).optional(),
grpcServiceName: z.union([z.string(), z.literal("")]).default(""),
selectedDisks: z.array(z.string()).optional(),
@@ -96,8 +94,6 @@ export const editMonitorBodyValidation = z.object({
jsonPath: z.union([z.string(), z.literal("")]).optional(),
expectedValue: z.union([z.string(), z.literal("")]).optional(),
matchMethod: z.union([z.string(), z.null(), z.literal("")]).optional(),
basicAuthUser: z.union([z.string(), z.literal("")]).optional(),
basicAuthPassword: z.union([z.string(), z.literal("")]).optional(),
port: z.number().min(1).max(65535).optional(),
cpuAlertThreshold: z.number().optional(),
memoryAlertThreshold: z.number().optional(),