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
Alexander Holliday 189293b04d Merge pull request #3366 from bluewave-labs/fix/eslint-warnings
fix: eslint warnings
2026-03-04 10:29:35 -08:00
Alex Holliday 13e9185011 bump version 2026-03-04 18:24:06 +00:00
Alex Holliday a7cf78cecc emailService 2026-03-04 18:16:31 +00:00
Alex Holliday 0b30e23783 globalPing 2026-03-04 17:50:27 +00:00
Alexander Holliday 1155619835 Merge pull request #3365 from bluewave-labs/fix/eslint
fix: eslint
2026-03-04 09:47:51 -08:00
Alex Holliday e0608ad3ed notificationMessageBuilder 2026-03-04 17:43:25 +00:00
Alex Holliday d665e95f02 discord 2026-03-04 17:39:31 +00:00
Alex Holliday afc27c804d email 2026-03-04 17:32:50 +00:00
Alex Holliday 1deb22bf63 pagerduty 2026-03-04 17:17:52 +00:00
Alex Holliday af8b128ac2 notificaiton providers 2026-03-03 23:27:48 +00:00
Alex Holliday 00150ba6f4 checkService, bufferService 2026-03-03 23:19:54 +00:00
Alex Holliday 7f04dd5d9a checksService 2026-03-03 23:09:10 +00:00
Alex Holliday ab5389190d stautsService and settingsService 2026-03-03 23:01:35 +00:00
50 changed files with 2172 additions and 1859 deletions

No files matched your search

+138 -170
View File
@@ -58,7 +58,7 @@
"lint-staged": "^16.2.7", "lint-staged": "^16.2.7",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"typescript": "5.9.3", "typescript": "5.9.3",
"vite": "6.3.6" "vite": "6.4.1"
} }
}, },
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
@@ -83,6 +83,7 @@
"node_modules/@babel/core": { "node_modules/@babel/core": {
"version": "7.29.0", "version": "7.29.0",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/code-frame": "^7.29.0", "@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0", "@babel/generator": "^7.29.0",
@@ -353,6 +354,7 @@
"node_modules/@emotion/react": { "node_modules/@emotion/react": {
"version": "11.14.0", "version": "11.14.0",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.18.3", "@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5", "@emotion/babel-plugin": "^11.13.5",
@@ -390,6 +392,7 @@
"node_modules/@emotion/styled": { "node_modules/@emotion/styled": {
"version": "11.14.1", "version": "11.14.1",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.18.3", "@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5", "@emotion/babel-plugin": "^11.13.5",
@@ -434,7 +437,6 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -451,7 +453,6 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -468,7 +469,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -485,7 +485,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -502,7 +501,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -519,7 +517,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -536,7 +533,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -553,7 +549,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -570,7 +565,6 @@
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -587,7 +581,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -604,7 +597,6 @@
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -621,7 +613,6 @@
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -638,7 +629,6 @@
"cpu": [ "cpu": [
"mips64el" "mips64el"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -655,7 +645,6 @@
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -672,7 +661,6 @@
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -689,7 +677,6 @@
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -704,7 +691,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -721,7 +707,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -738,7 +723,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -755,7 +739,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -772,7 +755,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -789,7 +771,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -806,7 +787,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -823,7 +803,6 @@
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -840,7 +819,6 @@
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -857,7 +835,6 @@
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1185,6 +1162,7 @@
"node_modules/@mui/material": { "node_modules/@mui/material": {
"version": "7.3.7", "version": "7.3.7",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.28.4", "@babel/runtime": "^7.28.4",
"@mui/core-downloads-tracker": "^7.3.7", "@mui/core-downloads-tracker": "^7.3.7",
@@ -1289,6 +1267,7 @@
"node_modules/@mui/system": { "node_modules/@mui/system": {
"version": "7.3.7", "version": "7.3.7",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.28.4", "@babel/runtime": "^7.28.4",
"@mui/private-theming": "^7.3.7", "@mui/private-theming": "^7.3.7",
@@ -1660,13 +1639,12 @@
} }
}, },
"node_modules/@rollup/rollup-android-arm-eabi": { "node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz",
"integrity": "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg==", "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1674,13 +1652,12 @@
] ]
}, },
"node_modules/@rollup/rollup-android-arm64": { "node_modules/@rollup/rollup-android-arm64": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz",
"integrity": "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w==", "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1688,13 +1665,12 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-arm64": { "node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz",
"integrity": "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg==", "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1702,13 +1678,12 @@
] ]
}, },
"node_modules/@rollup/rollup-darwin-x64": { "node_modules/@rollup/rollup-darwin-x64": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz",
"integrity": "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w==", "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1716,13 +1691,12 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-arm64": { "node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz",
"integrity": "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug==", "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1730,13 +1704,12 @@
] ]
}, },
"node_modules/@rollup/rollup-freebsd-x64": { "node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz",
"integrity": "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q==", "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1744,13 +1717,12 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-gnueabihf": { "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz",
"integrity": "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw==", "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1758,13 +1730,12 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm-musleabihf": { "node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz",
"integrity": "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw==", "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1772,13 +1743,12 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-gnu": { "node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz",
"integrity": "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g==", "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1786,13 +1756,12 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-arm64-musl": { "node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz",
"integrity": "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q==", "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1800,13 +1769,12 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loong64-gnu": { "node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz",
"integrity": "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA==", "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1814,13 +1782,12 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-loong64-musl": { "node_modules/@rollup/rollup-linux-loong64-musl": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz",
"integrity": "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw==", "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==",
"cpu": [ "cpu": [
"loong64" "loong64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1828,13 +1795,12 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-ppc64-gnu": { "node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz",
"integrity": "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w==", "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1842,13 +1808,12 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-ppc64-musl": { "node_modules/@rollup/rollup-linux-ppc64-musl": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz",
"integrity": "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw==", "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==",
"cpu": [ "cpu": [
"ppc64" "ppc64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1856,13 +1821,12 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-gnu": { "node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz",
"integrity": "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A==", "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1870,13 +1834,12 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-riscv64-musl": { "node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz",
"integrity": "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw==", "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1884,13 +1847,12 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-s390x-gnu": { "node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz",
"integrity": "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg==", "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==",
"cpu": [ "cpu": [
"s390x" "s390x"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1898,11 +1860,12 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-gnu": { "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": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1910,13 +1873,12 @@
] ]
}, },
"node_modules/@rollup/rollup-linux-x64-musl": { "node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz",
"integrity": "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw==", "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1924,13 +1886,12 @@
] ]
}, },
"node_modules/@rollup/rollup-openbsd-x64": { "node_modules/@rollup/rollup-openbsd-x64": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz",
"integrity": "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw==", "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1938,13 +1899,12 @@
] ]
}, },
"node_modules/@rollup/rollup-openharmony-arm64": { "node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz",
"integrity": "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ==", "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1952,13 +1912,12 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-arm64-msvc": { "node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz",
"integrity": "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ==", "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1966,13 +1925,12 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-ia32-msvc": { "node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz",
"integrity": "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew==", "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==",
"cpu": [ "cpu": [
"ia32" "ia32"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1980,13 +1938,12 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-gnu": { "node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz",
"integrity": "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ==", "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -1994,13 +1951,12 @@
] ]
}, },
"node_modules/@rollup/rollup-win32-x64-msvc": { "node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.57.1", "version": "4.59.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz",
"integrity": "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA==", "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
@@ -2350,8 +2306,9 @@
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "24.5.2", "version": "24.5.2",
"dev": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"undici-types": "~7.12.0" "undici-types": "~7.12.0"
} }
@@ -2366,8 +2323,8 @@
}, },
"node_modules/@types/react": { "node_modules/@types/react": {
"version": "18.3.27", "version": "18.3.27",
"dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/prop-types": "*", "@types/prop-types": "*",
"csstype": "^3.2.2" "csstype": "^3.2.2"
@@ -2489,6 +2446,7 @@
"version": "8.15.0", "version": "8.15.0",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"bin": { "bin": {
"acorn": "bin/acorn" "acorn": "bin/acorn"
}, },
@@ -2505,7 +2463,9 @@
} }
}, },
"node_modules/ajv": { "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, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -2811,6 +2771,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
@@ -3242,7 +3203,8 @@
}, },
"node_modules/dayjs": { "node_modules/dayjs": {
"version": "1.11.13", "version": "1.11.13",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/debug": { "node_modules/debug": {
"version": "4.4.3", "version": "4.4.3",
@@ -3566,7 +3528,6 @@
}, },
"node_modules/esbuild": { "node_modules/esbuild": {
"version": "0.25.12", "version": "0.25.12",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@@ -3625,6 +3586,7 @@
"version": "8.57.1", "version": "8.57.1",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1", "@eslint-community/regexpp": "^4.6.1",
@@ -3884,7 +3846,6 @@
}, },
"node_modules/fdir": { "node_modules/fdir": {
"version": "6.5.0", "version": "6.5.0",
"dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12.0.0" "node": ">=12.0.0"
@@ -4018,7 +3979,6 @@
"version": "2.3.3", "version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -4371,6 +4331,7 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/runtime": "^7.27.6" "@babel/runtime": "^7.27.6"
}, },
@@ -5154,6 +5115,7 @@
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz", "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz",
"integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==", "integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"peer": true,
"dependencies": { "dependencies": {
"@mapbox/geojson-rewind": "^0.5.2", "@mapbox/geojson-rewind": "^0.5.2",
"@mapbox/jsonlint-lines-primitives": "^2.0.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2",
@@ -5251,7 +5213,9 @@
} }
}, },
"node_modules/minimatch": { "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, "dev": true,
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
@@ -5317,7 +5281,6 @@
}, },
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"dev": true,
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -5650,7 +5613,6 @@
}, },
"node_modules/postcss": { "node_modules/postcss": {
"version": "8.5.6", "version": "8.5.6",
"dev": true,
"funding": [ "funding": [
{ {
"type": "opencollective", "type": "opencollective",
@@ -5801,6 +5763,7 @@
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "18.3.1", "version": "18.3.1",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0", "loose-envify": "^1.1.0",
"scheduler": "^0.23.2" "scheduler": "^0.23.2"
@@ -5812,6 +5775,7 @@
"node_modules/react-hook-form": { "node_modules/react-hook-form": {
"version": "7.71.1", "version": "7.71.1",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=18.0.0" "node": ">=18.0.0"
}, },
@@ -6011,7 +5975,8 @@
}, },
"node_modules/redux": { "node_modules/redux": {
"version": "5.0.1", "version": "5.0.1",
"license": "MIT" "license": "MIT",
"peer": true
}, },
"node_modules/redux-persist": { "node_modules/redux-persist": {
"version": "6.0.0", "version": "6.0.0",
@@ -6157,9 +6122,11 @@
"license": "Unlicense" "license": "Unlicense"
}, },
"node_modules/rollup": { "node_modules/rollup": {
"version": "4.57.1", "version": "4.59.0",
"dev": true, "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/estree": "1.0.8" "@types/estree": "1.0.8"
}, },
@@ -6171,31 +6138,31 @@
"npm": ">=8.0.0" "npm": ">=8.0.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm-eabi": "4.59.0",
"@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-android-arm64": "4.59.0",
"@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.59.0",
"@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-darwin-x64": "4.59.0",
"@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.59.0",
"@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.59.0",
"@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.59.0",
"@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.59.0",
"@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.59.0",
"@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.59.0",
"@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.59.0",
"@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.59.0",
"@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.59.0",
"@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.59.0",
"@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.59.0",
"@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.59.0",
"@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.59.0",
"@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.59.0",
"@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.59.0",
"@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openbsd-x64": "4.59.0",
"@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.59.0",
"@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.59.0",
"@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.59.0",
"@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.59.0",
"@rollup/rollup-win32-x64-msvc": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.59.0",
"fsevents": "~2.3.2" "fsevents": "~2.3.2"
} }
}, },
@@ -6530,7 +6497,6 @@
}, },
"node_modules/source-map-js": { "node_modules/source-map-js": {
"version": "1.2.1", "version": "1.2.1",
"dev": true,
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
@@ -6818,7 +6784,6 @@
}, },
"node_modules/tinyglobby": { "node_modules/tinyglobby": {
"version": "0.2.15", "version": "0.2.15",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"fdir": "^6.5.0", "fdir": "^6.5.0",
@@ -6948,8 +6913,9 @@
}, },
"node_modules/typescript": { "node_modules/typescript": {
"version": "5.9.3", "version": "5.9.3",
"dev": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"
@@ -6992,7 +6958,7 @@
}, },
"node_modules/undici-types": { "node_modules/undici-types": {
"version": "7.12.0", "version": "7.12.0",
"dev": true, "devOptional": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/union-value": { "node_modules/union-value": {
@@ -7081,9 +7047,11 @@
} }
}, },
"node_modules/vite": { "node_modules/vite": {
"version": "6.3.6", "version": "6.4.1",
"dev": true, "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.25.0", "esbuild": "^0.25.0",
"fdir": "^6.4.4", "fdir": "^6.4.4",
@@ -7366,7 +7334,7 @@
"version": "2.8.2", "version": "2.8.2",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
"dev": true, "devOptional": true,
"license": "ISC", "license": "ISC",
"bin": { "bin": {
"yaml": "bin.mjs" "yaml": "bin.mjs"
+1 -1
View File
@@ -66,6 +66,6 @@
"lint-staged": "^16.2.7", "lint-staged": "^16.2.7",
"prettier": "^3.3.3", "prettier": "^3.3.3",
"typescript": "5.9.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; if (!checks || checks.length === 0) return null;
const latestChecks = checks.slice(-25).reverse(); const latestChecks = checks.slice(-25);
let data: Array<CheckSnapshot | PlaceholderCheck>; let data: Array<CheckSnapshot | PlaceholderCheck>;
if (latestChecks.length !== 25) { if (latestChecks.length !== 25) {
+1 -1
View File
@@ -7,7 +7,7 @@ const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
export default defineConfig(({}) => { export default defineConfig(({}) => {
let version = "3.4.0"; let version = "3.5.0";
return { return {
base: "/", base: "/",
+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", "dotenv": "^16.4.5",
"express": "^4.19.2", "express": "^4.19.2",
"express-rate-limit": "8.0.1", "express-rate-limit": "8.0.1",
"gamedig": "^5.3.1", "gamedig": "5.2.0",
"got": "14.4.7", "got": "14.4.7",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"helmet": "^8.0.0", "helmet": "^8.0.0",
@@ -51,7 +51,7 @@
"mjml": "^5.0.0-alpha.4", "mjml": "^5.0.0-alpha.4",
"mongoose": "^8.3.3", "mongoose": "^8.3.3",
"multer": "^1.4.5-lts.1", "multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.14", "nodemailer": "8.0.1",
"ping": "0.4.4", "ping": "0.4.4",
"sharp": "0.33.5", "sharp": "0.33.5",
"ssl-checker": "2.0.10", "ssl-checker": "2.0.10",
+1 -3
View File
@@ -3,8 +3,6 @@ import { MonitorModel } from "../dist/db/models/Monitor.js";
import { CheckModel } from "../dist/db/models/Check.js"; import { CheckModel } from "../dist/db/models/Check.js";
const DEFAULT_MONITOR_ID = "000000000000000000000001"; const DEFAULT_MONITOR_ID = "000000000000000000000001";
const DEFAULT_TEAM_ID = "0000000000000000000000aa";
const DEFAULT_USER_ID = "0000000000000000000000bb";
const DEFAULT_MONITOR_TYPE = "http"; const DEFAULT_MONITOR_TYPE = "http";
const DEFAULT_TOTAL = 1_000_000; const DEFAULT_TOTAL = 1_000_000;
const DEFAULT_BATCH_SIZE = 5_000; const DEFAULT_BATCH_SIZE = 5_000;
@@ -12,7 +10,7 @@ const DEFAULT_BATCH_SIZE = 5_000;
const parseObjectId = (value, fallback) => { const parseObjectId = (value, fallback) => {
try { try {
return new mongoose.Types.ObjectId(value || fallback); return new mongoose.Types.ObjectId(value || fallback);
} catch (error) { } catch {
console.warn(`Invalid ObjectId '${value}', falling back to '${fallback}'.`); console.warn(`Invalid ObjectId '${value}', falling back to '${fallback}'.`);
return new mongoose.Types.ObjectId(fallback); return new mongoose.Types.ObjectId(fallback);
} }
+16 -2
View File
@@ -74,8 +74,22 @@ export const createApp = ({
}, },
}) })
); );
// Swagger UI // Swagger UI — dynamically set server URL from request
app.use("/api-docs", swaggerUi.serve, swaggerUi.setup(openApiSpec)); 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) => { app.use("/api/v1/health", (req, res) => {
res.json({ res.json({
+28 -14
View File
@@ -29,8 +29,6 @@ import { NotificationMessageBuilder, INotificationMessageBuilder } from "../serv
import axios from "axios"; import axios from "axios";
import got from "got"; import got from "got";
import ping from "ping"; import ping from "ping";
import http from "http";
import https from "https";
import Docker from "dockerode"; import Docker from "dockerode";
import net from "net"; import net from "net";
import fs from "fs"; import fs from "fs";
@@ -76,6 +74,15 @@ import {
} from "@/repositories/index.js"; } from "@/repositories/index.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
import { EnvConfig } from "@/service/system/settingsService.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 = { export type InitializedServices = {
settingsService: any; settingsService: any;
@@ -145,21 +152,28 @@ export const initializeServices = async ({
const teamsRepository = new MongoTeamsRepository(); const teamsRepository = new MongoTeamsRepository();
const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository(); 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( const networkService = new NetworkService(
axios, axios,
got,
https,
jmespath,
GameDig as unknown as {
query: (options: { type: string; host: string; port?: number }) => Promise<{ ping?: number } & { [key: string]: unknown }>;
},
ping,
logger, logger,
Docker, pingProvider,
net, httpProvider,
settingsService, pageSpeedProvider,
grpc, hardwareProvider,
protoLoader dockerProvider,
portProvider,
gameProvider,
grpcProvider
); );
const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger);
@@ -11,7 +11,6 @@ import { requireTeamId, requireUserId } from "@/controllers/controllerUtils.js";
import { IStatusPageService } from "@/service/business/statusPageService.js"; import { IStatusPageService } from "@/service/business/statusPageService.js";
import { IMonitorsRepository } from "@/repositories/index.js"; import { IMonitorsRepository } from "@/repositories/index.js";
import { ISettingsService } from "@/service/system/settingsService.js"; import { ISettingsService } from "@/service/system/settingsService.js";
import { ParseBoolean } from "@/utils/utils.js";
import { NormalizeData } from "@/utils/dataUtils.js"; import { NormalizeData } from "@/utils/dataUtils.js";
const SERVICE_NAME = "statusPageController"; const SERVICE_NAME = "statusPageController";
@@ -110,6 +109,7 @@ class StatusPageController {
const normalizedMonitors = sortedMonitors.map((monitor) => { const normalizedMonitors = sortedMonitors.map((monitor) => {
const normalizedChecks = NormalizeData(monitor.recentChecks, 10, 100); const normalizedChecks = NormalizeData(monitor.recentChecks, 10, 100);
if (!showURL) { if (!showURL) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { url, port, secret, notifications, ...rest } = monitor; const { url, port, secret, notifications, ...rest } = monitor;
return { ...rest, checks: normalizedChecks }; return { ...rest, checks: normalizedChecks };
} }
@@ -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 { migrateStatusWindowThreshold } from "./0001_migrateStatusWindowThreshold.js";
import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js"; import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js";
import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js"; import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js";
import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js";
import MigrationModel from "../models/Migration.js"; import MigrationModel from "../models/Migration.js";
type MigrationEntry = { type MigrationEntry = {
@@ -12,6 +13,7 @@ const migrations: MigrationEntry[] = [
{ name: "0001_migrateStatusWindowThreshold", execute: migrateStatusWindowThreshold }, { name: "0001_migrateStatusWindowThreshold", execute: migrateStatusWindowThreshold },
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries }, { name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
{ name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats }, { name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats },
{ name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds },
]; ];
const runMigrations = async (logger?: { info: Function; error: Function }) => { const runMigrations = async (logger?: { info: Function; error: Function }) => {
@@ -20,12 +20,19 @@ export interface IChecksRepository {
monitorId: string, monitorId: string,
sortOrder: string, sortOrder: string,
dateRange: string, dateRange: string,
filter: string, filter: string | undefined,
page: number, page: number,
rowsPerPage: number, rowsPerPage: number,
status: boolean | undefined status: boolean | undefined
): Promise<ChecksQueryResult>; ): 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>; findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise<LatestChecksMap>;
findByDateRangeAndMonitorId( findByDateRangeAndMonitorId(
monitorId: string, monitorId: string,
@@ -231,7 +231,7 @@ class MongoChecksRepository implements IChecksRepository {
monitorId: string, monitorId: string,
sortOrder: string, sortOrder: string,
dateRange: string, dateRange: string,
filter: string, filter: string | undefined,
page: number, page: number,
rowsPerPage: number, rowsPerPage: number,
status: boolean | undefined status: boolean | undefined
@@ -36,7 +36,7 @@ class MongoInvitesRepository implements IInvitesRepository {
create = async (invite: Partial<Invite>) => { create = async (invite: Partial<Invite>) => {
await InviteModel.deleteMany({ email: invite.email }); await InviteModel.deleteMany({ email: invite.email });
invite.token = crypto.randomBytes(32).toString("hex"); invite.token = crypto.randomBytes(32).toString("hex");
let inviteToken = await InviteModel.create(invite); const inviteToken = await InviteModel.create(invite);
return this.toEntity(inviteToken); return this.toEntity(inviteToken);
}; };
@@ -92,7 +92,7 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository
} }
// Sorting // Sorting
let sort: Record<string, any> = {}; const sort: Record<string, any> = {};
if (field !== undefined && order !== undefined) { if (field !== undefined && order !== undefined) {
sort[field] = order === "asc" ? 1 : -1; sort[field] = order === "asc" ? 1 : -1;
} }
@@ -52,7 +52,7 @@ class MongoSettingsRepository implements ISettingsRepository {
}; };
findSingleton = async () => { findSingleton = async () => {
let settings = await AppSettingsModel.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean(); const settings = await AppSettingsModel.findOne({ singleton: true }).select("-__v -_id -createdAt -updatedAt -singleton").lean();
if (!settings) { if (!settings) {
return null; return null;
} }
+37 -16
View File
@@ -1,6 +1,6 @@
import { Types } from "mongoose"; import { Types } from "mongoose";
import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js"; import { IChecksRepository, IMonitorsRepository } from "@/repositories/index.js";
import type { MonitorStatusResponse, CheckErrorInfo, Check } from "@/types/index.js"; import type { MonitorStatusResponse, CheckErrorInfo, Check, ILighthouseAudit, ChecksQueryResult, ChecksSummary } from "@/types/index.js";
import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { ParseBoolean } from "@/utils/utils.js"; import { ParseBoolean } from "@/utils/utils.js";
@@ -8,12 +8,23 @@ import { ILogger } from "@/utils/logger.js";
const SERVICE_NAME = "checkService"; const SERVICE_NAME = "checkService";
class CheckService { export interface ICheckService {
createChecks(checks: Check[]): Promise<Check[]>;
buildCheck(statusResponse: MonitorStatusResponse<PageSpeedStatusPayload | HardwareStatusPayload | undefined>): Partial<Check> | undefined;
getChecksByMonitor(params: { monitorId: string; query: Record<string, string>; teamId: string }): Promise<ChecksQueryResult>;
getChecksByTeam(params: { teamId: string; query: Record<string, string> }): Promise<ChecksQueryResult>;
getChecksSummaryByTeamId(params: { teamId: string; dateRange: string }): Promise<ChecksSummary>;
deleteChecks(params: { monitorId: string; teamId: string }): Promise<number>;
deleteChecksByTeamId(params: { teamId: string }): Promise<number>;
updateChecksTTL(params: { teamId: string; ttl: string }): Promise<void>;
}
class CheckService implements ICheckService {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private monitorsRepository: IMonitorsRepository; private monitorsRepository: IMonitorsRepository;
private checksRepository: IChecksRepository; private checksRepository: IChecksRepository;
private logger: any; private logger: ILogger;
constructor(monitorsRepository: IMonitorsRepository, logger: ILogger, checksRepository: IChecksRepository) { constructor(monitorsRepository: IMonitorsRepository, logger: ILogger, checksRepository: IChecksRepository) {
this.monitorsRepository = monitorsRepository; this.monitorsRepository = monitorsRepository;
this.logger = logger; this.logger = logger;
@@ -53,13 +64,12 @@ class CheckService {
message: "Failed to build check", message: "Failed to build check",
service: SERVICE_NAME, service: SERVICE_NAME,
method: "buildCheck", method: "buildCheck",
details: "empty payload",
}); });
return undefined; return undefined;
} }
const categories = pageSpeedPayload.lighthouseResult?.categories ?? {}; const categories = pageSpeedPayload.lighthouseResult?.categories ?? {};
const audits = pageSpeedPayload.lighthouseResult?.audits ?? {}; const audits = pageSpeedPayload.lighthouseResult?.audits ?? {};
const mapAudit = (audit: any) => { const mapAudit = (audit: ILighthouseAudit | undefined) => {
if (!audit || typeof audit !== "object") { if (!audit || typeof audit !== "object") {
return undefined; return undefined;
} }
@@ -102,7 +112,7 @@ class CheckService {
return check; return check;
}; };
getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: any; teamId: string }) => { getChecksByMonitor = async ({ monitorId, query, teamId }: { monitorId: string; query: Record<string, string>; teamId: string }) => {
if (!monitorId) { if (!monitorId) {
throw new AppError({ message: "No monitor ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 }); throw new AppError({ message: "No monitor ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 });
} }
@@ -110,28 +120,39 @@ class CheckService {
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 }); throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 });
} }
// For verificaiton, throws an error if monitor doesn't belong to team // For verification, throws an error if monitor doesn't belong to team
await this.monitorsRepository.findById(monitorId, teamId); await this.monitorsRepository.findById(monitorId, teamId);
let { sortOrder, dateRange, filter, page, rowsPerPage, status } = query; const { sortOrder, dateRange, filter, page, rowsPerPage, status } = query;
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 parsedStatus = typeof status === "undefined" ? status : ParseBoolean(status);
const parsedPage = page ? parseInt(page) : page; const parsedPage = page ? parseInt(page) : 0;
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage; const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : 5;
const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, parsedStatus); const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, parsedStatus);
return result; return result;
}; };
getChecksByTeam = async ({ teamId, query }: { teamId: string; query: any }) => { getChecksByTeam = async ({ teamId, query }: { teamId: string; query: Record<string, string> }) => {
let { sortOrder, dateRange, filter, page, rowsPerPage } = query; const { sortOrder, dateRange, filter, page, rowsPerPage } = query;
if (!sortOrder || !dateRange || !page || !rowsPerPage) {
throw new AppError({ message: "Missing required query parameters", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 });
}
if (!teamId) { if (!teamId) {
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 }); throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "getChecksByTeam", status: 400 });
} }
const parsedPage = page ? parseInt(page) : page; const parsedPage = page ? parseInt(page) : 0;
const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : rowsPerPage; const parsedRowsPerPage = rowsPerPage ? parseInt(rowsPerPage) : 5;
const checkData = await this.checksRepository.findByTeamId(sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, teamId); const checkData = await this.checksRepository.findByTeamId(sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, teamId);
return checkData; return checkData;
@@ -154,7 +175,7 @@ class CheckService {
throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteChecks", status: 400 }); throw new AppError({ message: "No team ID in request", service: SERVICE_NAME, method: "deleteChecks", status: 400 });
} }
// For verificaiton, throws an error if monitor doesn't belong to team // For verification, throws an error if monitor doesn't belong to team
await this.monitorsRepository.findById(monitorId, teamId); await this.monitorsRepository.findById(monitorId, teamId);
const deletedCount = await this.checksRepository.deleteByMonitorId(monitorId); const deletedCount = await this.checksRepository.deleteByMonitorId(monitorId);
@@ -170,7 +191,7 @@ class CheckService {
}; };
updateChecksTTL = async ({ teamId, ttl }: { teamId: string; ttl: string }) => { updateChecksTTL = async ({ teamId, ttl }: { teamId: string; ttl: string }) => {
throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500 }); throw new AppError({ message: "Not implemented", service: SERVICE_NAME, method: "updateChecksTTL", status: 500, details: { teamId, ttl } });
}; };
} }
@@ -170,7 +170,7 @@ class GeoChecksService implements IGeoChecksService {
}); });
} }
let { sortOrder, dateRange, page, rowsPerPage, continent } = query; const { sortOrder, dateRange, page, rowsPerPage, continent } = query;
const continents = continent ? (Array.isArray(continent) ? continent : [continent]) : undefined; const continents = continent ? (Array.isArray(continent) ? continent : [continent]) : undefined;
this.logger.debug({ this.logger.debug({
@@ -2,7 +2,7 @@ const SERVICE_NAME = "JobQueueHelper";
import type { Monitor } from "@/types/monitor.js"; import type { Monitor } from "@/types/monitor.js";
import { AppError } from "@/utils/AppError.js"; import { AppError } from "@/utils/AppError.js";
import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js"; import { INetworkService, INotificationsService, IStatusService } from "@/service/index.js";
import type { StatusChangeResult, MonitorStatusResponse, HardwareStatusPayload, MonitorStatus } from "@/types/index.js"; import type { StatusChangeResult } from "@/types/index.js";
import IncidentService from "@/service/business/incidentService.js"; import IncidentService from "@/service/business/incidentService.js";
import type { IGeoChecksService } from "@/service/business/geoChecksService.js"; import type { IGeoChecksService } from "@/service/business/geoChecksService.js";
import { import {
@@ -3,6 +3,7 @@ import type { GeoCheck } from "@/types/index.js";
import type { IGeoChecksService } from "../business/geoChecksService.js"; import type { IGeoChecksService } from "../business/geoChecksService.js";
import type { ILogger } from "@/utils/logger.js"; import type { ILogger } from "@/utils/logger.js";
import type { ISettingsService } from "@/service/system/settingsService.js"; import type { ISettingsService } from "@/service/system/settingsService.js";
import { ICheckService } from "../business/checkService.js";
const SERVICE_NAME = "BufferService"; const SERVICE_NAME = "BufferService";
export interface IBufferService { export interface IBufferService {
@@ -19,13 +20,13 @@ class BufferService implements IBufferService {
private BUFFER_TIMEOUT: number; private BUFFER_TIMEOUT: number;
private logger: ILogger; private logger: ILogger;
private SERVICE_NAME: string; private SERVICE_NAME: string;
private buffer: any[]; private buffer: Check[];
private geoBuffer: any[]; private geoBuffer: GeoCheck[];
private bufferTimer: NodeJS.Timeout | null = null; private bufferTimer: NodeJS.Timeout | null = null;
private checksService: any; private checksService: ICheckService;
private geoChecksService: IGeoChecksService; private geoChecksService: IGeoChecksService;
constructor(logger: ILogger, checkService: any, geoChecksService: IGeoChecksService, settingsService: ISettingsService) { constructor(logger: ILogger, checkService: ICheckService, geoChecksService: IGeoChecksService, settingsService: ISettingsService) {
this.BUFFER_TIMEOUT = settingsService.getSettings().nodeEnv === "development" ? 10 : 1000 * 60 * 1; // 1 minute this.BUFFER_TIMEOUT = settingsService.getSettings().nodeEnv === "development" ? 10 : 1000 * 60 * 1; // 1 minute
this.logger = logger; this.logger = logger;
this.checksService = checkService; this.checksService = checkService;
@@ -48,12 +49,12 @@ class BufferService implements IBufferService {
addToBuffer(check: Check) { addToBuffer(check: Check) {
try { try {
this.buffer.push(check); this.buffer.push(check);
} catch (error: any) { } catch (error: unknown) {
this.logger.error({ this.logger.error({
message: error.message, message: error instanceof Error ? error.message : "Unknown error",
service: this.SERVICE_NAME, service: this.SERVICE_NAME,
method: "addToBuffer", method: "addToBuffer",
stack: error.stack, stack: error instanceof Error ? error.stack : undefined,
}); });
} }
} }
@@ -61,12 +62,12 @@ class BufferService implements IBufferService {
addGeoCheckToBuffer(geoCheck: GeoCheck) { addGeoCheckToBuffer(geoCheck: GeoCheck) {
try { try {
this.geoBuffer.push(geoCheck); this.geoBuffer.push(geoCheck);
} catch (error: any) { } catch (error: unknown) {
this.logger.error({ this.logger.error({
message: error.message, message: error instanceof Error ? error.message : "Unknown error",
service: this.SERVICE_NAME, service: this.SERVICE_NAME,
method: "addGeoCheckToBuffer", method: "addGeoCheckToBuffer",
stack: error.stack, stack: error instanceof Error ? error.stack : undefined,
}); });
} }
} }
@@ -98,12 +99,12 @@ class BufferService implements IBufferService {
} }
return false; return false;
} catch (error: any) { } catch (error: unknown) {
this.logger.error({ this.logger.error({
message: error.message, message: error instanceof Error ? error.message : "Unknown error",
service: this.SERVICE_NAME, service: this.SERVICE_NAME,
method: "removeCheckFromBuffer", method: "removeCheckFromBuffer",
stack: error.stack, stack: error instanceof Error ? error.stack : undefined,
}); });
return false; return false;
} }
@@ -117,12 +118,12 @@ class BufferService implements IBufferService {
try { try {
await this.flushBuffer(); await this.flushBuffer();
await this.flushGeoBuffer(); await this.flushGeoBuffer();
} catch (error: any) { } catch (error: unknown) {
this.logger.error({ this.logger.error({
message: `Error in flush cycle: ${error.message}`, message: error instanceof Error ? error.message : "Unknown error",
service: this.SERVICE_NAME, service: this.SERVICE_NAME,
method: "scheduleNextFlush", method: "scheduleNextFlush",
stack: error.stack, stack: error instanceof Error ? error.stack : undefined,
}); });
} finally { } finally {
// Schedule the next flush only after the current one completes // Schedule the next flush only after the current one completes
@@ -141,12 +142,12 @@ class BufferService implements IBufferService {
await this.checksService.createChecks(this.buffer); await this.checksService.createChecks(this.buffer);
this.buffer = []; this.buffer = [];
} }
} catch (error: any) { } catch (error: unknown) {
this.logger.error({ this.logger.error({
message: `Error flushing checks buffer: ${error.message}`, message: error instanceof Error ? error.message : "Unknown error",
service: this.SERVICE_NAME, service: this.SERVICE_NAME,
method: "flushBuffer", method: "flushBuffer",
stack: error.stack, stack: error instanceof Error ? error.stack : undefined,
}); });
// Clear buffer even on error to prevent infinite retry loops // Clear buffer even on error to prevent infinite retry loops
this.buffer = []; this.buffer = [];
@@ -164,12 +165,12 @@ class BufferService implements IBufferService {
await this.geoChecksService.createGeoChecks(this.geoBuffer); await this.geoChecksService.createGeoChecks(this.geoBuffer);
this.geoBuffer = []; this.geoBuffer = [];
} }
} catch (error: any) { } catch (error: unknown) {
this.logger.error({ this.logger.error({
message: `Error flushing geo checks buffer: ${error.message}`, message: error instanceof Error ? error.message : "Unknown error",
service: this.SERVICE_NAME, service: this.SERVICE_NAME,
method: "flushGeoBuffer", method: "flushGeoBuffer",
stack: error.stack, stack: error instanceof Error ? error.stack : undefined,
}); });
// Clear buffer even on error to prevent infinite retry loops // Clear buffer even on error to prevent infinite retry loops
this.geoBuffer = []; this.geoBuffer = [];
@@ -1,27 +1,51 @@
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import path from "path";
import { EmailTransportConfig } from "@/types/index.js"; import { EmailTransportConfig } from "@/types/index.js";
import { ISettingsService } from "@/service/system/settingsService.js";
import { ILogger } from "@/utils/logger.js";
import fs from "node:fs";
import path from "node:path";
import nodemailer from "nodemailer";
import mjml2html from "mjml";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const SERVICE_NAME = "EmailService"; const SERVICE_NAME = "EmailService";
type MjmlFn = typeof mjml2html;
type FileSystem = typeof fs;
type PathModule = typeof path;
type Mailer = typeof nodemailer;
type TemplateCompiler = (template: string) => (context: Record<string, unknown>) => string;
class EmailService { export interface IEmailService {
init(): void;
buildEmail(template: string, context: Record<string, unknown>): Promise<string | undefined>;
sendEmail(to: string, subject: string, html: string, transportConfig?: EmailTransportConfig): Promise<string | false | undefined>;
}
class EmailService implements IEmailService {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private settingsService: any; private settingsService: ISettingsService;
private fs: any; private fs: FileSystem;
private path: any; private path: PathModule;
private compile: any; private compile: TemplateCompiler;
private mjml2html: any; private mjml2html: MjmlFn;
private nodemailer: any; private nodemailer: Mailer;
private logger: any; private logger: ILogger;
private transporter: any; private transporter: ReturnType<typeof import("nodemailer").createTransport> | null = null;
private templateLookup: Record<string, Function>; private templateLookup: Record<string, ((context: Record<string, unknown>) => string) | undefined>;
private loadTemplate: (templateName: string) => Function; private loadTemplate: (templateName: string) => ((context: Record<string, unknown>) => string) | undefined;
constructor(settingsService: any, fs: any, path: any, compile: any, mjml2html: any, nodemailer: any, logger: any) { constructor(
settingsService: ISettingsService,
fs: FileSystem,
path: PathModule,
compile: TemplateCompiler,
mjml2html: MjmlFn,
nodemailer: Mailer,
logger: ILogger
) {
this.settingsService = settingsService; this.settingsService = settingsService;
this.fs = fs; this.fs = fs;
this.path = path; this.path = path;
@@ -30,9 +54,7 @@ class EmailService {
this.nodemailer = nodemailer; this.nodemailer = nodemailer;
this.logger = logger; this.logger = logger;
this.templateLookup = {}; this.templateLookup = {};
this.loadTemplate = () => { this.loadTemplate = () => undefined;
return () => {};
};
this.init(); this.init();
} }
@@ -46,12 +68,12 @@ class EmailService {
const templatePath = this.path.join(__dirname, `../../templates/${templateName}.mjml`); const templatePath = this.path.join(__dirname, `../../templates/${templateName}.mjml`);
const templateContent = this.fs.readFileSync(templatePath, "utf8"); const templateContent = this.fs.readFileSync(templatePath, "utf8");
return this.compile(templateContent); return this.compile(templateContent);
} catch (error: any) { } catch (error: unknown) {
this.logger.error({ this.logger.error({
message: error.message, message: error instanceof Error ? error.message : "Unknown error",
service: SERVICE_NAME, service: SERVICE_NAME,
method: "loadTemplate", method: "loadTemplate",
stack: error.stack, stack: error instanceof Error ? error.stack : undefined,
}); });
} }
}; };
@@ -66,17 +88,20 @@ class EmailService {
}; };
}; };
buildEmail = async (template: string, context: Record<string, any>) => { buildEmail = async (template: string, context: Record<string, unknown>) => {
try { try {
const mjml = this.templateLookup[template]?.(context); const mjml = this.templateLookup[template]?.(context);
if (!mjml) {
throw new Error(`Template ${template} not found`);
}
const html = await this.mjml2html(mjml); const html = await this.mjml2html(mjml);
return html.html; return html.html;
} catch (error: any) { } catch (error: unknown) {
this.logger.error({ this.logger.error({
message: error.message, message: error instanceof Error ? error.message : "Unknown error",
service: SERVICE_NAME, service: SERVICE_NAME,
method: "buildEmail", method: "buildEmail",
stack: error.stack, stack: error instanceof Error ? error.stack : undefined,
}); });
} }
}; };
@@ -125,11 +150,12 @@ class EmailService {
try { try {
await this.transporter.verify(); await this.transporter.verify();
} catch (error) { } catch (error: unknown) {
this.logger.warn({ this.logger.warn({
message: "Email transporter verification failed", message: "Email transporter verification failed",
service: SERVICE_NAME, service: SERVICE_NAME,
method: "verifyTransporter", method: "verifyTransporter",
stack: error instanceof Error ? error.stack : undefined,
}); });
return false; return false;
} }
@@ -142,12 +168,12 @@ class EmailService {
html: html, html: html,
}); });
return info?.messageId; return info?.messageId;
} catch (error: any) { } catch (error: unknown) {
this.logger.error({ this.logger.error({
message: error.message, message: error instanceof Error ? error.message : "Unknown error",
service: SERVICE_NAME, service: SERVICE_NAME,
method: "sendEmail", method: "sendEmail",
stack: error.stack, stack: error instanceof Error ? error.stack : undefined,
}); });
} }
}; };
@@ -95,12 +95,12 @@ class GlobalPingService implements IGlobalPingService {
}); });
return measurementId; return measurementId;
} catch (error: any) { } catch (error: unknown) {
this.logger.error({ this.logger.error({
message: "GlobalPing API unavailable, skipping geo check", message: "GlobalPing API unavailable, skipping geo check",
service: SERVICE_NAME, service: SERVICE_NAME,
method: "createMeasurement", method: "createMeasurement",
details: error, stack: error instanceof Error ? error.stack : undefined,
}); });
return null; return null;
} }
@@ -140,12 +140,12 @@ class GlobalPingService implements IGlobalPingService {
// Still in-progress, wait and poll again // Still in-progress, wait and poll again
await this.sleep(POLL_INTERVAL_MS); await this.sleep(POLL_INTERVAL_MS);
} catch (error: any) { } catch (error: unknown) {
this.logger.error({ this.logger.error({
message: "Error polling GlobalPing API", message: "Error polling GlobalPing API",
service: SERVICE_NAME, service: SERVICE_NAME,
method: "pollForResults", method: "pollForResults",
details: error.message, stack: error instanceof Error ? error.stack : undefined,
}); });
return []; return [];
} }
@@ -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 { import type {
Monitor, Monitor,
MonitorStatusResponse, MonitorStatusResponse,
@@ -14,28 +12,11 @@ import type {
} from "@/types/index.js"; } from "@/types/index.js";
import type { AxiosStatic } from "axios"; import type { AxiosStatic } from "axios";
import { AppError } from "@/utils/AppError.js"; 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 { ILogger } from "@/utils/logger.js";
import { IStatusProvider } from "./network/IStatusProvider.js";
const SERVICE_NAME = "NetworkService"; 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 { export interface INetworkService {
readonly serviceName: string; readonly serviceName: string;
requestStatus( requestStatus(
@@ -82,85 +63,27 @@ class NetworkService implements INetworkService {
private PING_ERROR: number; private PING_ERROR: number;
private axios: AxiosStatic; 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 logger: ILogger;
private Docker: typeof import("dockerode"); private pingProvider;
private net: typeof import("net"); private httpProvider;
private settingsService: ISettingsService; private pageSpeedProvider;
private grpc: typeof import("@grpc/grpc-js"); private hardwareProvider;
private protoLoader: typeof import("@grpc/proto-loader"); private dockerProvider;
private portProvider;
private buildStatusResponse = <T>({ private gameProvider;
monitor, private grpcProvider;
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>;
};
constructor( constructor(
axios: AxiosStatic, 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, logger: ILogger,
Docker: typeof import("dockerode"), pingProvider: IStatusProvider<PingStatusPayload>,
net: typeof import("net"), httpProvider: IStatusProvider<HttpStatusPayload>,
settingsService: ISettingsService, pagespeedProvider: IStatusProvider<PageSpeedStatusPayload>,
grpc: typeof import("@grpc/grpc-js"), hardwareProvider: IStatusProvider<HardwareStatusPayload>,
protoLoader: typeof import("@grpc/proto-loader") dockerProvider: IStatusProvider<DockerStatusPayload>,
portProvider: IStatusProvider<PortStatusPayload>,
gameProvider: IStatusProvider<GameStatusPayload>,
grpcProvider: IStatusProvider<GrpcStatusPayload>
) { ) {
this.TYPE_PING = "ping"; this.TYPE_PING = "ping";
this.TYPE_HTTP = "http"; this.TYPE_HTTP = "http";
@@ -174,45 +97,22 @@ class NetworkService implements INetworkService {
this.NETWORK_ERROR = 5000; this.NETWORK_ERROR = 5000;
this.PING_ERROR = 5001; this.PING_ERROR = 5001;
this.axios = axios; this.axios = axios;
this.https = https;
this.jmespath = jmespath;
this.GameDig = GameDig;
this.ping = ping;
this.logger = logger; this.logger = logger;
this.Docker = Docker;
this.net = net;
this.settingsService = settingsService;
this.grpc = grpc;
this.protoLoader = protoLoader;
const cacheable = new CacheableLookup(); this.pingProvider = pingProvider;
this.httpProvider = httpProvider;
this.got = got.extend({ this.pageSpeedProvider = pagespeedProvider;
dnsCache: cacheable, this.hardwareProvider = hardwareProvider;
timeout: { this.dockerProvider = dockerProvider;
request: 30000, this.portProvider = portProvider;
}, this.gameProvider = gameProvider;
retry: { limit: 1 }, this.grpcProvider = grpcProvider;
});
} }
get serviceName(): string { get serviceName(): string {
return NetworkService.SERVICE_NAME; 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 // Main entry point
async requestStatus( async requestStatus(
monitor: Monitor monitor: Monitor
@@ -231,628 +131,26 @@ class NetworkService implements INetworkService {
const type = monitor?.type || "unknown"; const type = monitor?.type || "unknown";
switch (type) { switch (type) {
case this.TYPE_PING: case this.TYPE_PING:
return await this.requestPing(monitor); return await this.pingProvider.handle(monitor);
case this.TYPE_HTTP: case this.TYPE_HTTP:
return await this.requestHttp<HttpStatusPayload>(monitor); return await this.httpProvider.handle(monitor);
case this.TYPE_PAGESPEED: case this.TYPE_PAGESPEED:
return await this.requestPageSpeed(monitor); return await this.pageSpeedProvider.handle(monitor);
case this.TYPE_HARDWARE: case this.TYPE_HARDWARE:
return await this.requestHardware(monitor); return await this.hardwareProvider.handle(monitor);
case this.TYPE_DOCKER: case this.TYPE_DOCKER:
return await this.requestDocker(monitor); return await this.dockerProvider.handle(monitor);
case this.TYPE_PORT: case this.TYPE_PORT:
return await this.requestPort(monitor); return await this.portProvider.handle(monitor);
case this.TYPE_GAME: case this.TYPE_GAME:
return await this.requestGame(monitor); return await this.gameProvider.handle(monitor);
case this.TYPE_GRPC: case this.TYPE_GRPC:
return await this.requestGrpc(monitor); return await this.grpcProvider.handle(monitor);
default: default:
return this.handleUnsupportedType(type); 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 } = 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 config: Record<string, unknown> = {
headers: secret ? { Authorization: `Bearer ${secret}` } : 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> { private async handleUnsupportedType(type: string): Promise<MonitorStatusResponse> {
return { return {
monitorId: "unknown", monitorId: "unknown",
@@ -1,4 +1,4 @@
import type { Monitor, MonitorStatusResponse } from "@/types/index.js"; import type { HardwareStatusPayload, Monitor, MonitorStatusResponse } from "@/types/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js"; import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import type { import type {
NotificationMessage, NotificationMessage,
@@ -30,8 +30,8 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
clientHost: string clientHost: string
): NotificationMessage { ): NotificationMessage {
const type = this.determineNotificationType(decision, monitor); const type = this.determineNotificationType(decision, monitor);
const severity = this.determineSeverity(type, monitor.status); const severity = this.determineSeverity(type);
const content = this.buildContent(type, monitor, monitorStatusResponse, decision, clientHost); const content = this.buildContent(type, monitor, monitorStatusResponse);
return { return {
type, type,
@@ -77,7 +77,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
return "monitor_up"; return "monitor_up";
} }
private determineSeverity(type: NotificationType, monitorStatus: string): NotificationSeverity { private determineSeverity(type: NotificationType): NotificationSeverity {
switch (type) { switch (type) {
case "monitor_down": case "monitor_down":
return "critical"; return "critical";
@@ -93,28 +93,22 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
} }
} }
private buildContent( private buildContent(type: NotificationType, monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): NotificationContent {
type: NotificationType,
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision,
clientHost: string
): NotificationContent {
switch (type) { switch (type) {
case "monitor_down": case "monitor_down":
return this.buildMonitorDownContent(monitor, monitorStatusResponse, clientHost); return this.buildMonitorDownContent(monitor, monitorStatusResponse);
case "monitor_up": case "monitor_up":
return this.buildMonitorUpContent(monitor, clientHost); return this.buildMonitorUpContent(monitor);
case "threshold_breach": case "threshold_breach":
return this.buildThresholdBreachContent(monitor, monitorStatusResponse, decision, clientHost); return this.buildThresholdBreachContent(monitor, monitorStatusResponse as MonitorStatusResponse<HardwareStatusPayload>);
case "threshold_resolved": case "threshold_resolved":
return this.buildThresholdResolvedContent(monitor, clientHost); return this.buildThresholdResolvedContent(monitor);
default: default:
return this.buildDefaultContent(monitor, clientHost); return this.buildDefaultContent(monitor);
} }
} }
private buildMonitorDownContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, clientHost: string): NotificationContent { private buildMonitorDownContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): NotificationContent {
const title = `Monitor Down: ${monitor.name}`; const title = `Monitor Down: ${monitor.name}`;
const summary = `Monitor "${monitor.name}" is currently down and unreachable.`; const summary = `Monitor "${monitor.name}" is currently down and unreachable.`;
const details = [`URL: ${monitor.url}`, `Status: Down`, `Type: ${monitor.type}`]; const details = [`URL: ${monitor.url}`, `Status: Down`, `Type: ${monitor.type}`];
@@ -137,7 +131,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
}; };
} }
private buildMonitorUpContent(monitor: Monitor, clientHost: string): NotificationContent { private buildMonitorUpContent(monitor: Monitor): NotificationContent {
const title = `Monitor Recovered: ${monitor.name}`; const title = `Monitor Recovered: ${monitor.name}`;
const summary = `Monitor "${monitor.name}" is back up and operational.`; const summary = `Monitor "${monitor.name}" is back up and operational.`;
const details = [`URL: ${monitor.url}`, `Status: Up`, `Type: ${monitor.type}`]; const details = [`URL: ${monitor.url}`, `Status: Up`, `Type: ${monitor.type}`];
@@ -150,12 +144,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
}; };
} }
private buildThresholdBreachContent( private buildThresholdBreachContent(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse<HardwareStatusPayload>): NotificationContent {
monitor: Monitor,
monitorStatusResponse: MonitorStatusResponse,
decision: MonitorActionDecision,
clientHost: string
): NotificationContent {
const title = `Threshold Breach: ${monitor.name}`; const title = `Threshold Breach: ${monitor.name}`;
const summary = `Monitor "${monitor.name}" has breached one or more thresholds.`; const summary = `Monitor "${monitor.name}" has breached one or more thresholds.`;
const details = [`URL: ${monitor.url}`, `Status: Breached`, `Type: ${monitor.type}`]; const details = [`URL: ${monitor.url}`, `Status: Breached`, `Type: ${monitor.type}`];
@@ -171,7 +160,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
}; };
} }
private buildThresholdResolvedContent(monitor: Monitor, clientHost: string): NotificationContent { private buildThresholdResolvedContent(monitor: Monitor): NotificationContent {
const title = `Thresholds Resolved: ${monitor.name}`; const title = `Thresholds Resolved: ${monitor.name}`;
const summary = `Monitor "${monitor.name}" thresholds have returned to normal.`; const summary = `Monitor "${monitor.name}" thresholds have returned to normal.`;
const details = [`URL: ${monitor.url}`, `Status: Up`, `Type: ${monitor.type}`]; const details = [`URL: ${monitor.url}`, `Status: Up`, `Type: ${monitor.type}`];
@@ -184,7 +173,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
}; };
} }
private buildDefaultContent(monitor: Monitor, clientHost: string): NotificationContent { private buildDefaultContent(monitor: Monitor): NotificationContent {
return { return {
title: `Monitor: ${monitor.name}`, title: `Monitor: ${monitor.name}`,
summary: `Status update for monitor "${monitor.name}".`, summary: `Status update for monitor "${monitor.name}".`,
@@ -193,7 +182,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
}; };
} }
public extractThresholdBreaches(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse): ThresholdBreach[] { public extractThresholdBreaches(monitor: Monitor, monitorStatusResponse: MonitorStatusResponse<HardwareStatusPayload>): ThresholdBreach[] {
const breaches: ThresholdBreach[] = []; const breaches: ThresholdBreach[] = [];
// Check if this is a hardware monitor with threshold data // Check if this is a hardware monitor with threshold data
@@ -202,7 +191,7 @@ export class NotificationMessageBuilder implements INotificationMessageBuilder {
} }
// Cast to HardwareStatusPayload type // Cast to HardwareStatusPayload type
const payload = monitorStatusResponse.payload as { data?: any }; const payload = monitorStatusResponse.payload;
const hardware = payload.data; const hardware = payload.data;
if (!hardware) { if (!hardware) {
@@ -1,14 +1,15 @@
const SERVICE_NAME = "DiscordProvider"; const SERVICE_NAME = "DiscordProvider";
import type { Notification } from "@/types/index.js"; import type { AlertDiscordPayload, DiscordEmbedField, Notification } from "@/types/index.js";
import { INotificationProvider } from "@/service/index.js"; import { INotificationProvider } from "@/service/index.js";
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
import type { NotificationMessage } from "@/types/notificationMessage.js"; import type { NotificationMessage, NotificationSeverity } from "@/types/notificationMessage.js";
import got from "got"; import got from "got";
import { ILogger } from "@/utils/logger.js";
export class DiscordProvider implements INotificationProvider { export class DiscordProvider implements INotificationProvider {
private logger: any; private logger: ILogger;
constructor(logger: any) { constructor(logger: ILogger) {
this.logger = logger; this.logger = logger;
} }
@@ -36,9 +37,6 @@ export class DiscordProvider implements INotificationProvider {
} }
}; };
/**
* New unified message format implementation
*/
async sendMessage(notification: Notification, message: NotificationMessage): Promise<boolean> { async sendMessage(notification: Notification, message: NotificationMessage): Promise<boolean> {
if (!notification.address) { if (!notification.address) {
this.logger.warn({ this.logger.warn({
@@ -71,18 +69,17 @@ export class DiscordProvider implements INotificationProvider {
} }
} }
private buildDiscordEmbed(message: NotificationMessage): any { private buildDiscordEmbed(message: NotificationMessage): AlertDiscordPayload {
// Map severity to Discord embed colors const colorMap: Record<NotificationSeverity, number> = {
const colorMap: Record<string, number> = {
critical: 0xdc2626, // red-600 critical: 0xdc2626, // red-600
warning: 0xf59e0b, // amber-500 warning: 0xf59e0b, // amber-500
info: 0x3b82f6, // blue-500 info: 0x3b82f6, // blue-500
success: 0x10b981, // green-500 success: 0x10b981, // green-500
}; };
const color = colorMap[message.severity] || colorMap.info; const color = colorMap[message.severity] ?? colorMap.info;
const fields: Array<{ name: string; value: string; inline?: boolean }> = []; const fields: Array<DiscordEmbedField> = [];
// Add monitor details // Add monitor details
fields.push({ fields.push({
@@ -1,15 +1,15 @@
const SERVICE_NAME = "EmailProvider"; const SERVICE_NAME = "EmailProvider";
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import type { Notification } from "@/types/index.js";
import { INotificationProvider } from "@/service/index.js"; import { INotificationProvider } from "@/service/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import { buildTestEmail } from "@/service/infrastructure/notificationProviders/utils.js"; import { buildTestEmail } from "@/service/infrastructure/notificationProviders/utils.js";
import type { NotificationMessage } from "@/types/notificationMessage.js"; import type { NotificationMessage } from "@/types/notificationMessage.js";
import type { ILogger } from "@/utils/logger.js";
import { IEmailService } from "@/service/infrastructure/emailService.js";
export class EmailProvider implements INotificationProvider { export class EmailProvider implements INotificationProvider {
private emailService: any; private emailService: IEmailService;
private logger: any; private logger: ILogger;
constructor(emailService: any, logger: any) { constructor(emailService: IEmailService, logger: ILogger) {
this.emailService = emailService; this.emailService = emailService;
this.logger = logger; this.logger = logger;
} }
@@ -18,6 +18,24 @@ export class EmailProvider implements INotificationProvider {
const subject = "Test notification"; const subject = "Test notification";
const html = await buildTestEmail(this.emailService); const html = await buildTestEmail(this.emailService);
if (!notification.address) {
this.logger.warn({
message: "Missing address",
service: SERVICE_NAME,
method: "sendTestAlert",
});
return false;
}
if (!html) {
this.logger.warn({
message: "Failed to build test email content",
service: SERVICE_NAME,
method: "sendTestAlert",
});
return false;
}
const messageId = await this.emailService.sendEmail(notification.address, subject, html); const messageId = await this.emailService.sendEmail(notification.address, subject, html);
if (!messageId) { if (!messageId) {
this.logger.warn({ this.logger.warn({
@@ -38,6 +56,15 @@ export class EmailProvider implements INotificationProvider {
const subject = this.buildSubject(message); const subject = this.buildSubject(message);
const html = await this.buildEmailFromMessage(message); const html = await this.buildEmailFromMessage(message);
if (!html) {
this.logger.warn({
message: "Failed to build email content",
service: SERVICE_NAME,
method: "sendMessage",
});
return false;
}
const messageId = await this.emailService.sendEmail(notification.address, subject, html); const messageId = await this.emailService.sendEmail(notification.address, subject, html);
if (!messageId) { if (!messageId) {
this.logger.warn({ this.logger.warn({
@@ -65,7 +92,7 @@ export class EmailProvider implements INotificationProvider {
} }
} }
private async buildEmailFromMessage(message: NotificationMessage): Promise<string> { private async buildEmailFromMessage(message: NotificationMessage): Promise<string | undefined> {
const context = { const context = {
title: message.content.title, title: message.content.title,
summary: message.content.summary, summary: message.content.summary,
@@ -88,16 +115,6 @@ export class EmailProvider implements INotificationProvider {
const html = await this.emailService.buildEmail("unifiedNotificationTemplate", context); const html = await this.emailService.buildEmail("unifiedNotificationTemplate", context);
this.logger.info({
message: "[DEBUG] Email HTML generated",
service: SERVICE_NAME,
method: "buildEmailFromMessage",
details: {
htmlLength: html?.length || 0,
htmlPreview: html?.substring(0, 200),
},
});
return html; return html;
} }
@@ -1,14 +1,15 @@
const SERVICE_NAME = "MatrixProvider"; const SERVICE_NAME = "MatrixProvider";
import got from "got"; import got from "got";
import type { INotificationProvider } from "@/service/index.js"; import type { INotificationProvider } from "@/service/index.js";
import type { Notification } from "@/types/index.js"; import type { AlertMatrixPayload, Notification } from "@/types/index.js";
import type { NotificationMessage } from "@/types/notificationMessage.js"; import type { NotificationMessage } from "@/types/notificationMessage.js";
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
import { ILogger } from "@/utils/logger.js";
export class MatrixProvider implements INotificationProvider { export class MatrixProvider implements INotificationProvider {
private logger: any; private logger: ILogger;
constructor(logger: any) { constructor(logger: ILogger) {
this.logger = logger; this.logger = logger;
} }
@@ -42,9 +43,6 @@ export class MatrixProvider implements INotificationProvider {
} }
}; };
/**
* New unified message format - builds Matrix HTML message from NotificationMessage
*/
sendMessage = async (notification: Notification, message: NotificationMessage): Promise<boolean> => { sendMessage = async (notification: Notification, message: NotificationMessage): Promise<boolean> => {
const { homeserverUrl, accessToken, roomId } = notification; const { homeserverUrl, accessToken, roomId } = notification;
@@ -87,7 +85,7 @@ export class MatrixProvider implements INotificationProvider {
* Returns both plain text and HTML formatted versions * Returns both plain text and HTML formatted versions
* Matrix supports HTML subset for rich formatting * Matrix supports HTML subset for rich formatting
*/ */
private buildMatrixMessage(message: NotificationMessage): { plainText: string; htmlText: string } { private buildMatrixMessage(message: NotificationMessage): AlertMatrixPayload {
const plainLines: string[] = []; const plainLines: string[] = [];
const htmlLines: string[] = []; const htmlLines: string[] = [];
@@ -1,15 +1,16 @@
const SERVICE_NAME = "PagerDutyProvider"; const SERVICE_NAME = "PagerDutyProvider";
import got from "got"; import got from "got";
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import type { Notification } from "@/types/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import { INotificationProvider } from "@/service/index.js"; import { INotificationProvider } from "@/service/index.js";
import type { NotificationMessage } from "@/types/notificationMessage.js"; import type { NotificationMessage } from "@/types/notificationMessage.js";
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
import { ILogger } from "@/utils/logger.js";
import { AlertPagerDutyPayload } from "@/types/index.js";
export class PagerDutyProvider implements INotificationProvider { export class PagerDutyProvider implements INotificationProvider {
private logger: any; private logger: ILogger;
constructor(logger: any) { constructor(logger: ILogger) {
this.logger = logger; this.logger = logger;
} }
@@ -77,7 +78,7 @@ export class PagerDutyProvider implements INotificationProvider {
} }
} }
private buildPagerDutyPayload(notification: Notification, message: NotificationMessage): any { private buildPagerDutyPayload(notification: Notification, message: NotificationMessage): AlertPagerDutyPayload {
// Map our notification type to PagerDuty event_action // Map our notification type to PagerDuty event_action
const eventAction = message.type === "monitor_up" || message.type === "threshold_resolved" ? "resolve" : "trigger"; const eventAction = message.type === "monitor_up" || message.type === "threshold_resolved" ? "resolve" : "trigger";
@@ -106,7 +107,7 @@ export class PagerDutyProvider implements INotificationProvider {
} }
// Build custom details // Build custom details
const customDetails: any = { const customDetails: Record<string, unknown> = {
monitor_name: message.monitor.name, monitor_name: message.monitor.name,
monitor_url: message.monitor.url, monitor_url: message.monitor.url,
monitor_type: message.monitor.type, monitor_type: message.monitor.type,
@@ -1,15 +1,15 @@
const SERVICE_NAME = "SlackProvider"; const SERVICE_NAME = "SlackProvider";
import type { Monitor, Notification, MonitorStatusResponse } from "@/types/index.js"; import type { Notification } from "@/types/index.js";
import { INotificationProvider } from "@/service/index.js"; import { INotificationProvider } from "@/service/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import type { NotificationMessage } from "@/types/notificationMessage.js"; import type { NotificationMessage } from "@/types/notificationMessage.js";
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
import got, { HTTPError } from "got"; import got, { HTTPError } from "got";
import { ILogger } from "@/utils/logger.js";
export class SlackProvider implements INotificationProvider { export class SlackProvider implements INotificationProvider {
private logger: any; private logger: ILogger;
constructor(logger: any) { constructor(logger: ILogger) {
this.logger = logger; this.logger = logger;
} }
@@ -78,7 +78,7 @@ export class SlackProvider implements INotificationProvider {
* Uses Slack's rich formatting with sections, fields, and context blocks * Uses Slack's rich formatting with sections, fields, and context blocks
*/ */
private buildSlackPayload(message: NotificationMessage): object { private buildSlackPayload(message: NotificationMessage): object {
const blocks: any[] = []; const blocks: unknown[] = [];
// Determine color based on severity // Determine color based on severity
const colorMap = { const colorMap = {
@@ -1,6 +1,5 @@
import { Monitor, HardwareStatusPayload, MonitorStatusResponse } from "@/types/index.js"; import { IEmailService } from "@/service/infrastructure/emailService.js";
// Test notification helpers - used by all providers for test alerts export const buildTestEmail = async (emailService: IEmailService) => {
export const buildTestEmail = async (emailService: any) => {
const context = { testName: "Monitoring System" }; const context = { testName: "Monitoring System" };
const html = await emailService.buildEmail("testEmailTemplate", context); const html = await emailService.buildEmail("testEmailTemplate", context);
return html; return html;
@@ -1,15 +1,15 @@
const SERVICE_NAME = "WebhookProvider"; const SERVICE_NAME = "WebhookProvider";
import type { Monitor, Alert, Notification, MonitorStatusResponse } from "@/types/index.js"; import type { Notification } from "@/types/index.js";
import { INotificationProvider } from "@/service/index.js"; import { INotificationProvider } from "@/service/index.js";
import type { MonitorActionDecision } from "@/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js";
import type { NotificationMessage } from "@/types/notificationMessage.js"; import type { NotificationMessage } from "@/types/notificationMessage.js";
import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js"; import { getTestMessage } from "@/service/infrastructure/notificationProviders/utils.js";
import got from "got"; import got from "got";
import { ILogger } from "@/utils/logger.js";
export class WebhookProvider implements INotificationProvider { export class WebhookProvider implements INotificationProvider {
private logger: any; private logger: ILogger;
constructor(logger: any) { constructor(logger: ILogger) {
this.logger = logger; this.logger = logger;
} }
@@ -46,10 +46,6 @@ export class WebhookProvider implements INotificationProvider {
} }
}; };
/**
* Build webhook payload from NotificationMessage
* Format: { text: string, severity: string, monitor: object, details: object }
*/
private buildWebhookPayload(message: NotificationMessage): object { private buildWebhookPayload(message: NotificationMessage): object {
const lines: string[] = []; const lines: string[] = [];
@@ -1,4 +1,4 @@
import type { HardwareStatusPayload, Monitor, MonitorStatusResponse, Notification, MonitorStatus } from "@/types/index.js"; import type { Monitor, MonitorStatusResponse, Notification } from "@/types/index.js";
import type { NotificationMessage } from "@/types/notificationMessage.js"; import type { NotificationMessage } from "@/types/notificationMessage.js";
import { IMonitorsRepository, INotificationsRepository } from "@/repositories/index.js"; import { IMonitorsRepository, INotificationsRepository } from "@/repositories/index.js";
import { INotificationProvider } from "./notificationProviders/INotificationProvider.js"; import { INotificationProvider } from "./notificationProviders/INotificationProvider.js";
@@ -15,13 +15,15 @@ import type {
GrpcStatusPayload, GrpcStatusPayload,
CheckSnapshot, CheckSnapshot,
MonitorStats, MonitorStats,
CheckDiskInfo,
} from "@/types/index.js"; } from "@/types/index.js";
import { AppError } from "@/utils/AppError.js";
import { ILogger } from "@/utils/logger.js"; import { ILogger } from "@/utils/logger.js";
import { IBufferService } from "./bufferService.js";
const SERVICE_NAME = "StatusService"; const SERVICE_NAME = "StatusService";
export interface IStatusService { export interface IStatusService {
updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: any }): Promise<boolean>; updateRunningStats(monitor: Monitor, networkResponse: MonitorStatusResponse): Promise<boolean>;
handleIncidentForCheck(check: any, monitor: Monitor, action: any, errorContext?: string): Promise<void>;
updateMonitorStatus( updateMonitorStatus(
statusResponse: MonitorStatusResponse< statusResponse: MonitorStatusResponse<
| PingStatusPayload | PingStatusPayload
@@ -40,15 +42,15 @@ export interface IStatusService {
export class StatusService implements IStatusService { export class StatusService implements IStatusService {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
private logger: any; private logger: ILogger;
private buffer: any; private buffer: IBufferService;
private monitorsRepository: IMonitorsRepository; private monitorsRepository: IMonitorsRepository;
private monitorStatsRepository: IMonitorStatsRepository; private monitorStatsRepository: IMonitorStatsRepository;
private checksRepository: IChecksRepository; private checksRepository: IChecksRepository;
constructor( constructor(
logger: ILogger, logger: ILogger,
buffer: any, buffer: IBufferService,
monitorsRepository: IMonitorsRepository, monitorsRepository: IMonitorsRepository,
monitorStatsRepository: IMonitorStatsRepository, monitorStatsRepository: IMonitorStatsRepository,
checksRepository: IChecksRepository checksRepository: IChecksRepository
@@ -64,7 +66,7 @@ export class StatusService implements IStatusService {
return StatusService.SERVICE_NAME; return StatusService.SERVICE_NAME;
} }
async updateRunningStats({ monitor, networkResponse }: { monitor: Monitor; networkResponse: MonitorStatusResponse }) { async updateRunningStats(monitor: Monitor, networkResponse: MonitorStatusResponse) {
try { try {
const monitorId = monitor.id; const monitorId = monitor.id;
const { responseTime, status } = networkResponse; const { responseTime, status } = networkResponse;
@@ -164,62 +166,17 @@ export class StatusService implements IStatusService {
} }
return true; return true;
} catch (error: any) { } catch (error: unknown) {
this.logger.error({ this.logger.error({
service: SERVICE_NAME, service: SERVICE_NAME,
message: error.message, message: error instanceof Error ? error.message : "Unknown error",
method: "updateRunningStats", method: "updateRunningStats",
stack: error.stack, stack: error instanceof Error ? error.stack : undefined,
}); });
return false; return false;
} }
} }
handleIncidentForCheck = async (check: Check, monitor: Monitor, action: any, errorContext = "incident handling") => {
try {
let savedCheck: Check | null = null;
if (!check.id) {
try {
savedCheck = await this.checksRepository.create(check);
this.buffer.removeCheckFromBuffer(check);
} catch (checkError: any) {
this.logger.error({
service: SERVICE_NAME,
method: "handleIncidentForCheck",
message: `Failed to save check immediately for ${errorContext}: ${checkError.message}`,
monitorId: monitor.id,
stack: checkError.stack,
});
savedCheck = null;
}
}
if (savedCheck && savedCheck.id) {
try {
this.buffer.addIncidentToBuffer({ monitor, check: savedCheck, action });
} catch (incidentError: any) {
this.logger.error({
service: SERVICE_NAME,
method: "handleIncidentForCheck",
message: `Failed to add incident to buffer for ${errorContext}: ${incidentError.message}`,
monitorId: monitor.id,
action,
stack: incidentError.stack,
});
}
}
} catch (error: any) {
this.logger.error({
service: SERVICE_NAME,
method: "handleIncidentForCheck",
message: `Error in ${errorContext}: ${error.message}`,
monitorId: monitor?.id,
stack: error.stack,
});
}
};
updateMonitorStatus = async ( updateMonitorStatus = async (
statusResponse: MonitorStatusResponse< statusResponse: MonitorStatusResponse<
| PingStatusPayload | PingStatusPayload
@@ -239,7 +196,7 @@ export class StatusService implements IStatusService {
const monitor = await this.monitorsRepository.findById(monitorId, teamId); const monitor = await this.monitorsRepository.findById(monitorId, teamId);
// Update running stats // Update running stats
this.updateRunningStats({ monitor, networkResponse: statusResponse }); this.updateRunningStats(monitor, statusResponse);
monitor.statusWindow = monitor.statusWindow || []; monitor.statusWindow = monitor.statusWindow || [];
monitor.statusWindow.push(status); monitor.statusWindow.push(status);
@@ -322,7 +279,8 @@ export class StatusService implements IStatusService {
const memoryBreach = memoryUsage !== -1 && memoryUsage > monitor.memoryAlertThreshold / 100; const memoryBreach = memoryUsage !== -1 && memoryUsage > monitor.memoryAlertThreshold / 100;
const diskBreach = const diskBreach =
metrics.disk?.some((d: any) => typeof d?.usage_percent === "number" && d.usage_percent > monitor.diskAlertThreshold / 100) ?? false; metrics.disk?.some((d: CheckDiskInfo) => typeof d?.usage_percent === "number" && d.usage_percent > monitor.diskAlertThreshold / 100) ??
false;
const temps = metrics.cpu?.temperature ?? []; const temps = metrics.cpu?.temperature ?? [];
const tempBreach = temps.some((temp: number) => temp > monitor.tempAlertThreshold); const tempBreach = temps.some((temp: number) => temp > monitor.tempAlertThreshold);
@@ -400,10 +358,13 @@ export class StatusService implements IStatusService {
timestamp: new Date().getTime(), timestamp: new Date().getTime(),
thresholdBreaches, thresholdBreaches,
}; };
} catch (error: any) { } catch (error: unknown) {
error.service = SERVICE_NAME; throw new AppError({
error.method = "updateStatus"; message: `Failed to update monitor status: ${error instanceof Error ? error.message : "Unknown error"}`,
throw error; details: `Error updating status for monitor with id ${check.metadata.monitorId}`,
service: SERVICE_NAME,
method: "updateMonitorStatus",
});
} }
}; };
@@ -419,13 +380,13 @@ export class StatusService implements IStatusService {
} }
this.buffer.addToBuffer(check); this.buffer.addToBuffer(check);
return true; return true;
} catch (error: any) { } catch (error: unknown) {
this.logger.error({ this.logger.error({
message: error.message, message: error instanceof Error ? error.message : "Unknown error",
service: error.service || SERVICE_NAME, service: SERVICE_NAME,
method: error.method || "insertCheck", method: "insertCheck",
details: error.details || `Error inserting check for monitor: ${check?.metadata.monitorId}`, details: { msg: `Error inserting check for monitor: ${check?.metadata.monitorId}` },
stack: error.stack, stack: error instanceof Error ? error.stack : undefined,
}); });
} }
}; };
+1 -1
View File
@@ -22,7 +22,7 @@ export interface ISettingsService {
} }
class SettingsService implements ISettingsService { class SettingsService implements ISettingsService {
static SERVICE_NAME = "SettingsService"; static SERVICE_NAME = SERVICE_NAME;
private settings: EnvConfig; private settings: EnvConfig;
private settingsRepository: ISettingsRepository; private settingsRepository: ISettingsRepository;
+20 -7
View File
@@ -4,16 +4,29 @@ export interface AlertWebhookPayload {
} }
export interface AlertPagerDutyPayload { export interface AlertPagerDutyPayload {
routingKey: string; routing_key?: string;
monitorUrl?: string; dedup_key?: string;
event_action?: "trigger" | "resolve";
payload: Record<string, unknown>;
} }
export interface AlertMatrixPayload { export interface AlertMatrixPayload {
friendlyName?: string; plainText: string;
homeserverUrl: string; htmlText: string;
accessToken: string; }
roomId: string;
monitorName: string; export interface DiscordEmbedField {
name: string;
value: string;
inline?: boolean;
}
export interface AlertDiscordPayload {
title: string;
description: string;
color: number;
fields: DiscordEmbedField[];
timestamp: string;
} }
export interface Alert { export interface Alert {
+1 -1
View File
@@ -5,7 +5,7 @@ declare global {
interface Request { interface Request {
file?: Multer.File; file?: Multer.File;
user?: User | undefined; user?: User | undefined;
resource?: any; resource?: unknown;
} }
} }
} }
+1 -1
View File
@@ -2,7 +2,7 @@ export type DurationUnit = "seconds" | "minutes" | "hours" | "days";
export interface MaintenanceWindow { export interface MaintenanceWindow {
id: string; id: string;
monitorId: String; monitorId: string;
teamId: string; teamId: string;
active: boolean; active: boolean;
name: string; name: string;
+6 -2
View File
@@ -14,6 +14,8 @@ import type {
MonitorStatus, MonitorStatus,
} from "@/types/index.js"; } from "@/types/index.js";
import type { QueryResult } from "gamedig";
export interface MonitorStatusResponse< export interface MonitorStatusResponse<
T = T =
| HttpStatusPayload | HttpStatusPayload
@@ -49,7 +51,7 @@ export interface PingStatusPayload {
host: string; host: string;
numeric_host?: string; numeric_host?: string;
alive: boolean; alive: boolean;
time: number; time: number | unknown;
times?: number[]; times?: number[];
output?: string; output?: string;
min?: string; min?: string;
@@ -100,7 +102,7 @@ export interface PortStatusPayload {
success: boolean; success: boolean;
} }
export type GameStatusPayload = Record<string, unknown>; export type GameStatusPayload = QueryResult;
export interface GrpcStatusPayload { export interface GrpcStatusPayload {
grpcStatusCode: number; grpcStatusCode: number;
@@ -134,3 +136,5 @@ export type StatusChangeResult = {
temp: boolean; temp: boolean;
}; };
}; };
export type MonitorStatusResponseOverrides<T> = Partial<Omit<MonitorStatusResponse<T>, "monitorId" | "teamId" | "type">>;