diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 95b5bacbe..9c9ba0c65 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -1,28 +1,28 @@
-**(Please remove this line only before submitting your PR. Ensure that all relevant items are checked before submission.)**
+**(Please remove this line only before submitting your PR. Ensure that all relevant items are checked before submission.)**
## Describe your changes
-Briefly describe the changes you made and their purpose.
+Briefly describe the changes you made and their purpose.
## Write your issue number after "Fixes "
-Fixes #123
+Fixes #123
## Please ensure all items are checked off before requesting a review. "Checked off" means you need to add an "x" character between brackets so they turn into checkmarks.
- [ ] (Do not skip this or your PR will be closed) I deployed the application locally.
- [ ] (Do not skip this or your PR will be closed) I have performed a self-reviewing and testing of my code.
- [ ] I have included the issue # in the PR.
-- [ ] I have added i18n support to visible strings (instead of `
Add
`, use):
+- [ ] I have added i18n support to visible strings (instead of `Add
`, use):
+
```Javascript
const { t } = useTranslation();
{t('add')}
```
+
- [ ] I have **not** included any files that are not related to my pull request, including package-lock and package-json if dependencies have not changed
- [ ] I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application).
- [ ] I made sure font sizes, color choices etc are all referenced from the theme. I don't have any hardcoded dimensions.
- [ ] My PR is granular and targeted to one specific feature.
-- [ ] There are two database implementations (MongoDB and PostgreSQL TimescaleDB) and I have taken care of them appropriately.
- [ ] I ran `npm run format` in server and client directories, which automatically formats your code.
- [ ] I took a screenshot or a video and attached to this PR if there is a UI change.
-
diff --git a/.gitignore b/.gitignore
index 055e3d31e..ff828c1ec 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,10 +3,8 @@
.VSCodeCounter
*.sh
mongo
-timescaledb
node_modules/
docs/architecture
docs/reviews
docs/todo
-docs/frontend
-docs/timescale
\ No newline at end of file
+docs/frontend
\ No newline at end of file
diff --git a/client/src/Hooks/useNotificationForm.ts b/client/src/Hooks/useNotificationForm.ts
index 0b42b6d40..d08f17548 100644
--- a/client/src/Hooks/useNotificationForm.ts
+++ b/client/src/Hooks/useNotificationForm.ts
@@ -60,6 +60,16 @@ function buildDefaults(data: Notification | null): NotificationFormData {
address: data.address || "",
};
}
+ if (data?.type === "twilio") {
+ return {
+ type: "twilio",
+ notificationName: data.notificationName || "",
+ accountSid: data.accountSid || "",
+ accessToken: data.accessToken || "",
+ phone: data.phone || "",
+ twilioPhoneNumber: data.twilioPhoneNumber || "",
+ };
+ }
if (data?.type === "pushover") {
return {
type: "pushover",
diff --git a/client/src/Pages/Notifications/create/index.tsx b/client/src/Pages/Notifications/create/index.tsx
index 3da9e6f75..0f6d9b652 100644
--- a/client/src/Pages/Notifications/create/index.tsx
+++ b/client/src/Pages/Notifications/create/index.tsx
@@ -149,7 +149,8 @@ const NotificationsCreatePage = () => {
/>
{watchedType !== "matrix" &&
watchedType !== "telegram" &&
- watchedType !== "pushover" && (
+ watchedType !== "pushover" &&
+ watchedType !== "twilio" && (
{
}
/>
)}
+ {watchedType === "twilio" && (
+
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+ (
+
+ )}
+ />
+
+ }
+ />
+ )}
{watchedType === "matrix" && (
;
diff --git a/client/src/locales/en.json b/client/src/locales/en.json
index 5e016df8b..eb9a34339 100644
--- a/client/src/locales/en.json
+++ b/client/src/locales/en.json
@@ -969,6 +969,18 @@
"placeholderAppToken": "azGDORePK8gMaC0QOYAMyEEuzJnyUi",
"optionUserKey": "User key",
"placeholderUserKey": "uQiRzpo4DXghDmr9QzzfQu27cmVRsG"
+ },
+ "twilio": {
+ "title": "Twilio SMS configuration",
+ "description": "Configure Twilio to send SMS notifications to a phone number.",
+ "optionAccountSid": "Account SID",
+ "placeholderAccountSid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "optionAuthToken": "Auth token",
+ "placeholderAuthToken": "your_auth_token",
+ "optionFromNumber": "From number (Twilio)",
+ "placeholderFromNumber": "+15551234567",
+ "optionToNumber": "To number (recipient)",
+ "placeholderToNumber": "+15559876543"
}
},
"table": {
diff --git a/server/package-lock.json b/server/package-lock.json
index 1b9061d0c..8423aa8d0 100644
--- a/server/package-lock.json
+++ b/server/package-lock.json
@@ -14,7 +14,6 @@
"@pulsecron/pulse": "1.6.8",
"axios": "^1.7.2",
"bcryptjs": "3.0.2",
- "bullmq": "5.41.2",
"cacheable-lookup": "7.0.0",
"compression": "1.8.1",
"cookie-parser": "^1.4.7",
@@ -22,7 +21,7 @@
"dockerode": "4.0.6",
"dotenv": "^16.4.5",
"express": "^4.19.2",
- "express-rate-limit": "8.0.1",
+ "express-rate-limit": "8.3.2",
"gamedig": "5.2.0",
"got": "14.4.7",
"handlebars": "^4.7.8",
@@ -36,8 +35,7 @@
"mjml": "^5.0.0-alpha.4",
"mongoose": "^8.3.3",
"multer": "^1.4.5-lts.1",
- "nodemailer": "8.0.1",
- "pg": "8.20.0",
+ "nodemailer": "8.0.5",
"ping": "0.4.4",
"sharp": "0.33.5",
"ssl-checker": "2.0.10",
@@ -63,7 +61,6 @@
"@types/multer": "^2.0.0",
"@types/nodemailer": "7.0.1",
"@types/papaparse": "^5.5.2",
- "@types/pg": "8.20.0",
"@types/ping": "0.4.4",
"@types/swagger-ui-express": "4.1.8",
"@types/ws": "^8.18.1",
@@ -3169,84 +3166,6 @@
"sparse-bitfield": "^3.0.3"
}
},
- "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
- "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
- "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "darwin"
- ]
- },
- "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
- "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
- "cpu": [
- "arm"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
- "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
- "cpu": [
- "arm64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
- "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "linux"
- ]
- },
- "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
- "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
- "cpu": [
- "x64"
- ],
- "license": "MIT",
- "optional": true,
- "os": [
- "win32"
- ]
- },
"node_modules/@napi-rs/wasm-runtime": {
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
@@ -4498,18 +4417,6 @@
"@types/node": "*"
}
},
- "node_modules/@types/pg": {
- "version": "8.20.0",
- "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz",
- "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==",
- "dev": true,
- "license": "MIT",
- "dependencies": {
- "@types/node": "*",
- "pg-protocol": "*",
- "pg-types": "^2.2.0"
- }
- },
"node_modules/@types/ping": {
"version": "0.4.4",
"resolved": "https://registry.npmjs.org/@types/ping/-/ping-0.4.4.tgz",
@@ -5534,14 +5441,14 @@
"license": "MIT"
},
"node_modules/axios": {
- "version": "1.13.6",
- "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
- "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
+ "version": "1.15.1",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.1.tgz",
+ "integrity": "sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
- "proxy-from-env": "^1.1.0"
+ "proxy-from-env": "^2.1.0"
}
},
"node_modules/babel-jest": {
@@ -6007,21 +5914,6 @@
"node": ">=10.0.0"
}
},
- "node_modules/bullmq": {
- "version": "5.41.2",
- "resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.2.tgz",
- "integrity": "sha512-wqsUIHW2Td86mTKTepqQpKLLUtP4gmX89bUO1YL2fAorxwj3da1GYtroGZMCg/zgB/+zMRsbylL6DHyMUWX7fA==",
- "license": "MIT",
- "dependencies": {
- "cron-parser": "^4.9.0",
- "ioredis": "^5.4.1",
- "msgpackr": "^1.11.2",
- "node-abort-controller": "^3.1.1",
- "semver": "^7.5.4",
- "tslib": "^2.0.0",
- "uuid": "^9.0.0"
- }
- },
"node_modules/busboy": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
@@ -7270,13 +7162,10 @@
}
},
"node_modules/dompurify": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
- "integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==",
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.0.tgz",
+ "integrity": "sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==",
"license": "(MPL-2.0 OR Apache-2.0)",
- "engines": {
- "node": ">=20"
- },
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
}
@@ -7997,12 +7886,12 @@
}
},
"node_modules/express-rate-limit": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.0.1.tgz",
- "integrity": "sha512-aZVCnybn7TVmxO4BtlmnvX+nuz8qHW124KKJ8dumsBsmv5ZLxE0pYu7S2nwyRBGHHCAzdmnGyrc5U/rksSPO7Q==",
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
+ "integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==",
"license": "MIT",
"dependencies": {
- "ip-address": "10.0.1"
+ "ip-address": "10.1.0"
},
"engines": {
"node": ">= 16"
@@ -8275,9 +8164,9 @@
"license": "MIT"
},
"node_modules/follow-redirects": {
- "version": "1.15.11",
- "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
- "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
+ "version": "1.16.0",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
+ "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
"funding": [
{
"type": "individual",
@@ -9194,9 +9083,9 @@
}
},
"node_modules/ip-address": {
- "version": "10.0.1",
- "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
- "integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
+ "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
"license": "MIT",
"engines": {
"node": ">= 12"
@@ -11688,37 +11577,6 @@
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
- "node_modules/msgpackr": {
- "version": "1.11.5",
- "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz",
- "integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==",
- "license": "MIT",
- "optionalDependencies": {
- "msgpackr-extract": "^3.0.2"
- }
- },
- "node_modules/msgpackr-extract": {
- "version": "3.0.3",
- "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
- "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
- "hasInstallScript": true,
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "node-gyp-build-optional-packages": "5.2.2"
- },
- "bin": {
- "download-msgpackr-prebuilds": "bin/download-prebuilds.js"
- },
- "optionalDependencies": {
- "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
- "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
- "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
- "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
- "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
- "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
- }
- },
"node_modules/multer": {
"version": "1.4.5-lts.2",
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
@@ -11828,12 +11686,6 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"license": "MIT"
},
- "node_modules/node-abort-controller": {
- "version": "3.1.1",
- "resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
- "integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==",
- "license": "MIT"
- },
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@@ -11896,21 +11748,6 @@
"webidl-conversions": "^3.0.0"
}
},
- "node_modules/node-gyp-build-optional-packages": {
- "version": "5.2.2",
- "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
- "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
- "license": "MIT",
- "optional": true,
- "dependencies": {
- "detect-libc": "^2.0.1"
- },
- "bin": {
- "node-gyp-build-optional-packages": "bin.js",
- "node-gyp-build-optional-packages-optional": "optional.js",
- "node-gyp-build-optional-packages-test": "build-test.js"
- }
- },
"node_modules/node-int64": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
@@ -11925,9 +11762,9 @@
"license": "MIT"
},
"node_modules/nodemailer": {
- "version": "8.0.1",
- "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz",
- "integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==",
+ "version": "8.0.5",
+ "resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz",
+ "integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==",
"license": "MIT-0",
"engines": {
"node": ">=6.0.0"
@@ -12359,96 +12196,6 @@
"node": ">=8"
}
},
- "node_modules/pg": {
- "version": "8.20.0",
- "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
- "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
- "license": "MIT",
- "peer": true,
- "dependencies": {
- "pg-connection-string": "^2.12.0",
- "pg-pool": "^3.13.0",
- "pg-protocol": "^1.13.0",
- "pg-types": "2.2.0",
- "pgpass": "1.0.5"
- },
- "engines": {
- "node": ">= 16.0.0"
- },
- "optionalDependencies": {
- "pg-cloudflare": "^1.3.0"
- },
- "peerDependencies": {
- "pg-native": ">=3.0.1"
- },
- "peerDependenciesMeta": {
- "pg-native": {
- "optional": true
- }
- }
- },
- "node_modules/pg-cloudflare": {
- "version": "1.3.0",
- "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
- "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
- "license": "MIT",
- "optional": true
- },
- "node_modules/pg-connection-string": {
- "version": "2.12.0",
- "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
- "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
- "license": "MIT"
- },
- "node_modules/pg-int8": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
- "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
- "license": "ISC",
- "engines": {
- "node": ">=4.0.0"
- }
- },
- "node_modules/pg-pool": {
- "version": "3.13.0",
- "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
- "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
- "license": "MIT",
- "peerDependencies": {
- "pg": ">=8.0"
- }
- },
- "node_modules/pg-protocol": {
- "version": "1.13.0",
- "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz",
- "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
- "license": "MIT"
- },
- "node_modules/pg-types": {
- "version": "2.2.0",
- "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
- "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
- "license": "MIT",
- "dependencies": {
- "pg-int8": "1.0.1",
- "postgres-array": "~2.0.0",
- "postgres-bytea": "~1.0.0",
- "postgres-date": "~1.0.4",
- "postgres-interval": "^1.1.0"
- },
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/pgpass": {
- "version": "1.0.5",
- "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
- "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
- "license": "MIT",
- "dependencies": {
- "split2": "^4.1.0"
- }
- },
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -13074,45 +12821,6 @@
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
"license": "MIT"
},
- "node_modules/postgres-array": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
- "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
- "license": "MIT",
- "engines": {
- "node": ">=4"
- }
- },
- "node_modules/postgres-bytea": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
- "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/postgres-date": {
- "version": "1.0.7",
- "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
- "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
- "license": "MIT",
- "engines": {
- "node": ">=0.10.0"
- }
- },
- "node_modules/postgres-interval": {
- "version": "1.2.0",
- "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
- "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
- "license": "MIT",
- "dependencies": {
- "xtend": "^4.0.0"
- },
- "engines": {
- "node": ">=0.10.0"
- }
- },
"node_modules/posthtml": {
"version": "0.16.7",
"resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.7.tgz",
@@ -13293,9 +13001,9 @@
"license": "MIT"
},
"node_modules/protobufjs": {
- "version": "7.5.4",
- "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
- "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
+ "version": "7.5.5",
+ "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz",
+ "integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==",
"hasInstallScript": true,
"license": "BSD-3-Clause",
"dependencies": {
@@ -13330,10 +13038,13 @@
}
},
"node_modules/proxy-from-env": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
- "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
- "license": "MIT"
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
+ "integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
},
"node_modules/pstree.remy": {
"version": "1.1.8",
@@ -14158,15 +13869,6 @@
"integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==",
"license": "ISC"
},
- "node_modules/split2": {
- "version": "4.2.0",
- "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
- "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
- "license": "ISC",
- "engines": {
- "node": ">= 10.x"
- }
- },
"node_modules/sprintf-js": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
@@ -14912,6 +14614,7 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "devOptional": true,
"license": "0BSD"
},
"node_modules/tsx": {
diff --git a/server/package.json b/server/package.json
index d24e21864..4251a2f9d 100755
--- a/server/package.json
+++ b/server/package.json
@@ -32,7 +32,6 @@
"@pulsecron/pulse": "1.6.8",
"axios": "^1.7.2",
"bcryptjs": "3.0.2",
- "bullmq": "5.41.2",
"cacheable-lookup": "7.0.0",
"compression": "1.8.1",
"cookie-parser": "^1.4.7",
@@ -40,7 +39,7 @@
"dockerode": "4.0.6",
"dotenv": "^16.4.5",
"express": "^4.19.2",
- "express-rate-limit": "8.0.1",
+ "express-rate-limit": "8.3.2",
"gamedig": "5.2.0",
"got": "14.4.7",
"handlebars": "^4.7.8",
@@ -54,8 +53,7 @@
"mjml": "^5.0.0-alpha.4",
"mongoose": "^8.3.3",
"multer": "^1.4.5-lts.1",
- "nodemailer": "8.0.1",
- "pg": "8.20.0",
+ "nodemailer": "8.0.5",
"ping": "0.4.4",
"sharp": "0.33.5",
"ssl-checker": "2.0.10",
@@ -81,7 +79,6 @@
"@types/multer": "^2.0.0",
"@types/nodemailer": "7.0.1",
"@types/papaparse": "^5.5.2",
- "@types/pg": "8.20.0",
"@types/ping": "0.4.4",
"@types/swagger-ui-express": "4.1.8",
"@types/ws": "^8.18.1",
diff --git a/server/src/config/services.ts b/server/src/config/services.ts
index ed9f02a9f..c7e1a4469 100644
--- a/server/src/config/services.ts
+++ b/server/src/config/services.ts
@@ -30,6 +30,7 @@ import {
TeamsProvider,
TelegramProvider,
PushoverProvider,
+ TwilioProvider,
// Interfaces
INetworkService,
IEmailService,
@@ -99,19 +100,6 @@ import {
MongoTeamsRepository,
MongoMaintenanceWindowsRepository,
MongoSettingsRepository,
- TimescaleMonitorsRepository,
- TimescaleChecksRepository,
- TimescaleGeoChecksRepository,
- TimescaleMonitorStatsRepository,
- TimescaleStatusPagesRepository,
- TimescaleUsersRepository,
- TimescaleInvitesRepository,
- TimescaleRecoveryTokensRepository,
- TimescaleNotificationsRepository,
- TimescaleIncidentsRepository,
- TimescaleTeamsRepository,
- TimescaleMaintenanceWindowsRepository,
- TimescaleSettingsRepository,
IMonitorsRepository,
IChecksRepository,
IGeoChecksRepository,
@@ -127,7 +115,6 @@ import {
IMaintenanceWindowsRepository,
} from "@/repositories/index.js";
import { ILogger } from "@/utils/logger.js";
-import TimescaleDB from "@/db/TimescaleDB.js";
import { AppError } from "@/utils/AppError.js";
export type InitializedServices = {
@@ -184,8 +171,6 @@ export const initializeServices = async ({
if (dbType === "mongodb") {
db = new MongoDB(logger, envSettings);
- } else if (dbType === "timescaledb") {
- db = new TimescaleDB(logger, envSettings);
}
if (!db) {
@@ -225,23 +210,7 @@ export const initializeServices = async ({
teamsRepository = new MongoTeamsRepository();
maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
} else {
- const pool = db.getPool();
- if (!pool) {
- throw new Error("Failed to get database pool");
- }
- monitorsRepository = new TimescaleMonitorsRepository(pool);
- checksRepository = new TimescaleChecksRepository(pool);
- geoChecksRepository = new TimescaleGeoChecksRepository(pool);
- monitorStatsRepository = new TimescaleMonitorStatsRepository(pool);
- statusPagesRepository = new TimescaleStatusPagesRepository(pool);
- usersRepository = new TimescaleUsersRepository(pool);
- invitesRepository = new TimescaleInvitesRepository(pool);
- recoveryTokensRepository = new TimescaleRecoveryTokensRepository(pool);
- settingsRepository = new TimescaleSettingsRepository(pool);
- notificationsRepository = new TimescaleNotificationsRepository(pool);
- incidentsRepository = new TimescaleIncidentsRepository(pool);
- teamsRepository = new TimescaleTeamsRepository(pool);
- maintenanceWindowsRepository = new TimescaleMaintenanceWindowsRepository(pool);
+ throw new AppError({ message: "Unsupported database type", status: 500 });
}
// Inject settings repository into settings service (now that DB is connected)
@@ -300,6 +269,7 @@ export const initializeServices = async ({
const teamsProvider = new TeamsProvider(logger);
const telegramProvider = new TelegramProvider(logger);
const pushoverProvider = new PushoverProvider(logger);
+ const twilioProvider = new TwilioProvider(logger);
const notificationsService = new NotificationsService(
notificationsRepository,
@@ -313,6 +283,7 @@ export const initializeServices = async ({
teamsProvider,
telegramProvider,
pushoverProvider,
+ twilioProvider,
settingsService,
logger,
notificationMessageBuilder
diff --git a/server/src/db/IDb.ts b/server/src/db/IDb.ts
index e71e36cca..4c719999f 100644
--- a/server/src/db/IDb.ts
+++ b/server/src/db/IDb.ts
@@ -1,7 +1,4 @@
-import { Pool } from "pg";
-
export interface IDb {
connect(): Promise;
disconnect(): Promise;
- getPool(): Pool | null;
}
diff --git a/server/src/db/MongoDB.ts b/server/src/db/MongoDB.ts
index 59d2a32b7..1284aef4d 100755
--- a/server/src/db/MongoDB.ts
+++ b/server/src/db/MongoDB.ts
@@ -5,7 +5,6 @@ import { ILogger } from "@/utils/logger.js";
import { EnvConfig } from "@/service/system/settingsService.js";
const SERVICE_NAME = "MongoDB";
import { IDb } from "@/db/IDb.js";
-import { Pool } from "pg";
class MongoDB implements IDb {
static SERVICE_NAME = SERVICE_NAME;
@@ -79,10 +78,6 @@ class MongoDB implements IDb {
});
}
};
-
- getPool = () => {
- return null;
- };
}
export default MongoDB;
diff --git a/server/src/db/TimescaleDB.ts b/server/src/db/TimescaleDB.ts
deleted file mode 100644
index b36c54292..000000000
--- a/server/src/db/TimescaleDB.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import { IDb } from "@/db/IDb.js";
-import { ILogger } from "@/utils/logger.js";
-import { EnvConfig } from "@/service/system/settingsService.js";
-import { Client, Pool } from "pg";
-import { runTimescaleDBMigrations } from "@/db/migration/timescaledb/index.js";
-
-const SERVICE_NAME = "TimescaleDB";
-
-class TimescaleDB implements IDb {
- private pool: Pool | null = null;
-
- constructor(
- private logger: ILogger,
- private envSettings: EnvConfig
- ) {}
-
- private ensureDatabaseExists = async (connectionString: string) => {
- const url = new URL(connectionString);
- const dbName = url.pathname.slice(1); // Remove leading "/"
-
- // Connect to the default "postgres" database
- url.pathname = "/postgres";
- const client = new Client({ connectionString: url.toString() });
- await client.connect();
-
- const result = await client.query("SELECT 1 FROM pg_database WHERE datname = $1", [dbName]);
- if (result.rowCount === 0) {
- await client.query(`CREATE DATABASE "${dbName}"`);
- this.logger.info({
- message: `Created database "${dbName}"`,
- service: SERVICE_NAME,
- method: "ensureDatabaseExists",
- });
- }
- await client.end();
- };
-
- connect = async () => {
- try {
- const connectionString = this.envSettings.dbConnectionString || "postgresql://postgres:password@localhost:5432/checkmate";
-
- await this.ensureDatabaseExists(connectionString);
-
- this.pool = new Pool({ connectionString });
-
- const client = await this.pool.connect();
- client.release();
-
- // Ensure TimescaleDB extension is enabled
- await this.pool.query("CREATE EXTENSION IF NOT EXISTS timescaledb;");
-
- this.logger.info({
- message: "Connected to TimescaleDB",
- service: SERVICE_NAME,
- method: "connect",
- });
-
- await runTimescaleDBMigrations(this.pool, this.logger);
- } catch (error: unknown) {
- this.logger.error({
- message: error instanceof Error ? error.message : "Unknown error",
- service: SERVICE_NAME,
- method: "connect",
- stack: error instanceof Error ? error.stack : undefined,
- });
- throw error;
- }
- };
-
- disconnect = async () => {
- try {
- if (this.pool === null) {
- return;
- }
- this.logger.info({
- message: "Disconnecting from TimescaleDB",
- service: SERVICE_NAME,
- method: "disconnect",
- });
- await this.pool.end();
- this.pool = null;
- this.logger.info({
- message: "Disconnected from TimescaleDB",
- service: SERVICE_NAME,
- method: "disconnect",
- });
- } catch (error: unknown) {
- this.logger.error({
- message: error instanceof Error ? error.message : "Unknown error",
- service: SERVICE_NAME,
- method: "disconnect",
- stack: error instanceof Error ? error.stack : undefined,
- });
- }
- };
-
- getPool = (): Pool => {
- if (this.pool === null) {
- throw new Error("TimescaleDB is not connected. Call connect() first.");
- }
- return this.pool;
- };
-}
-
-export default TimescaleDB;
diff --git a/server/src/db/migration/timescaledb/0001_create_enums.ts b/server/src/db/migration/timescaledb/0001_create_enums.ts
deleted file mode 100644
index a16ec955b..000000000
--- a/server/src/db/migration/timescaledb/0001_create_enums.ts
+++ /dev/null
@@ -1,50 +0,0 @@
-import type { Pool } from "pg";
-
-export const createEnums = async (pool: Pool) => {
- await pool.query(`
- DO $$ BEGIN
- CREATE TYPE monitor_type AS ENUM ('http', 'ping', 'pagespeed', 'hardware', 'docker', 'port', 'game', 'grpc', 'websocket', 'unknown');
- EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-
- DO $$ BEGIN
- CREATE TYPE monitor_status AS ENUM ('up', 'down', 'paused', 'initializing', 'maintenance', 'breached');
- EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-
- DO $$ BEGIN
- CREATE TYPE notification_type AS ENUM ('email', 'slack', 'discord', 'webhook', 'pager_duty', 'matrix', 'teams');
- EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-
- DO $$ BEGIN
- CREATE TYPE user_role AS ENUM ('user', 'admin', 'superadmin', 'demo');
- EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-
- DO $$ BEGIN
- CREATE TYPE incident_resolution_type AS ENUM ('automatic', 'manual');
- EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-
- DO $$ BEGIN
- CREATE TYPE duration_unit AS ENUM ('seconds', 'minutes', 'hours', 'days');
- EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-
- DO $$ BEGIN
- CREATE TYPE status_page_type AS ENUM ('uptime', 'infrastructure');
- EXCEPTION WHEN duplicate_object THEN NULL; END $$;
-
- DO $$ BEGIN
- CREATE TYPE match_method AS ENUM ('equal', 'include', 'regex');
- EXCEPTION WHEN duplicate_object THEN NULL; END $$;
- `);
-};
-
-export const dropEnums = async (pool: Pool) => {
- await pool.query(`
- DROP TYPE IF EXISTS match_method;
- DROP TYPE IF EXISTS status_page_type;
- DROP TYPE IF EXISTS duration_unit;
- DROP TYPE IF EXISTS incident_resolution_type;
- DROP TYPE IF EXISTS user_role;
- DROP TYPE IF EXISTS notification_type;
- DROP TYPE IF EXISTS monitor_status;
- DROP TYPE IF EXISTS monitor_type;
- `);
-};
diff --git a/server/src/db/migration/timescaledb/0002_create_teams.ts b/server/src/db/migration/timescaledb/0002_create_teams.ts
deleted file mode 100644
index 905883eb2..000000000
--- a/server/src/db/migration/timescaledb/0002_create_teams.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import type { Pool } from "pg";
-
-export const createTeams = async (pool: Pool) => {
- await pool.query(`
- CREATE TABLE IF NOT EXISTS teams (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- email TEXT NOT NULL UNIQUE,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
- );
-
- CREATE INDEX IF NOT EXISTS idx_teams_email ON teams (email);
- `);
-};
-
-export const dropTeams = async (pool: Pool) => {
- await pool.query(`DROP TABLE IF EXISTS teams;`);
-};
diff --git a/server/src/db/migration/timescaledb/0003_create_users.ts b/server/src/db/migration/timescaledb/0003_create_users.ts
deleted file mode 100644
index 6c8f57027..000000000
--- a/server/src/db/migration/timescaledb/0003_create_users.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import type { Pool } from "pg";
-
-export const createUsers = async (pool: Pool) => {
- await pool.query(`
- CREATE TABLE IF NOT EXISTS users (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- team_id UUID REFERENCES teams(id) ON DELETE SET NULL,
- first_name TEXT NOT NULL,
- last_name TEXT NOT NULL,
- email TEXT NOT NULL UNIQUE,
- password TEXT NOT NULL,
- avatar_image TEXT,
- profile_image BYTEA,
- profile_image_content_type TEXT,
- is_active BOOLEAN NOT NULL DEFAULT TRUE,
- is_verified BOOLEAN NOT NULL DEFAULT FALSE,
- roles user_role[] NOT NULL DEFAULT ARRAY['user']::user_role[],
- check_ttl INTEGER,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
- );
-
- CREATE INDEX IF NOT EXISTS idx_users_team_id ON users (team_id);
- CREATE INDEX IF NOT EXISTS idx_users_email ON users (email);
- `);
-};
-
-export const dropUsers = async (pool: Pool) => {
- await pool.query(`DROP TABLE IF EXISTS users;`);
-};
diff --git a/server/src/db/migration/timescaledb/0004_create_monitors.ts b/server/src/db/migration/timescaledb/0004_create_monitors.ts
deleted file mode 100644
index a4829c7f5..000000000
--- a/server/src/db/migration/timescaledb/0004_create_monitors.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import type { Pool } from "pg";
-
-export const createMonitors = async (pool: Pool) => {
- await pool.query(`
- CREATE TABLE IF NOT EXISTS monitors (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- user_id UUID NOT NULL REFERENCES users(id),
- team_id UUID NOT NULL REFERENCES teams(id),
- name TEXT NOT NULL,
- description TEXT,
- type monitor_type NOT NULL,
- status monitor_status NOT NULL DEFAULT 'initializing',
-
- -- HTTP / Website monitoring
- url TEXT,
- port INTEGER,
- ignore_tls_errors BOOLEAN DEFAULT FALSE,
- use_advanced_matching BOOLEAN DEFAULT FALSE,
- json_path TEXT,
- expected_value TEXT,
- match_method match_method,
- secret TEXT,
-
- -- Scheduling
- interval_ms INTEGER NOT NULL DEFAULT 60000,
- is_active BOOLEAN NOT NULL DEFAULT TRUE,
-
- -- Uptime tracking
- status_window BOOLEAN[],
- status_window_size INTEGER DEFAULT 5,
- status_window_threshold INTEGER DEFAULT 60,
- uptime_percentage DOUBLE PRECISION,
-
- -- Infrastructure alert thresholds
- cpu_alert_threshold INTEGER,
- cpu_alert_counter INTEGER,
- memory_alert_threshold INTEGER,
- memory_alert_counter INTEGER,
- disk_alert_threshold INTEGER,
- disk_alert_counter INTEGER,
- temp_alert_threshold INTEGER,
- temp_alert_counter INTEGER,
- selected_disks TEXT[],
-
- -- Game monitoring
- game_id TEXT,
- grpc_service_name TEXT,
-
- -- Grouping
- monitor_group TEXT,
-
- -- Geo checks
- geo_check_enabled BOOLEAN DEFAULT FALSE,
- geo_check_locations TEXT[],
- geo_check_interval_ms INTEGER DEFAULT 300000,
-
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
- );
-
- CREATE INDEX IF NOT EXISTS idx_monitors_team_type ON monitors (team_id, type);
- CREATE INDEX IF NOT EXISTS idx_monitors_user_id ON monitors (user_id);
- CREATE INDEX IF NOT EXISTS idx_monitors_status ON monitors (status);
- `);
-};
-
-export const dropMonitors = async (pool: Pool) => {
- await pool.query(`DROP TABLE IF EXISTS monitors;`);
-};
diff --git a/server/src/db/migration/timescaledb/0005_create_notifications.ts b/server/src/db/migration/timescaledb/0005_create_notifications.ts
deleted file mode 100644
index 2a4a065fa..000000000
--- a/server/src/db/migration/timescaledb/0005_create_notifications.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-import type { Pool } from "pg";
-
-export const createNotifications = async (pool: Pool) => {
- await pool.query(`
- CREATE TABLE IF NOT EXISTS notifications (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- user_id UUID NOT NULL REFERENCES users(id),
- team_id UUID NOT NULL REFERENCES teams(id),
- type notification_type NOT NULL,
- notification_name TEXT,
- address TEXT,
- phone TEXT,
-
- -- Matrix-specific fields
- homeserver_url TEXT,
- room_id TEXT,
- access_token TEXT,
-
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
- );
-
- CREATE INDEX IF NOT EXISTS idx_notifications_team ON notifications (team_id);
- CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications (user_id);
- `);
-};
-
-export const dropNotifications = async (pool: Pool) => {
- await pool.query(`DROP TABLE IF EXISTS notifications;`);
-};
diff --git a/server/src/db/migration/timescaledb/0006_create_monitor_notifications.ts b/server/src/db/migration/timescaledb/0006_create_monitor_notifications.ts
deleted file mode 100644
index d2a306322..000000000
--- a/server/src/db/migration/timescaledb/0006_create_monitor_notifications.ts
+++ /dev/null
@@ -1,18 +0,0 @@
-import type { Pool } from "pg";
-
-export const createMonitorNotifications = async (pool: Pool) => {
- await pool.query(`
- CREATE TABLE IF NOT EXISTS monitor_notifications (
- monitor_id UUID NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
- notification_id UUID NOT NULL REFERENCES notifications(id) ON DELETE CASCADE,
- PRIMARY KEY (monitor_id, notification_id)
- );
-
- CREATE INDEX IF NOT EXISTS idx_monitor_notifications_notification
- ON monitor_notifications (notification_id);
- `);
-};
-
-export const dropMonitorNotifications = async (pool: Pool) => {
- await pool.query(`DROP TABLE IF EXISTS monitor_notifications;`);
-};
diff --git a/server/src/db/migration/timescaledb/0007_create_incidents.ts b/server/src/db/migration/timescaledb/0007_create_incidents.ts
deleted file mode 100644
index e1c990f95..000000000
--- a/server/src/db/migration/timescaledb/0007_create_incidents.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import type { Pool } from "pg";
-
-export const createIncidents = async (pool: Pool) => {
- await pool.query(`
- CREATE TABLE IF NOT EXISTS incidents (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- monitor_id UUID NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
- team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
- start_time TIMESTAMPTZ NOT NULL,
- end_time TIMESTAMPTZ,
- status BOOLEAN,
- message TEXT,
- status_code INTEGER,
- resolution_type incident_resolution_type,
- resolved_by UUID REFERENCES users(id) ON DELETE SET NULL,
- resolved_by_email TEXT,
- comment TEXT,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
- );
-
- CREATE INDEX IF NOT EXISTS idx_incidents_monitor_status ON incidents (monitor_id, status);
- CREATE INDEX IF NOT EXISTS idx_incidents_team_status ON incidents (team_id, status);
- CREATE INDEX IF NOT EXISTS idx_incidents_team_start ON incidents (team_id, start_time DESC);
- CREATE INDEX IF NOT EXISTS idx_incidents_status_start ON incidents (status, start_time DESC);
- CREATE INDEX IF NOT EXISTS idx_incidents_resolution ON incidents (resolution_type, status);
- CREATE INDEX IF NOT EXISTS idx_incidents_resolved_by ON incidents (resolved_by, status);
- CREATE INDEX IF NOT EXISTS idx_incidents_created ON incidents (created_at DESC);
- `);
-};
-
-export const dropIncidents = async (pool: Pool) => {
- await pool.query(`DROP TABLE IF EXISTS incidents;`);
-};
diff --git a/server/src/db/migration/timescaledb/0008_create_monitor_stats.ts b/server/src/db/migration/timescaledb/0008_create_monitor_stats.ts
deleted file mode 100644
index f08004873..000000000
--- a/server/src/db/migration/timescaledb/0008_create_monitor_stats.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { Pool } from "pg";
-
-export const createMonitorStats = async (pool: Pool) => {
- await pool.query(`
- CREATE TABLE IF NOT EXISTS monitor_stats (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- monitor_id UUID NOT NULL UNIQUE REFERENCES monitors(id) ON DELETE CASCADE,
- avg_response_time DOUBLE PRECISION DEFAULT 0,
- max_response_time DOUBLE PRECISION DEFAULT 0,
- total_checks BIGINT DEFAULT 0,
- total_up_checks BIGINT DEFAULT 0,
- total_down_checks BIGINT DEFAULT 0,
- uptime_percentage DOUBLE PRECISION DEFAULT 0,
- last_check_timestamp TIMESTAMPTZ,
- last_response_time DOUBLE PRECISION DEFAULT 0,
- time_of_last_failure TIMESTAMPTZ,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
- );
- `);
-};
-
-export const dropMonitorStats = async (pool: Pool) => {
- await pool.query(`DROP TABLE IF EXISTS monitor_stats;`);
-};
diff --git a/server/src/db/migration/timescaledb/0009_create_checks.ts b/server/src/db/migration/timescaledb/0009_create_checks.ts
deleted file mode 100644
index 27753f524..000000000
--- a/server/src/db/migration/timescaledb/0009_create_checks.ts
+++ /dev/null
@@ -1,105 +0,0 @@
-import type { Pool } from "pg";
-
-export const createChecks = async (pool: Pool) => {
- await pool.query(`
- CREATE TABLE IF NOT EXISTS checks (
- id UUID NOT NULL DEFAULT gen_random_uuid(),
- monitor_id UUID NOT NULL,
- team_id UUID NOT NULL,
- monitor_type monitor_type NOT NULL,
- status BOOLEAN,
- response_time INTEGER,
- status_code INTEGER,
- message TEXT,
-
- -- HTTP request timings
- timing_start DOUBLE PRECISION,
- timing_socket DOUBLE PRECISION,
- timing_lookup DOUBLE PRECISION,
- timing_connect DOUBLE PRECISION,
- timing_secure_connect DOUBLE PRECISION,
- timing_upload DOUBLE PRECISION,
- timing_response DOUBLE PRECISION,
- timing_end DOUBLE PRECISION,
- phase_wait DOUBLE PRECISION,
- phase_dns DOUBLE PRECISION,
- phase_tcp DOUBLE PRECISION,
- phase_tls DOUBLE PRECISION,
- phase_request DOUBLE PRECISION,
- phase_first_byte DOUBLE PRECISION,
- phase_download DOUBLE PRECISION,
- phase_total DOUBLE PRECISION,
-
- -- CPU metrics
- cpu_physical_core INTEGER,
- cpu_logical_core INTEGER,
- cpu_frequency DOUBLE PRECISION,
- cpu_current_frequency DOUBLE PRECISION,
- cpu_temperature DOUBLE PRECISION[],
- cpu_free_percent DOUBLE PRECISION,
- cpu_usage_percent DOUBLE PRECISION,
-
- -- Memory metrics
- mem_total_bytes BIGINT,
- mem_available_bytes BIGINT,
- mem_used_bytes BIGINT,
- mem_usage_percent DOUBLE PRECISION,
-
- -- Host info
- host_os TEXT,
- host_platform TEXT,
- host_kernel_version TEXT,
- host_pretty_name TEXT,
-
- -- Capture agent info
- capture_version TEXT,
- capture_mode TEXT,
-
- -- Lighthouse / PageSpeed scores
- lighthouse_performance DOUBLE PRECISION,
- lighthouse_accessibility DOUBLE PRECISION,
- lighthouse_best_practices DOUBLE PRECISION,
- lighthouse_seo DOUBLE PRECISION,
-
- -- Lighthouse audit details
- audit_cls_score DOUBLE PRECISION,
- audit_cls_value DOUBLE PRECISION,
- audit_cls_display TEXT,
- audit_si_score DOUBLE PRECISION,
- audit_si_value DOUBLE PRECISION,
- audit_si_display TEXT,
- audit_fcp_score DOUBLE PRECISION,
- audit_fcp_value DOUBLE PRECISION,
- audit_fcp_display TEXT,
- audit_lcp_score DOUBLE PRECISION,
- audit_lcp_value DOUBLE PRECISION,
- audit_lcp_display TEXT,
- audit_tbt_score DOUBLE PRECISION,
- audit_tbt_value DOUBLE PRECISION,
- audit_tbt_display TEXT,
-
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
- );
-
- SELECT create_hypertable('checks', 'created_at',
- chunk_time_interval => INTERVAL '7 days',
- if_not_exists => TRUE
- );
-
- CREATE INDEX IF NOT EXISTS idx_checks_monitor_time
- ON checks (monitor_id, created_at DESC);
- CREATE INDEX IF NOT EXISTS idx_checks_team_time
- ON checks (team_id, created_at DESC);
- CREATE INDEX IF NOT EXISTS idx_checks_monitor_type_time
- ON checks (monitor_id, monitor_type, created_at DESC);
- CREATE INDEX IF NOT EXISTS idx_checks_team_status_time
- ON checks (team_id, status, created_at DESC);
- CREATE INDEX IF NOT EXISTS idx_checks_status_code
- ON checks (status_code, created_at DESC);
- `);
-};
-
-export const dropChecks = async (pool: Pool) => {
- await pool.query(`DROP TABLE IF EXISTS checks;`);
-};
diff --git a/server/src/db/migration/timescaledb/0010_create_check_disks.ts b/server/src/db/migration/timescaledb/0010_create_check_disks.ts
deleted file mode 100644
index e77784d39..000000000
--- a/server/src/db/migration/timescaledb/0010_create_check_disks.ts
+++ /dev/null
@@ -1,37 +0,0 @@
-import type { Pool } from "pg";
-
-export const createCheckDisks = async (pool: Pool) => {
- await pool.query(`
- CREATE TABLE IF NOT EXISTS check_disks (
- id UUID NOT NULL DEFAULT gen_random_uuid(),
- check_id UUID NOT NULL,
- check_created_at TIMESTAMPTZ NOT NULL,
- device TEXT,
- mountpoint TEXT,
- total_bytes BIGINT,
- free_bytes BIGINT,
- used_bytes BIGINT,
- usage_percent DOUBLE PRECISION,
- total_inodes BIGINT,
- free_inodes BIGINT,
- used_inodes BIGINT,
- inodes_usage_percent DOUBLE PRECISION,
- read_bytes BIGINT,
- write_bytes BIGINT,
- read_time BIGINT,
- write_time BIGINT
- );
-
- SELECT create_hypertable('check_disks', 'check_created_at',
- chunk_time_interval => INTERVAL '7 days',
- if_not_exists => TRUE
- );
-
- CREATE INDEX IF NOT EXISTS idx_check_disks_check
- ON check_disks (check_id, check_created_at);
- `);
-};
-
-export const dropCheckDisks = async (pool: Pool) => {
- await pool.query(`DROP TABLE IF EXISTS check_disks;`);
-};
diff --git a/server/src/db/migration/timescaledb/0011_create_check_network_interfaces.ts b/server/src/db/migration/timescaledb/0011_create_check_network_interfaces.ts
deleted file mode 100644
index 4988fe58e..000000000
--- a/server/src/db/migration/timescaledb/0011_create_check_network_interfaces.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-import type { Pool } from "pg";
-
-export const createCheckNetworkInterfaces = async (pool: Pool) => {
- await pool.query(`
- CREATE TABLE IF NOT EXISTS check_network_interfaces (
- id UUID NOT NULL DEFAULT gen_random_uuid(),
- check_id UUID NOT NULL,
- check_created_at TIMESTAMPTZ NOT NULL,
- name TEXT,
- bytes_sent BIGINT,
- bytes_recv BIGINT,
- packets_sent BIGINT,
- packets_recv BIGINT,
- err_in BIGINT,
- err_out BIGINT,
- drop_in BIGINT,
- drop_out BIGINT,
- fifo_in BIGINT,
- fifo_out BIGINT
- );
-
- SELECT create_hypertable('check_network_interfaces', 'check_created_at',
- chunk_time_interval => INTERVAL '7 days',
- if_not_exists => TRUE
- );
-
- CREATE INDEX IF NOT EXISTS idx_check_net_check
- ON check_network_interfaces (check_id, check_created_at);
- `);
-};
-
-export const dropCheckNetworkInterfaces = async (pool: Pool) => {
- await pool.query(`DROP TABLE IF EXISTS check_network_interfaces;`);
-};
diff --git a/server/src/db/migration/timescaledb/0012_create_check_errors.ts b/server/src/db/migration/timescaledb/0012_create_check_errors.ts
deleted file mode 100644
index c84bacac8..000000000
--- a/server/src/db/migration/timescaledb/0012_create_check_errors.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import type { Pool } from "pg";
-
-export const createCheckErrors = async (pool: Pool) => {
- await pool.query(`
- CREATE TABLE IF NOT EXISTS check_errors (
- id UUID NOT NULL DEFAULT gen_random_uuid(),
- check_id UUID NOT NULL,
- check_created_at TIMESTAMPTZ NOT NULL,
- metrics TEXT[],
- error TEXT
- );
-
- SELECT create_hypertable('check_errors', 'check_created_at',
- chunk_time_interval => INTERVAL '7 days',
- if_not_exists => TRUE
- );
-
- CREATE INDEX IF NOT EXISTS idx_check_errors_check
- ON check_errors (check_id, check_created_at);
- `);
-};
-
-export const dropCheckErrors = async (pool: Pool) => {
- await pool.query(`DROP TABLE IF EXISTS check_errors;`);
-};
diff --git a/server/src/db/migration/timescaledb/0013_create_geo_checks.ts b/server/src/db/migration/timescaledb/0013_create_geo_checks.ts
deleted file mode 100644
index a6ee03061..000000000
--- a/server/src/db/migration/timescaledb/0013_create_geo_checks.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import type { Pool } from "pg";
-
-export const createGeoChecks = async (pool: Pool) => {
- await pool.query(`
- CREATE TABLE IF NOT EXISTS geo_checks (
- id UUID NOT NULL DEFAULT gen_random_uuid(),
- monitor_id UUID NOT NULL,
- team_id UUID NOT NULL,
- monitor_type monitor_type NOT NULL,
- expiry TIMESTAMPTZ,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
- );
-
- SELECT create_hypertable('geo_checks', 'created_at',
- chunk_time_interval => INTERVAL '7 days',
- if_not_exists => TRUE
- );
-
- CREATE INDEX IF NOT EXISTS idx_geo_checks_monitor_time
- ON geo_checks (monitor_id, created_at DESC);
- CREATE INDEX IF NOT EXISTS idx_geo_checks_team_time
- ON geo_checks (team_id, created_at DESC);
- `);
-};
-
-export const dropGeoChecks = async (pool: Pool) => {
- await pool.query(`DROP TABLE IF EXISTS geo_checks;`);
-};
diff --git a/server/src/db/migration/timescaledb/0014_create_geo_check_results.ts b/server/src/db/migration/timescaledb/0014_create_geo_check_results.ts
deleted file mode 100644
index 4e0e45e1a..000000000
--- a/server/src/db/migration/timescaledb/0014_create_geo_check_results.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-import type { Pool } from "pg";
-
-export const createGeoCheckResults = async (pool: Pool) => {
- await pool.query(`
- CREATE TABLE IF NOT EXISTS geo_check_results (
- id UUID NOT NULL DEFAULT gen_random_uuid(),
- geo_check_id UUID NOT NULL,
- geo_check_created_at TIMESTAMPTZ NOT NULL,
-
- -- Status
- status BOOLEAN,
- status_code INTEGER,
-
- -- Location
- location_continent TEXT,
- location_region TEXT,
- location_country TEXT,
- location_state TEXT,
- location_city TEXT,
- location_longitude DOUBLE PRECISION,
- location_latitude DOUBLE PRECISION,
-
- -- Timings
- timing_total DOUBLE PRECISION,
- timing_dns DOUBLE PRECISION,
- timing_tcp DOUBLE PRECISION,
- timing_tls DOUBLE PRECISION,
- timing_first_byte DOUBLE PRECISION,
- timing_download DOUBLE PRECISION
- );
-
- SELECT create_hypertable('geo_check_results', 'geo_check_created_at',
- chunk_time_interval => INTERVAL '7 days',
- if_not_exists => TRUE
- );
-
- CREATE INDEX IF NOT EXISTS idx_geo_results_check
- ON geo_check_results (geo_check_id, geo_check_created_at);
- CREATE INDEX IF NOT EXISTS idx_geo_results_country
- ON geo_check_results (location_country, geo_check_created_at DESC);
- `);
-};
-
-export const dropGeoCheckResults = async (pool: Pool) => {
- await pool.query(`DROP TABLE IF EXISTS geo_check_results;`);
-};
diff --git a/server/src/db/migration/timescaledb/0015_create_invites.ts b/server/src/db/migration/timescaledb/0015_create_invites.ts
deleted file mode 100644
index 80aca5255..000000000
--- a/server/src/db/migration/timescaledb/0015_create_invites.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import type { Pool } from "pg";
-
-export const createInvites = async (pool: Pool) => {
- await pool.query(`
- CREATE TABLE IF NOT EXISTS invites (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- email TEXT NOT NULL UNIQUE,
- team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
- roles user_role[] NOT NULL DEFAULT ARRAY['user']::user_role[],
- token TEXT NOT NULL,
- expiry TIMESTAMPTZ,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
- );
-
- CREATE INDEX IF NOT EXISTS idx_invites_expiry ON invites (expiry);
- `);
-};
-
-export const dropInvites = async (pool: Pool) => {
- await pool.query(`DROP TABLE IF EXISTS invites;`);
-};
diff --git a/server/src/db/migration/timescaledb/0016_create_recovery_tokens.ts b/server/src/db/migration/timescaledb/0016_create_recovery_tokens.ts
deleted file mode 100644
index 36a9d0de3..000000000
--- a/server/src/db/migration/timescaledb/0016_create_recovery_tokens.ts
+++ /dev/null
@@ -1,20 +0,0 @@
-import type { Pool } from "pg";
-
-export const createRecoveryTokens = async (pool: Pool) => {
- await pool.query(`
- CREATE TABLE IF NOT EXISTS recovery_tokens (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- email TEXT NOT NULL UNIQUE,
- token TEXT NOT NULL,
- expiry TIMESTAMPTZ,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
- );
-
- CREATE INDEX IF NOT EXISTS idx_recovery_expiry ON recovery_tokens (expiry);
- `);
-};
-
-export const dropRecoveryTokens = async (pool: Pool) => {
- await pool.query(`DROP TABLE IF EXISTS recovery_tokens;`);
-};
diff --git a/server/src/db/migration/timescaledb/0017_create_maintenance_windows.ts b/server/src/db/migration/timescaledb/0017_create_maintenance_windows.ts
deleted file mode 100644
index 190769261..000000000
--- a/server/src/db/migration/timescaledb/0017_create_maintenance_windows.ts
+++ /dev/null
@@ -1,29 +0,0 @@
-import type { Pool } from "pg";
-
-export const createMaintenanceWindows = async (pool: Pool) => {
- await pool.query(`
- CREATE TABLE IF NOT EXISTS maintenance_windows (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- monitor_id UUID NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
- team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
- active BOOLEAN NOT NULL DEFAULT TRUE,
- name TEXT,
- duration INTEGER,
- duration_unit duration_unit,
- repeat INTEGER,
- start_time TIMESTAMPTZ,
- end_time TIMESTAMPTZ,
- expiry TIMESTAMPTZ,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
- );
-
- CREATE INDEX IF NOT EXISTS idx_maint_monitor ON maintenance_windows (monitor_id);
- CREATE INDEX IF NOT EXISTS idx_maint_team ON maintenance_windows (team_id);
- CREATE INDEX IF NOT EXISTS idx_maint_expiry ON maintenance_windows (expiry);
- `);
-};
-
-export const dropMaintenanceWindows = async (pool: Pool) => {
- await pool.query(`DROP TABLE IF EXISTS maintenance_windows;`);
-};
diff --git a/server/src/db/migration/timescaledb/0018_create_status_pages.ts b/server/src/db/migration/timescaledb/0018_create_status_pages.ts
deleted file mode 100644
index 6037f6852..000000000
--- a/server/src/db/migration/timescaledb/0018_create_status_pages.ts
+++ /dev/null
@@ -1,51 +0,0 @@
-import type { Pool } from "pg";
-
-export const createStatusPages = async (pool: Pool) => {
- await pool.query(`
- CREATE TABLE IF NOT EXISTS status_pages (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- user_id UUID NOT NULL REFERENCES users(id),
- team_id UUID NOT NULL REFERENCES teams(id),
- types status_page_type[] NOT NULL DEFAULT ARRAY['uptime']::status_page_type[],
- company_name TEXT,
- url TEXT NOT NULL UNIQUE,
- timezone TEXT,
- color TEXT DEFAULT '#4169E1',
- logo_data BYTEA,
- logo_content_type TEXT,
- is_published BOOLEAN DEFAULT FALSE,
- show_charts BOOLEAN DEFAULT FALSE,
- show_uptime_percentage BOOLEAN DEFAULT FALSE,
- show_admin_login_link BOOLEAN DEFAULT FALSE,
- show_infrastructure BOOLEAN DEFAULT FALSE,
- custom_css TEXT,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
- );
-
- CREATE INDEX IF NOT EXISTS idx_status_pages_team ON status_pages (team_id);
- CREATE INDEX IF NOT EXISTS idx_status_pages_url ON status_pages (url);
-
- CREATE TABLE IF NOT EXISTS status_page_monitors (
- status_page_id UUID NOT NULL REFERENCES status_pages(id) ON DELETE CASCADE,
- monitor_id UUID NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
- sort_order INTEGER DEFAULT 0,
- PRIMARY KEY (status_page_id, monitor_id)
- );
-
- CREATE TABLE IF NOT EXISTS status_page_sub_monitors (
- status_page_id UUID NOT NULL REFERENCES status_pages(id) ON DELETE CASCADE,
- monitor_id UUID NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
- sort_order INTEGER DEFAULT 0,
- PRIMARY KEY (status_page_id, monitor_id)
- );
- `);
-};
-
-export const dropStatusPages = async (pool: Pool) => {
- await pool.query(`
- DROP TABLE IF EXISTS status_page_sub_monitors;
- DROP TABLE IF EXISTS status_page_monitors;
- DROP TABLE IF EXISTS status_pages;
- `);
-};
diff --git a/server/src/db/migration/timescaledb/0019_create_app_settings.ts b/server/src/db/migration/timescaledb/0019_create_app_settings.ts
deleted file mode 100644
index d2d624d07..000000000
--- a/server/src/db/migration/timescaledb/0019_create_app_settings.ts
+++ /dev/null
@@ -1,62 +0,0 @@
-import type { Pool } from "pg";
-
-export const createAppSettings = async (pool: Pool) => {
- await pool.query(`
- CREATE TABLE IF NOT EXISTS app_settings (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- check_ttl INTEGER DEFAULT 30,
- language TEXT,
- jwt_secret TEXT,
- pagespeed_api_key TEXT,
-
- -- SMTP / Email settings
- system_email_host TEXT,
- system_email_port INTEGER,
- system_email_address TEXT,
- system_email_password TEXT,
- system_email_user TEXT,
- system_email_connection_host TEXT,
- system_email_tls_servername TEXT,
- system_email_secure BOOLEAN,
- system_email_pool BOOLEAN,
- system_email_ignore_tls BOOLEAN,
- system_email_require_tls BOOLEAN,
- system_email_reject_unauthorized BOOLEAN,
-
- show_url BOOLEAN,
- version TEXT,
-
- -- Global infrastructure thresholds
- threshold_cpu_usage INTEGER,
- threshold_memory_usage INTEGER,
- threshold_disk_usage INTEGER,
- threshold_temperature INTEGER,
-
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
- );
-
- -- Enforce singleton: only one row allowed
- CREATE OR REPLACE FUNCTION enforce_singleton_app_settings()
- RETURNS TRIGGER AS $$
- BEGIN
- IF (SELECT COUNT(*) FROM app_settings) >= 1 THEN
- RAISE EXCEPTION 'app_settings allows only one row';
- END IF;
- RETURN NEW;
- END;
- $$ LANGUAGE plpgsql;
-
- DROP TRIGGER IF EXISTS trg_singleton_app_settings ON app_settings;
- CREATE TRIGGER trg_singleton_app_settings
- BEFORE INSERT ON app_settings
- FOR EACH ROW EXECUTE FUNCTION enforce_singleton_app_settings();
- `);
-};
-
-export const dropAppSettings = async (pool: Pool) => {
- await pool.query(`
- DROP TABLE IF EXISTS app_settings;
- DROP FUNCTION IF EXISTS enforce_singleton_app_settings();
- `);
-};
diff --git a/server/src/db/migration/timescaledb/0020_create_continuous_aggregates.ts b/server/src/db/migration/timescaledb/0020_create_continuous_aggregates.ts
deleted file mode 100644
index d435ffdf9..000000000
--- a/server/src/db/migration/timescaledb/0020_create_continuous_aggregates.ts
+++ /dev/null
@@ -1,127 +0,0 @@
-import type { Pool } from "pg";
-
-export const createContinuousAggregates = async (pool: Pool) => {
- // Uptime & Response Time (Hourly)
- await pool.query(`
- CREATE MATERIALIZED VIEW IF NOT EXISTS checks_hourly
- WITH (timescaledb.continuous) AS
- SELECT
- monitor_id,
- time_bucket('1 hour', created_at) AS bucket,
- COUNT(*) AS total_checks,
- COUNT(*) FILTER (WHERE status = TRUE) AS up_checks,
- COUNT(*) FILTER (WHERE status = FALSE) AS down_checks,
- AVG(response_time) AS avg_response_time,
- AVG(response_time) FILTER (WHERE status = TRUE) AS avg_up_response_time,
- AVG(response_time) FILTER (WHERE status = FALSE) AS avg_down_response_time,
- MAX(response_time) AS max_response_time,
- MIN(response_time) AS min_response_time
- FROM checks
- GROUP BY monitor_id, bucket
- WITH NO DATA;
- `);
- await pool.query(`ALTER MATERIALIZED VIEW checks_hourly SET (timescaledb.materialized_only = false);`);
- await pool.query(`
- SELECT add_continuous_aggregate_policy('checks_hourly',
- start_offset => INTERVAL '3 hours',
- end_offset => INTERVAL '1 hour',
- schedule_interval => INTERVAL '1 hour',
- if_not_exists => TRUE
- );
- `);
- await pool.query(`CALL refresh_continuous_aggregate('checks_hourly', NULL, NULL);`);
-
- // Uptime & Response Time (Daily)
- await pool.query(`
- CREATE MATERIALIZED VIEW IF NOT EXISTS checks_daily
- WITH (timescaledb.continuous) AS
- SELECT
- monitor_id,
- time_bucket('1 day', created_at) AS bucket,
- COUNT(*) AS total_checks,
- COUNT(*) FILTER (WHERE status = TRUE) AS up_checks,
- COUNT(*) FILTER (WHERE status = FALSE) AS down_checks,
- AVG(response_time) AS avg_response_time,
- AVG(response_time) FILTER (WHERE status = TRUE) AS avg_up_response_time,
- AVG(response_time) FILTER (WHERE status = FALSE) AS avg_down_response_time,
- MAX(response_time) AS max_response_time
- FROM checks
- GROUP BY monitor_id, bucket
- WITH NO DATA;
- `);
- await pool.query(`ALTER MATERIALIZED VIEW checks_daily SET (timescaledb.materialized_only = false);`);
- await pool.query(`
- SELECT add_continuous_aggregate_policy('checks_daily',
- start_offset => INTERVAL '3 days',
- end_offset => INTERVAL '1 day',
- schedule_interval => INTERVAL '1 day',
- if_not_exists => TRUE
- );
- `);
- await pool.query(`CALL refresh_continuous_aggregate('checks_daily', NULL, NULL);`);
-
- // Infrastructure Metrics (Hourly)
- await pool.query(`
- CREATE MATERIALIZED VIEW IF NOT EXISTS hardware_hourly
- WITH (timescaledb.continuous) AS
- SELECT
- monitor_id,
- time_bucket('1 hour', created_at) AS bucket,
- AVG(cpu_usage_percent) AS avg_cpu,
- MAX(cpu_usage_percent) AS max_cpu,
- AVG(mem_usage_percent) AS avg_memory,
- MAX(mem_usage_percent) AS max_memory,
- COUNT(*) AS sample_count
- FROM checks
- WHERE monitor_type = 'hardware'
- GROUP BY monitor_id, bucket
- WITH NO DATA;
- `);
- await pool.query(`ALTER MATERIALIZED VIEW hardware_hourly SET (timescaledb.materialized_only = false);`);
- await pool.query(`
- SELECT add_continuous_aggregate_policy('hardware_hourly',
- start_offset => INTERVAL '3 hours',
- end_offset => INTERVAL '1 hour',
- schedule_interval => INTERVAL '1 hour',
- if_not_exists => TRUE
- );
- `);
- await pool.query(`CALL refresh_continuous_aggregate('hardware_hourly', NULL, NULL);`);
-
- // PageSpeed Scores (Daily)
- await pool.query(`
- CREATE MATERIALIZED VIEW IF NOT EXISTS pagespeed_daily
- WITH (timescaledb.continuous) AS
- SELECT
- monitor_id,
- time_bucket('1 day', created_at) AS bucket,
- AVG(lighthouse_performance) AS avg_performance,
- AVG(lighthouse_accessibility) AS avg_accessibility,
- AVG(lighthouse_best_practices) AS avg_best_practices,
- AVG(lighthouse_seo) AS avg_seo,
- COUNT(*) AS sample_count
- FROM checks
- WHERE monitor_type = 'pagespeed'
- GROUP BY monitor_id, bucket
- WITH NO DATA;
- `);
- await pool.query(`ALTER MATERIALIZED VIEW pagespeed_daily SET (timescaledb.materialized_only = false);`);
- await pool.query(`
- SELECT add_continuous_aggregate_policy('pagespeed_daily',
- start_offset => INTERVAL '3 days',
- end_offset => INTERVAL '1 day',
- schedule_interval => INTERVAL '1 day',
- if_not_exists => TRUE
- );
- `);
- await pool.query(`CALL refresh_continuous_aggregate('pagespeed_daily', NULL, NULL);`);
-};
-
-export const dropContinuousAggregates = async (pool: Pool) => {
- await pool.query(`
- DROP MATERIALIZED VIEW IF EXISTS pagespeed_daily CASCADE;
- DROP MATERIALIZED VIEW IF EXISTS hardware_hourly CASCADE;
- DROP MATERIALIZED VIEW IF EXISTS checks_daily CASCADE;
- DROP MATERIALIZED VIEW IF EXISTS checks_hourly CASCADE;
- `);
-};
diff --git a/server/src/db/migration/timescaledb/0021_create_retention_compression.ts b/server/src/db/migration/timescaledb/0021_create_retention_compression.ts
deleted file mode 100644
index 95230e248..000000000
--- a/server/src/db/migration/timescaledb/0021_create_retention_compression.ts
+++ /dev/null
@@ -1,135 +0,0 @@
-import type { Pool } from "pg";
-
-export const createRetentionCompression = async (pool: Pool) => {
- // -- Compression policies --
-
- // Checks
- await pool.query(`
- ALTER TABLE checks SET (
- timescaledb.compress,
- timescaledb.compress_segmentby = 'monitor_id',
- timescaledb.compress_orderby = 'created_at DESC'
- );
- SELECT add_compression_policy('checks', INTERVAL '2 days', if_not_exists => TRUE);
- `);
-
- // Check child tables
- await pool.query(`
- ALTER TABLE check_disks SET (
- timescaledb.compress,
- timescaledb.compress_segmentby = 'check_id',
- timescaledb.compress_orderby = 'check_created_at DESC'
- );
- SELECT add_compression_policy('check_disks', INTERVAL '2 days', if_not_exists => TRUE);
- `);
-
- await pool.query(`
- ALTER TABLE check_network_interfaces SET (
- timescaledb.compress,
- timescaledb.compress_segmentby = 'check_id',
- timescaledb.compress_orderby = 'check_created_at DESC'
- );
- SELECT add_compression_policy('check_network_interfaces', INTERVAL '2 days', if_not_exists => TRUE);
- `);
-
- await pool.query(`
- ALTER TABLE check_errors SET (
- timescaledb.compress,
- timescaledb.compress_segmentby = 'check_id',
- timescaledb.compress_orderby = 'check_created_at DESC'
- );
- SELECT add_compression_policy('check_errors', INTERVAL '2 days', if_not_exists => TRUE);
- `);
-
- // Geo checks
- await pool.query(`
- ALTER TABLE geo_checks SET (
- timescaledb.compress,
- timescaledb.compress_segmentby = 'monitor_id',
- timescaledb.compress_orderby = 'created_at DESC'
- );
- SELECT add_compression_policy('geo_checks', INTERVAL '2 days', if_not_exists => TRUE);
- `);
-
- await pool.query(`
- ALTER TABLE geo_check_results SET (
- timescaledb.compress,
- timescaledb.compress_segmentby = 'geo_check_id',
- timescaledb.compress_orderby = 'geo_check_created_at DESC'
- );
- SELECT add_compression_policy('geo_check_results', INTERVAL '2 days', if_not_exists => TRUE);
- `);
-
- // -- Retention policies (default 30 days) --
-
- await pool.query(`
- SELECT add_retention_policy('checks', INTERVAL '30 days', if_not_exists => TRUE);
- SELECT add_retention_policy('check_disks', INTERVAL '30 days', if_not_exists => TRUE);
- SELECT add_retention_policy('check_network_interfaces', INTERVAL '30 days', if_not_exists => TRUE);
- SELECT add_retention_policy('check_errors', INTERVAL '30 days', if_not_exists => TRUE);
- SELECT add_retention_policy('geo_checks', INTERVAL '30 days', if_not_exists => TRUE);
- SELECT add_retention_policy('geo_check_results', INTERVAL '30 days', if_not_exists => TRUE);
- `);
-
- // -- Dynamic retention helper function --
-
- await pool.query(`
- CREATE OR REPLACE FUNCTION update_check_retention(new_ttl_days INTEGER)
- RETURNS VOID AS $$
- DECLARE
- ttl_interval INTERVAL;
- BEGIN
- ttl_interval := (new_ttl_days || ' days')::INTERVAL;
-
- PERFORM remove_retention_policy('checks', if_exists => true);
- PERFORM remove_retention_policy('check_disks', if_exists => true);
- PERFORM remove_retention_policy('check_network_interfaces', if_exists => true);
- PERFORM remove_retention_policy('check_errors', if_exists => true);
- PERFORM remove_retention_policy('geo_checks', if_exists => true);
- PERFORM remove_retention_policy('geo_check_results', if_exists => true);
-
- PERFORM add_retention_policy('checks', ttl_interval);
- PERFORM add_retention_policy('check_disks', ttl_interval);
- PERFORM add_retention_policy('check_network_interfaces', ttl_interval);
- PERFORM add_retention_policy('check_errors', ttl_interval);
- PERFORM add_retention_policy('geo_checks', ttl_interval);
- PERFORM add_retention_policy('geo_check_results', ttl_interval);
- END;
- $$ LANGUAGE plpgsql;
- `);
-};
-
-export const dropRetentionCompression = async (pool: Pool) => {
- // Remove retention policies
- await pool.query(`
- SELECT remove_retention_policy('checks', if_exists => true);
- SELECT remove_retention_policy('check_disks', if_exists => true);
- SELECT remove_retention_policy('check_network_interfaces', if_exists => true);
- SELECT remove_retention_policy('check_errors', if_exists => true);
- SELECT remove_retention_policy('geo_checks', if_exists => true);
- SELECT remove_retention_policy('geo_check_results', if_exists => true);
- `);
-
- // Remove compression policies
- await pool.query(`
- SELECT remove_compression_policy('checks', if_exists => true);
- SELECT remove_compression_policy('check_disks', if_exists => true);
- SELECT remove_compression_policy('check_network_interfaces', if_exists => true);
- SELECT remove_compression_policy('check_errors', if_exists => true);
- SELECT remove_compression_policy('geo_checks', if_exists => true);
- SELECT remove_compression_policy('geo_check_results', if_exists => true);
- `);
-
- // Disable compression on tables
- await pool.query(`
- ALTER TABLE checks SET (timescaledb.compress = false);
- ALTER TABLE check_disks SET (timescaledb.compress = false);
- ALTER TABLE check_network_interfaces SET (timescaledb.compress = false);
- ALTER TABLE check_errors SET (timescaledb.compress = false);
- ALTER TABLE geo_checks SET (timescaledb.compress = false);
- ALTER TABLE geo_check_results SET (timescaledb.compress = false);
- `);
-
- // Drop dynamic retention function
- await pool.query(`DROP FUNCTION IF EXISTS update_check_retention(INTEGER);`);
-};
diff --git a/server/src/db/migration/timescaledb/index.ts b/server/src/db/migration/timescaledb/index.ts
deleted file mode 100644
index 14084ebf3..000000000
--- a/server/src/db/migration/timescaledb/index.ts
+++ /dev/null
@@ -1,148 +0,0 @@
-import type { Pool } from "pg";
-import type { ILogger } from "@/utils/logger.js";
-import { createEnums, dropEnums } from "./0001_create_enums.js";
-import { createTeams, dropTeams } from "./0002_create_teams.js";
-import { createUsers, dropUsers } from "./0003_create_users.js";
-import { createMonitors, dropMonitors } from "./0004_create_monitors.js";
-import { createNotifications, dropNotifications } from "./0005_create_notifications.js";
-import { createMonitorNotifications, dropMonitorNotifications } from "./0006_create_monitor_notifications.js";
-import { createIncidents, dropIncidents } from "./0007_create_incidents.js";
-import { createMonitorStats, dropMonitorStats } from "./0008_create_monitor_stats.js";
-import { createChecks, dropChecks } from "./0009_create_checks.js";
-import { createCheckDisks, dropCheckDisks } from "./0010_create_check_disks.js";
-import { createCheckNetworkInterfaces, dropCheckNetworkInterfaces } from "./0011_create_check_network_interfaces.js";
-import { createCheckErrors, dropCheckErrors } from "./0012_create_check_errors.js";
-import { createGeoChecks, dropGeoChecks } from "./0013_create_geo_checks.js";
-import { createGeoCheckResults, dropGeoCheckResults } from "./0014_create_geo_check_results.js";
-import { createInvites, dropInvites } from "./0015_create_invites.js";
-import { createRecoveryTokens, dropRecoveryTokens } from "./0016_create_recovery_tokens.js";
-import { createMaintenanceWindows, dropMaintenanceWindows } from "./0017_create_maintenance_windows.js";
-import { createStatusPages, dropStatusPages } from "./0018_create_status_pages.js";
-import { createAppSettings, dropAppSettings } from "./0019_create_app_settings.js";
-import { createContinuousAggregates, dropContinuousAggregates } from "./0020_create_continuous_aggregates.js";
-import { createRetentionCompression, dropRetentionCompression } from "./0021_create_retention_compression.js";
-
-const SERVICE_NAME = "TimescaleDB Migrations";
-
-type MigrationEntry = {
- name: string;
- up: (pool: Pool) => Promise;
- down: (pool: Pool) => Promise;
-};
-
-const migrations: MigrationEntry[] = [
- { name: "0001_create_enums", up: createEnums, down: dropEnums },
- { name: "0002_create_teams", up: createTeams, down: dropTeams },
- { name: "0003_create_users", up: createUsers, down: dropUsers },
- { name: "0004_create_monitors", up: createMonitors, down: dropMonitors },
- { name: "0005_create_notifications", up: createNotifications, down: dropNotifications },
- { name: "0006_create_monitor_notifications", up: createMonitorNotifications, down: dropMonitorNotifications },
- { name: "0007_create_incidents", up: createIncidents, down: dropIncidents },
- { name: "0008_create_monitor_stats", up: createMonitorStats, down: dropMonitorStats },
- { name: "0009_create_checks", up: createChecks, down: dropChecks },
- { name: "0010_create_check_disks", up: createCheckDisks, down: dropCheckDisks },
- { name: "0011_create_check_network_interfaces", up: createCheckNetworkInterfaces, down: dropCheckNetworkInterfaces },
- { name: "0012_create_check_errors", up: createCheckErrors, down: dropCheckErrors },
- { name: "0013_create_geo_checks", up: createGeoChecks, down: dropGeoChecks },
- { name: "0014_create_geo_check_results", up: createGeoCheckResults, down: dropGeoCheckResults },
- { name: "0015_create_invites", up: createInvites, down: dropInvites },
- { name: "0016_create_recovery_tokens", up: createRecoveryTokens, down: dropRecoveryTokens },
- { name: "0017_create_maintenance_windows", up: createMaintenanceWindows, down: dropMaintenanceWindows },
- { name: "0018_create_status_pages", up: createStatusPages, down: dropStatusPages },
- { name: "0019_create_app_settings", up: createAppSettings, down: dropAppSettings },
- { name: "0020_create_continuous_aggregates", up: createContinuousAggregates, down: dropContinuousAggregates },
- { name: "0021_create_retention_compression", up: createRetentionCompression, down: dropRetentionCompression },
-];
-
-const ensureMigrationsTable = async (pool: Pool) => {
- await pool.query(`
- DO $$ BEGIN
- CREATE TYPE migration_status AS ENUM ('completed', 'failed');
- EXCEPTION
- WHEN duplicate_object THEN NULL;
- END $$;
- `);
- await pool.query(`
- CREATE TABLE IF NOT EXISTS migrations (
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
- name TEXT NOT NULL UNIQUE,
- status migration_status NOT NULL,
- completed_at TIMESTAMPTZ,
- error TEXT,
- created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
- updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
- );
- `);
-};
-
-const runTimescaleDBMigrations = async (pool: Pool, logger?: ILogger) => {
- try {
- logger?.info({ message: "Running migrations", service: SERVICE_NAME });
-
- await ensureMigrationsTable(pool);
-
- for (const migration of migrations) {
- const result = await pool.query("SELECT 1 FROM migrations WHERE name = $1 AND status = 'completed'", [migration.name]);
- if (result.rowCount !== null && result.rowCount > 0) {
- logger?.info({ message: `Skipping ${migration.name}`, service: SERVICE_NAME });
- continue;
- }
-
- try {
- await migration.up(pool);
- await pool.query(
- `INSERT INTO migrations (name, status, completed_at)
- VALUES ($1, 'completed', NOW())
- ON CONFLICT (name) DO UPDATE SET status = 'completed', completed_at = NOW(), error = NULL, updated_at = NOW()`,
- [migration.name]
- );
- logger?.info({ message: `Completed ${migration.name}`, service: SERVICE_NAME });
- } catch (error) {
- const err = error as Error;
- await pool.query(
- `INSERT INTO migrations (name, status, error)
- VALUES ($1, 'failed', $2)
- ON CONFLICT (name) DO UPDATE SET status = 'failed', error = $2, updated_at = NOW()`,
- [migration.name, err?.message]
- );
- throw error;
- }
- }
-
- logger?.info({ message: "Migrations completed", service: SERVICE_NAME });
- } catch (error) {
- const err = error as Error;
- logger?.error({ message: "Migration failed", service: SERVICE_NAME, details: { error: err?.message }, stack: err?.stack });
- throw error;
- }
-};
-
-const rollbackTimescaleDBMigration = async (pool: Pool, logger?: ILogger) => {
- try {
- await ensureMigrationsTable(pool);
-
- // Find the last completed migration
- const result = await pool.query("SELECT name FROM migrations WHERE status = 'completed' ORDER BY completed_at DESC LIMIT 1");
- if (result.rowCount === 0) {
- logger?.info({ message: "No migrations to roll back", service: SERVICE_NAME });
- return;
- }
-
- const lastMigrationName = result.rows[0].name;
- const migration = migrations.find((m) => m.name === lastMigrationName);
- if (!migration) {
- throw new Error(`Migration "${lastMigrationName}" not found in migration list`);
- }
-
- logger?.info({ message: `Rolling back ${migration.name}`, service: SERVICE_NAME });
- await migration.down(pool);
- await pool.query("DELETE FROM migrations WHERE name = $1", [migration.name]);
- logger?.info({ message: `Rolled back ${migration.name}`, service: SERVICE_NAME });
- } catch (error) {
- const err = error as Error;
- logger?.error({ message: "Rollback failed", service: SERVICE_NAME, details: { error: err?.message }, stack: err?.stack });
- throw error;
- }
-};
-
-export { runTimescaleDBMigrations, rollbackTimescaleDBMigration };
diff --git a/server/src/db/models/Notification.ts b/server/src/db/models/Notification.ts
index 3b424bdde..2f7effa17 100755
--- a/server/src/db/models/Notification.ts
+++ b/server/src/db/models/Notification.ts
@@ -25,7 +25,7 @@ const NotificationSchema = new Schema(
},
type: {
type: String,
- enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams", "telegram", "pushover"] as NotificationChannel[],
+ enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams", "telegram", "pushover", "twilio"] as NotificationChannel[],
required: true,
},
notificationName: {
@@ -37,6 +37,8 @@ const NotificationSchema = new Schema(
homeserverUrl: { type: String },
roomId: { type: String },
accessToken: { type: String },
+ accountSid: { type: String },
+ twilioPhoneNumber: { type: String },
},
{
timestamps: true,
diff --git a/server/src/repositories/checks/TimescaleChecksRepository.ts b/server/src/repositories/checks/TimescaleChecksRepository.ts
deleted file mode 100644
index 466be0f71..000000000
--- a/server/src/repositories/checks/TimescaleChecksRepository.ts
+++ /dev/null
@@ -1,1012 +0,0 @@
-import type { Pool } from "pg";
-import type { IChecksRepository } from "@/repositories/checks/IChecksRepository.js";
-import type {
- Check,
- CheckAudits,
- CheckCaptureInfo,
- CheckCpuInfo,
- CheckDiskInfo,
- CheckErrorInfo,
- CheckHostInfo,
- CheckMemoryInfo,
- CheckNetworkInterfaceInfo,
- ChecksQueryResult,
- ChecksSummary,
- MonitorType,
- PageSpeedChecksResult,
- HardwareChecksResult,
- UptimeChecksResult,
- GotTimings,
-} from "@/types/index.js";
-import type { LatestChecksMap } from "@/repositories/checks/MongoChecksRepistory.js";
-import { getDateForRange } from "@/utils/dataUtils.js";
-
-// Map MongoDB dateString formats to TimescaleDB time_bucket intervals
-const dateStringToBucket = (dateString: string): string => {
- if (dateString.includes("%M")) return "1 minute";
- if (dateString.includes("%H")) return "1 hour";
- return "1 day";
-};
-
-export class TimescaleChecksRepository implements IChecksRepository {
- constructor(private pool: Pool) {}
-
- // Returns correct CA table name for the given bucket interval and query type
- // Or returns null if no CA is available (fall back to raw checks table).
- private getCaTable(bucket: string, type: "uptime" | "hardware" | "pagespeed"): string | null {
- if (type === "uptime" && bucket === "1 hour") return "checks_hourly";
- if (type === "uptime" && bucket === "1 day") return "checks_daily";
- if (type === "hardware" && bucket === "1 hour") return "hardware_hourly";
- if (type === "pagespeed" && bucket === "1 day") return "pagespeed_daily";
- return null;
- }
-
- create = async (check: Check): Promise => {
- const row = await this.insertCheck(check);
- if (!row) {
- throw new Error("Failed to create check");
- }
-
- if (check.disk?.length) {
- await this.insertDisks(row.id, row.created_at, check.disk);
- }
- if (check.net?.length) {
- await this.insertNetworkInterfaces(row.id, row.created_at, check.net);
- }
- if (check.errors?.length) {
- await this.insertErrors(row.id, row.created_at, check.errors);
- }
-
- return this.toEntity(row, check.disk, check.errors, check.net);
- };
-
- createChecks = async (checks: Check[]): Promise => {
- const results: Check[] = [];
- for (const check of checks) {
- results.push(await this.create(check));
- }
- return results;
- };
-
- findByMonitorId = async (
- monitorId: string,
- sortOrder: string,
- dateRange: string,
- filter: string | undefined,
- page: number,
- rowsPerPage: number,
- status: boolean | undefined
- ): Promise => {
- const { conditions, values, paramIndex } = this.buildCheckFilters({ monitorId, dateRange, filter, status }, "monitor_id");
-
- const direction = sortOrder === "asc" ? "ASC" : "DESC";
- const offset = page && rowsPerPage ? page * rowsPerPage : 0;
-
- const [countResult, checksResult] = await Promise.all([
- this.pool.query(`SELECT COUNT(*)::int AS count FROM checks WHERE ${conditions.join(" AND ")}`, values),
- this.pool.query(
- `SELECT * FROM checks WHERE ${conditions.join(" AND ")} ORDER BY created_at ${direction} LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
- [...values, rowsPerPage, offset]
- ),
- ]);
-
- return {
- checksCount: countResult.rows[0].count,
- checks: await this.populateChildData(checksResult.rows),
- };
- };
-
- findByTeamId = async (
- sortOrder: string,
- dateRange: string,
- filter: string | undefined,
- page: number,
- rowsPerPage: number,
- teamId: string
- ): Promise => {
- const { conditions, values, paramIndex } = this.buildCheckFilters({ teamId, dateRange, filter }, "team_id");
-
- const direction = sortOrder === "asc" ? "ASC" : "DESC";
- const offset = page && rowsPerPage ? page * rowsPerPage : 0;
-
- const [countResult, checksResult] = await Promise.all([
- this.pool.query(`SELECT COUNT(*)::int AS count FROM checks WHERE ${conditions.join(" AND ")}`, values),
- this.pool.query(
- `SELECT * FROM checks WHERE ${conditions.join(" AND ")} ORDER BY created_at ${direction} LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
- [...values, rowsPerPage, offset]
- ),
- ]);
-
- return {
- checksCount: countResult.rows[0].count,
- checks: await this.populateChildData(checksResult.rows),
- };
- };
-
- findLatestByMonitorIds = async (monitorIds: string[], options?: { limitPerMonitor?: number }): Promise => {
- if (!monitorIds.length) {
- return {};
- }
- const limitPerMonitor = options?.limitPerMonitor ?? 25;
- const dateFilter = new Date(Date.now() - 24 * 60 * 60 * 1000);
-
- const allResult = await this.pool.query(
- `SELECT * FROM (
- SELECT *, ROW_NUMBER() OVER (PARTITION BY monitor_id ORDER BY created_at DESC) AS rn
- FROM checks
- WHERE monitor_id = ANY($1) AND created_at >= $2
- ) sub
- WHERE rn <= $3
- ORDER BY monitor_id, created_at DESC`,
- [monitorIds, dateFilter, limitPerMonitor]
- );
-
- const checks = await this.populateChildData(allResult.rows);
-
- const mapped: LatestChecksMap = {};
- for (let i = 0; i < allResult.rows.length; i++) {
- const mid = allResult.rows[i].monitor_id;
- if (!mapped[mid]) {
- mapped[mid] = [];
- }
- const check = checks[i];
- if (check !== undefined) {
- mapped[mid].push(check);
- }
- }
- // Ensure all requested IDs have an entry
- for (const id of monitorIds) {
- if (!mapped[id]) {
- mapped[id] = [];
- }
- }
- return mapped;
- };
-
- findByDateRangeAndMonitorId = async (
- monitorId: string,
- startDate: Date,
- endDate: Date,
- dateString: string,
- options?: { type?: MonitorType }
- ): Promise => {
- if (options?.type === "hardware") {
- return this.findHardwareDateRangeChecks(monitorId, startDate, endDate, dateString);
- }
- if (options?.type === "pagespeed") {
- return this.findPageSpeedDateRangeChecks(monitorId, startDate, endDate, dateString);
- }
- return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorId, startDate, endDate, dateString);
- };
-
- findSummaryByTeamId = async (teamId: string, dateRange: string): Promise => {
- const rangeDate = getDateForRange(dateRange);
- const conditions: string[] = ["team_id = $1"];
- const values: unknown[] = [teamId];
-
- if (rangeDate) {
- conditions.push("created_at >= $2");
- values.push(rangeDate);
- }
-
- const result = await this.pool.query(
- `SELECT
- COUNT(*)::int AS "totalChecks",
- COUNT(*) FILTER (WHERE status = FALSE)::int AS "downChecks"
- FROM checks WHERE ${conditions.join(" AND ")}`,
- values
- );
-
- return result.rows[0] ?? { totalChecks: 0, downChecks: 0 };
- };
-
- deleteByMonitorId = async (monitorId: string): Promise => {
- const result = await this.pool.query(`DELETE FROM checks WHERE monitor_id = $1`, [monitorId]);
- return result.rowCount ?? 0;
- };
-
- deleteByTeamId = async (teamId: string): Promise => {
- const result = await this.pool.query(`DELETE FROM checks WHERE team_id = $1`, [teamId]);
- return result.rowCount ?? 0;
- };
-
- deleteByMonitorIdsNotIn = async (monitorIds: string[]): Promise => {
- if (!monitorIds.length) {
- const result = await this.pool.query(`DELETE FROM checks`);
- return result.rowCount ?? 0;
- }
- const result = await this.pool.query(`DELETE FROM checks WHERE monitor_id != ALL($1)`, [monitorIds]);
- return result.rowCount ?? 0;
- };
-
- deleteOlderThan = async (date: Date): Promise => {
- const result = await this.pool.query(`DELETE FROM checks WHERE created_at < $1`, [date]);
- return result.rowCount ?? 0;
- };
-
- private populateChildData = async (checkRows: Record[]): Promise => {
- if (!checkRows.length) return [];
-
- const checkIds = checkRows.map((r) => r.id as string);
-
- const [disksResult, netsResult, errsResult] = await Promise.all([
- this.pool.query(
- `SELECT check_id, device, mountpoint, total_bytes, free_bytes, used_bytes, usage_percent,
- total_inodes, free_inodes, used_inodes, inodes_usage_percent,
- read_bytes, write_bytes, read_time, write_time
- FROM check_disks WHERE check_id = ANY($1)`,
- [checkIds]
- ),
- this.pool.query(
- `SELECT check_id, name, bytes_sent, bytes_recv, packets_sent, packets_recv,
- err_in, err_out, drop_in, drop_out, fifo_in, fifo_out
- FROM check_network_interfaces WHERE check_id = ANY($1)`,
- [checkIds]
- ),
- this.pool.query(
- `SELECT check_id, metrics, error
- FROM check_errors WHERE check_id = ANY($1)`,
- [checkIds]
- ),
- ]);
-
- const diskMap = new Map();
- for (const d of disksResult.rows) {
- const key = d.check_id as string;
- if (!diskMap.has(key)) diskMap.set(key, []);
- diskMap.get(key)!.push({
- device: d.device,
- mountpoint: d.mountpoint,
- total_bytes: Number(d.total_bytes),
- free_bytes: Number(d.free_bytes),
- used_bytes: Number(d.used_bytes),
- usage_percent: d.usage_percent,
- total_inodes: Number(d.total_inodes),
- free_inodes: Number(d.free_inodes),
- used_inodes: Number(d.used_inodes),
- inodes_usage_percent: d.inodes_usage_percent,
- read_bytes: Number(d.read_bytes),
- write_bytes: Number(d.write_bytes),
- read_time: Number(d.read_time),
- write_time: Number(d.write_time),
- });
- }
-
- const netMap = new Map();
- for (const n of netsResult.rows) {
- const key = n.check_id as string;
- if (!netMap.has(key)) netMap.set(key, []);
- netMap.get(key)!.push({
- name: n.name,
- bytes_sent: Number(n.bytes_sent),
- bytes_recv: Number(n.bytes_recv),
- packets_sent: Number(n.packets_sent),
- packets_recv: Number(n.packets_recv),
- err_in: Number(n.err_in),
- err_out: Number(n.err_out),
- drop_in: Number(n.drop_in),
- drop_out: Number(n.drop_out),
- fifo_in: Number(n.fifo_in),
- fifo_out: Number(n.fifo_out),
- });
- }
-
- const errMap = new Map();
- for (const e of errsResult.rows) {
- const key = e.check_id as string;
- if (!errMap.has(key)) errMap.set(key, []);
- errMap.get(key)!.push({ metric: e.metrics ?? [], err: e.error ?? "" });
- }
-
- return checkRows.map((row) => {
- const id = row.id as string;
- return this.toEntity(row, diskMap.get(id) ?? [], errMap.get(id) ?? [], netMap.get(id) ?? []);
- });
- };
-
- private insertCheck = async (check: Check) => {
- const result = await this.pool.query(
- `INSERT INTO checks (
- monitor_id, team_id, monitor_type, status, response_time, status_code, message,
- timing_start, timing_socket, timing_lookup, timing_connect, timing_secure_connect,
- timing_upload, timing_response, timing_end,
- phase_wait, phase_dns, phase_tcp, phase_tls, phase_request, phase_first_byte, phase_download, phase_total,
- cpu_physical_core, cpu_logical_core, cpu_frequency, cpu_current_frequency, cpu_temperature,
- cpu_free_percent, cpu_usage_percent,
- mem_total_bytes, mem_available_bytes, mem_used_bytes, mem_usage_percent,
- host_os, host_platform, host_kernel_version, host_pretty_name,
- capture_version, capture_mode,
- lighthouse_performance, lighthouse_accessibility, lighthouse_best_practices, lighthouse_seo,
- audit_cls_score, audit_cls_value, audit_cls_display,
- audit_si_score, audit_si_value, audit_si_display,
- audit_fcp_score, audit_fcp_value, audit_fcp_display,
- audit_lcp_score, audit_lcp_value, audit_lcp_display,
- audit_tbt_score, audit_tbt_value, audit_tbt_display
- ) VALUES (
- $1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,
- $24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34,$35,$36,$37,$38,$39,$40,$41,$42,$43,$44,
- $45,$46,$47,$48,$49,$50,$51,$52,$53,$54,$55,$56,$57,$58,$59
- ) RETURNING *`,
- [
- check.metadata.monitorId,
- check.metadata.teamId,
- check.metadata.type,
- check.status,
- check.responseTime,
- check.statusCode,
- check.message,
- check.timings?.start ?? null,
- check.timings?.socket ?? null,
- check.timings?.lookup ?? null,
- check.timings?.connect ?? null,
- check.timings?.secureConnect ?? null,
- check.timings?.upload ?? null,
- check.timings?.response ?? null,
- check.timings?.end ?? null,
- check.timings?.phases?.wait ?? null,
- check.timings?.phases?.dns ?? null,
- check.timings?.phases?.tcp ?? null,
- check.timings?.phases?.tls ?? null,
- check.timings?.phases?.request ?? null,
- check.timings?.phases?.firstByte ?? null,
- check.timings?.phases?.download ?? null,
- check.timings?.phases?.total ?? null,
- check.cpu?.physical_core ?? null,
- check.cpu?.logical_core ?? null,
- check.cpu?.frequency ?? null,
- check.cpu?.current_frequency ?? null,
- check.cpu?.temperature ?? null,
- check.cpu?.free_percent ?? null,
- check.cpu?.usage_percent ?? null,
- check.memory?.total_bytes ?? null,
- check.memory?.available_bytes ?? null,
- check.memory?.used_bytes ?? null,
- check.memory?.usage_percent ?? null,
- check.host?.os ?? null,
- check.host?.platform ?? null,
- check.host?.kernel_version ?? null,
- check.host?.pretty_name ?? null,
- check.capture?.version ?? null,
- check.capture?.mode ?? null,
- check.performance ?? null,
- check.accessibility ?? null,
- check.bestPractices ?? null,
- check.seo ?? null,
- check.audits?.cls?.score ?? null,
- check.audits?.cls?.numericValue ?? null,
- check.audits?.cls?.displayValue ?? null,
- check.audits?.si?.score ?? null,
- check.audits?.si?.numericValue ?? null,
- check.audits?.si?.displayValue ?? null,
- check.audits?.fcp?.score ?? null,
- check.audits?.fcp?.numericValue ?? null,
- check.audits?.fcp?.displayValue ?? null,
- check.audits?.lcp?.score ?? null,
- check.audits?.lcp?.numericValue ?? null,
- check.audits?.lcp?.displayValue ?? null,
- check.audits?.tbt?.score ?? null,
- check.audits?.tbt?.numericValue ?? null,
- check.audits?.tbt?.displayValue ?? null,
- ]
- );
- return result.rows[0];
- };
-
- private insertDisks = async (checkId: string, checkCreatedAt: Date, disks: CheckDiskInfo[]) => {
- for (const disk of disks) {
- await this.pool.query(
- `INSERT INTO check_disks (check_id, check_created_at, device, mountpoint, total_bytes, free_bytes, used_bytes, usage_percent,
- total_inodes, free_inodes, used_inodes, inodes_usage_percent, read_bytes, write_bytes, read_time, write_time)
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)`,
- [
- checkId,
- checkCreatedAt,
- disk.device,
- disk.mountpoint,
- disk.total_bytes,
- disk.free_bytes,
- disk.used_bytes,
- disk.usage_percent,
- disk.total_inodes,
- disk.free_inodes,
- disk.used_inodes,
- disk.inodes_usage_percent,
- disk.read_bytes,
- disk.write_bytes,
- disk.read_time,
- disk.write_time,
- ]
- );
- }
- };
-
- private insertNetworkInterfaces = async (checkId: string, checkCreatedAt: Date, nets: CheckNetworkInterfaceInfo[]) => {
- for (const net of nets) {
- await this.pool.query(
- `INSERT INTO check_network_interfaces (check_id, check_created_at, name, bytes_sent, bytes_recv, packets_sent, packets_recv,
- err_in, err_out, drop_in, drop_out, fifo_in, fifo_out)
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13)`,
- [
- checkId,
- checkCreatedAt,
- net.name,
- net.bytes_sent,
- net.bytes_recv,
- net.packets_sent,
- net.packets_recv,
- net.err_in,
- net.err_out,
- net.drop_in,
- net.drop_out,
- net.fifo_in,
- net.fifo_out,
- ]
- );
- }
- };
-
- private insertErrors = async (checkId: string, checkCreatedAt: Date, errors: CheckErrorInfo[]) => {
- for (const err of errors) {
- await this.pool.query(
- `INSERT INTO check_errors (check_id, check_created_at, metrics, error)
- VALUES ($1,$2,$3,$4)`,
- [checkId, checkCreatedAt, err.metric, err.err]
- );
- }
- };
-
- private buildCheckFilters = (
- opts: { monitorId?: string; teamId?: string; dateRange?: string; filter?: string; status?: boolean },
- idColumn: string
- ) => {
- const conditions: string[] = [];
- const values: unknown[] = [];
- let paramIndex = 1;
-
- if (opts.monitorId) {
- conditions.push(`${idColumn} = $${paramIndex++}`);
- values.push(opts.monitorId);
- } else if (opts.teamId) {
- conditions.push(`${idColumn} = $${paramIndex++}`);
- values.push(opts.teamId);
- }
-
- if (opts.dateRange) {
- const rangeDate = getDateForRange(opts.dateRange);
- if (rangeDate) {
- conditions.push(`created_at >= $${paramIndex++}`);
- values.push(rangeDate);
- }
- }
-
- // Filter overwrites status (matching Mongo behavior where filter sets matchStage.status)
- let statusApplied = false;
- if (opts.filter !== undefined) {
- switch (opts.filter) {
- case "all":
- break;
- case "up":
- conditions.push(`status = TRUE`);
- statusApplied = true;
- break;
- case "down":
- conditions.push(`status = FALSE`);
- statusApplied = true;
- break;
- case "resolve":
- conditions.push(`status = FALSE`);
- conditions.push(`status_code = 5000`);
- statusApplied = true;
- break;
- default:
- break;
- }
- }
-
- if (!statusApplied && opts.status !== undefined) {
- conditions.push(`status = $${paramIndex++}`);
- values.push(opts.status);
- }
-
- return { conditions, values, paramIndex };
- };
-
- private findUptimeDateRangeChecks = async (
- monitorType: Exclude,
- monitorId: string,
- startDate: Date,
- endDate: Date,
- dateString: string
- ): Promise => {
- const bucket = dateStringToBucket(dateString);
- const caTable = this.getCaTable(bucket, "uptime");
-
- if (caTable) {
- return this.findUptimeFromCa(monitorType, monitorId, startDate, endDate, caTable);
- }
- return this.findUptimeFromRaw(monitorType, monitorId, startDate, endDate, bucket);
- };
-
- private findUptimeFromCa = async (
- monitorType: Exclude,
- monitorId: string,
- startDate: Date,
- endDate: Date,
- caTable: string
- ): Promise => {
- const result = await this.pool.query(
- `SELECT bucket AS bucket_date, total_checks::int, up_checks::int, down_checks::int,
- avg_response_time, avg_up_response_time, avg_down_response_time
- FROM ${caTable}
- WHERE monitor_id = $1 AND bucket >= $2 AND bucket <= $3
- ORDER BY bucket`,
- [monitorId, startDate, endDate]
- );
-
- let totalChecks = 0;
- let totalUp = 0;
- let weightedResponseTime = 0;
-
- const groupedChecks = result.rows.map((row) => {
- const count = Number(row.total_checks);
- const up = Number(row.up_checks);
- const avg = Number(row.avg_response_time ?? 0);
- totalChecks += count;
- totalUp += up;
- weightedResponseTime += avg * count;
- return {
- bucketDate: (row.bucket_date as Date).toISOString(),
- avgResponseTime: avg,
- totalChecks: count,
- };
- });
-
- const groupedUpChecks = result.rows
- .filter((row) => Number(row.up_checks) > 0)
- .map((row) => ({
- bucketDate: (row.bucket_date as Date).toISOString(),
- avgResponseTime: Number(row.avg_up_response_time ?? 0),
- totalChecks: Number(row.up_checks),
- }));
-
- const groupedDownChecks = result.rows
- .filter((row) => Number(row.down_checks) > 0)
- .map((row) => ({
- bucketDate: (row.bucket_date as Date).toISOString(),
- avgResponseTime: Number(row.avg_down_response_time ?? 0),
- totalChecks: Number(row.down_checks),
- }));
-
- return {
- monitorType,
- groupedChecks,
- groupedUpChecks,
- groupedDownChecks,
- uptimePercentage: totalChecks > 0 ? totalUp / totalChecks : 0,
- avgResponseTime: totalChecks > 0 ? weightedResponseTime / totalChecks : 0,
- };
- };
-
- private findUptimeFromRaw = async (
- monitorType: Exclude,
- monitorId: string,
- startDate: Date,
- endDate: Date,
- bucket: string
- ): Promise => {
- const [uptimeResult, groupedResult, upResult, downResult] = await Promise.all([
- this.pool.query(
- `SELECT
- COUNT(*) FILTER (WHERE status = TRUE) AS up_checks,
- COUNT(*) AS total_checks,
- AVG(response_time) AS avg_response_time
- FROM checks WHERE monitor_id = $1 AND created_at >= $2 AND created_at <= $3`,
- [monitorId, startDate, endDate]
- ),
- this.pool.query(
- `SELECT
- time_bucket($1::interval, created_at) AS bucket_date,
- AVG(response_time) AS "avgResponseTime",
- COUNT(*)::int AS "totalChecks"
- FROM checks WHERE monitor_id = $2 AND created_at >= $3 AND created_at <= $4
- GROUP BY bucket_date ORDER BY bucket_date`,
- [bucket, monitorId, startDate, endDate]
- ),
- this.pool.query(
- `SELECT
- time_bucket($1::interval, created_at) AS bucket_date,
- AVG(response_time) AS "avgResponseTime",
- COUNT(*)::int AS "totalChecks"
- FROM checks WHERE monitor_id = $2 AND created_at >= $3 AND created_at <= $4 AND status = TRUE
- GROUP BY bucket_date ORDER BY bucket_date`,
- [bucket, monitorId, startDate, endDate]
- ),
- this.pool.query(
- `SELECT
- time_bucket($1::interval, created_at) AS bucket_date,
- AVG(response_time) AS "avgResponseTime",
- COUNT(*)::int AS "totalChecks"
- FROM checks WHERE monitor_id = $2 AND created_at >= $3 AND created_at <= $4 AND status = FALSE
- GROUP BY bucket_date ORDER BY bucket_date`,
- [bucket, monitorId, startDate, endDate]
- ),
- ]);
-
- const stats = uptimeResult.rows[0];
- const totalChecks = Number(stats?.total_checks ?? 0);
- const upChecks = Number(stats?.up_checks ?? 0);
-
- const formatGrouped = (rows: Array>) =>
- rows.map((row) => ({
- bucketDate: (row.bucket_date as Date).toISOString(),
- avgResponseTime: Number(row.avgResponseTime ?? 0),
- totalChecks: Number(row.totalChecks ?? 0),
- }));
-
- return {
- monitorType,
- groupedChecks: formatGrouped(groupedResult.rows),
- groupedUpChecks: formatGrouped(upResult.rows),
- groupedDownChecks: formatGrouped(downResult.rows),
- uptimePercentage: totalChecks > 0 ? upChecks / totalChecks : 0,
- avgResponseTime: Number(stats?.avg_response_time ?? 0),
- };
- };
-
- private findHardwareDateRangeChecks = async (
- monitorId: string,
- startDate: Date,
- endDate: Date,
- dateString: string
- ): Promise => {
- const bucket = dateStringToBucket(dateString);
- const caTable = this.getCaTable(bucket, "hardware");
-
- // Build the metrics query: use hardware_hourly CA for cpu/mem when available,
- // but temperature always requires the raw table
- const metricsQuery = caTable
- ? this.pool.query(
- `SELECT bucket AS bucket_date, avg_cpu, avg_memory
- FROM ${caTable}
- WHERE monitor_id = $1 AND bucket >= $2 AND bucket <= $3
- ORDER BY bucket`,
- [monitorId, startDate, endDate]
- )
- : this.pool.query(
- `SELECT
- time_bucket($1::interval, created_at) AS bucket_date,
- AVG(cpu_usage_percent) AS avg_cpu,
- AVG(mem_usage_percent) AS avg_memory
- FROM checks
- WHERE monitor_id = $2 AND monitor_type = 'hardware' AND created_at >= $3 AND created_at <= $4
- GROUP BY bucket_date ORDER BY bucket_date`,
- [bucket, monitorId, startDate, endDate]
- );
-
- // Temperature query always hits raw table (not in any CA)
- const tempQuery = this.pool.query(
- `WITH temp_avg AS (
- SELECT time_bucket($1::interval, c.created_at) AS bucket_date, idx, AVG(val) AS avg_val
- FROM checks c, unnest(c.cpu_temperature) WITH ORDINALITY AS t(val, idx)
- WHERE c.monitor_id = $2 AND c.monitor_type = 'hardware' AND c.created_at >= $3 AND c.created_at <= $4
- GROUP BY bucket_date, idx
- )
- SELECT bucket_date, array_agg(avg_val ORDER BY idx) AS avg_temperature
- FROM temp_avg
- GROUP BY bucket_date
- ORDER BY bucket_date`,
- [bucket, monitorId, startDate, endDate]
- );
-
- const diskQuery = this.pool.query(
- `SELECT
- time_bucket($1::interval, d.check_created_at) AS bucket_date,
- d.device AS name,
- AVG(d.read_bytes) AS "readSpeed",
- AVG(d.write_bytes) AS "writeSpeed",
- AVG(d.total_bytes) AS "totalBytes",
- AVG(d.free_bytes) AS "freeBytes",
- AVG(d.usage_percent) AS "usagePercent"
- FROM check_disks d
- WHERE d.check_id IN (
- SELECT id FROM checks
- WHERE monitor_id = $2 AND monitor_type = 'hardware' AND created_at >= $3 AND created_at <= $4
- )
- GROUP BY bucket_date, d.device
- ORDER BY bucket_date, d.device`,
- [bucket, monitorId, startDate, endDate]
- );
-
- // Batched network query across all buckets (eliminates N+1)
- const netQuery = this.pool.query(
- `WITH bounded AS (
- SELECT
- time_bucket($1::interval, c.created_at) AS bucket_date,
- n.name,
- FIRST_VALUE(n.bytes_sent) OVER w AS first_bytes_sent,
- LAST_VALUE(n.bytes_sent) OVER w AS last_bytes_sent,
- FIRST_VALUE(n.bytes_recv) OVER w AS first_bytes_recv,
- LAST_VALUE(n.bytes_recv) OVER w AS last_bytes_recv,
- FIRST_VALUE(n.packets_sent) OVER w AS first_packets_sent,
- LAST_VALUE(n.packets_sent) OVER w AS last_packets_sent,
- FIRST_VALUE(n.packets_recv) OVER w AS first_packets_recv,
- LAST_VALUE(n.packets_recv) OVER w AS last_packets_recv,
- FIRST_VALUE(n.err_in) OVER w AS first_err_in,
- LAST_VALUE(n.err_in) OVER w AS last_err_in,
- FIRST_VALUE(n.err_out) OVER w AS first_err_out,
- LAST_VALUE(n.err_out) OVER w AS last_err_out,
- FIRST_VALUE(n.drop_in) OVER w AS first_drop_in,
- LAST_VALUE(n.drop_in) OVER w AS last_drop_in,
- FIRST_VALUE(n.drop_out) OVER w AS first_drop_out,
- LAST_VALUE(n.drop_out) OVER w AS last_drop_out,
- FIRST_VALUE(n.fifo_in) OVER w AS first_fifo_in,
- LAST_VALUE(n.fifo_in) OVER w AS last_fifo_in,
- FIRST_VALUE(n.fifo_out) OVER w AS first_fifo_out,
- LAST_VALUE(n.fifo_out) OVER w AS last_fifo_out,
- FIRST_VALUE(c.created_at) OVER w AS first_time,
- LAST_VALUE(c.created_at) OVER w AS last_time
- FROM check_network_interfaces n
- JOIN checks c ON c.id = n.check_id
- WHERE c.monitor_id = $2 AND c.monitor_type = 'hardware'
- AND c.created_at >= $3 AND c.created_at <= $4
- WINDOW w AS (PARTITION BY time_bucket($1::interval, c.created_at), n.name
- ORDER BY c.created_at ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
- )
- SELECT DISTINCT ON (bucket_date, name) bucket_date, name,
- CASE WHEN EXTRACT(EPOCH FROM (last_time - first_time)) > 0
- THEN (last_bytes_sent - first_bytes_sent) / EXTRACT(EPOCH FROM (last_time - first_time))
- ELSE 0 END AS "bytesSentPerSecond",
- CASE WHEN EXTRACT(EPOCH FROM (last_time - first_time)) > 0
- THEN (last_bytes_recv - first_bytes_recv) / EXTRACT(EPOCH FROM (last_time - first_time))
- ELSE 0 END AS "deltaBytesRecv",
- CASE WHEN EXTRACT(EPOCH FROM (last_time - first_time)) > 0
- THEN (last_packets_sent - first_packets_sent) / EXTRACT(EPOCH FROM (last_time - first_time))
- ELSE 0 END AS "deltaPacketsSent",
- CASE WHEN EXTRACT(EPOCH FROM (last_time - first_time)) > 0
- THEN (last_packets_recv - first_packets_recv) / EXTRACT(EPOCH FROM (last_time - first_time))
- ELSE 0 END AS "deltaPacketsRecv",
- CASE WHEN EXTRACT(EPOCH FROM (last_time - first_time)) > 0
- THEN (last_err_in - first_err_in) / EXTRACT(EPOCH FROM (last_time - first_time))
- ELSE 0 END AS "deltaErrIn",
- CASE WHEN EXTRACT(EPOCH FROM (last_time - first_time)) > 0
- THEN (last_err_out - first_err_out) / EXTRACT(EPOCH FROM (last_time - first_time))
- ELSE 0 END AS "deltaErrOut",
- CASE WHEN EXTRACT(EPOCH FROM (last_time - first_time)) > 0
- THEN (last_drop_in - first_drop_in) / EXTRACT(EPOCH FROM (last_time - first_time))
- ELSE 0 END AS "deltaDropIn",
- CASE WHEN EXTRACT(EPOCH FROM (last_time - first_time)) > 0
- THEN (last_drop_out - first_drop_out) / EXTRACT(EPOCH FROM (last_time - first_time))
- ELSE 0 END AS "deltaDropOut",
- CASE WHEN EXTRACT(EPOCH FROM (last_time - first_time)) > 0
- THEN (last_fifo_in - first_fifo_in) / EXTRACT(EPOCH FROM (last_time - first_time))
- ELSE 0 END AS "deltaFifoIn",
- CASE WHEN EXTRACT(EPOCH FROM (last_time - first_time)) > 0
- THEN (last_fifo_out - first_fifo_out) / EXTRACT(EPOCH FROM (last_time - first_time))
- ELSE 0 END AS "deltaFifoOut"
- FROM bounded
- ORDER BY bucket_date, name`,
- [bucket, monitorId, startDate, endDate]
- );
-
- const [totalResult, upResult, metricsResult, tempResult, diskResult, netResult] = await Promise.all([
- this.pool.query(
- `SELECT COUNT(*)::int AS count FROM checks
- WHERE monitor_id = $1 AND monitor_type = 'hardware' AND created_at >= $2 AND created_at <= $3`,
- [monitorId, startDate, endDate]
- ),
- this.pool.query(
- `SELECT COUNT(*)::int AS count FROM checks
- WHERE monitor_id = $1 AND monitor_type = 'hardware' AND created_at >= $2 AND created_at <= $3 AND status = TRUE`,
- [monitorId, startDate, endDate]
- ),
- metricsQuery,
- tempQuery,
- diskQuery,
- netQuery,
- ]);
-
- // Group disk and net results by bucket date
- const disksByBucket = new Map();
- for (const row of diskResult.rows) {
- const key = (row.bucket_date as Date).toISOString();
- if (!disksByBucket.has(key)) disksByBucket.set(key, []);
- disksByBucket.get(key)!.push(row);
- }
-
- const netsByBucket = new Map();
- for (const row of netResult.rows) {
- const key = (row.bucket_date as Date).toISOString();
- if (!netsByBucket.has(key)) netsByBucket.set(key, []);
- netsByBucket.get(key)!.push(row);
- }
-
- const tempByBucket = new Map();
- for (const row of tempResult.rows) {
- const key = (row.bucket_date as Date).toISOString();
- tempByBucket.set(key, Array.isArray(row.avg_temperature) ? row.avg_temperature.map(Number) : []);
- }
-
- // Assemble checks array from metrics + disk/net maps
- const checks = metricsResult.rows.map((row) => {
- const bucketDate = (row.bucket_date as Date).toISOString();
- const disks = disksByBucket.get(bucketDate) ?? [];
- const nets = netsByBucket.get(bucketDate) ?? [];
-
- return {
- bucketDate,
- avgCpuUsage: Number(row.avg_cpu ?? 0),
- avgMemoryUsage: Number(row.avg_memory ?? 0),
- avgTemperature: tempByBucket.get(bucketDate) ?? [],
- disks: disks.map((d) => ({
- name: d.name ?? "",
- readSpeed: Number(d.readSpeed ?? 0),
- writeSpeed: Number(d.writeSpeed ?? 0),
- totalBytes: Number(d.totalBytes ?? 0),
- freeBytes: Number(d.freeBytes ?? 0),
- usagePercent: Number(d.usagePercent ?? 0),
- })),
- net: nets.map((n) => ({
- name: n.name ?? "",
- bytesSentPerSecond: Number(n.bytesSentPerSecond ?? 0),
- deltaBytesRecv: Number(n.deltaBytesRecv ?? 0),
- deltaPacketsSent: Number(n.deltaPacketsSent ?? 0),
- deltaPacketsRecv: Number(n.deltaPacketsRecv ?? 0),
- deltaErrIn: Number(n.deltaErrIn ?? 0),
- deltaErrOut: Number(n.deltaErrOut ?? 0),
- deltaDropIn: Number(n.deltaDropIn ?? 0),
- deltaDropOut: Number(n.deltaDropOut ?? 0),
- deltaFifoIn: Number(n.deltaFifoIn ?? 0),
- deltaFifoOut: Number(n.deltaFifoOut ?? 0),
- })),
- };
- });
-
- return {
- monitorType: "hardware" as const,
- aggregateData: { totalChecks: totalResult.rows[0].count },
- upChecks: { totalChecks: upResult.rows[0].count },
- checks,
- };
- };
-
- private findPageSpeedDateRangeChecks = async (
- monitorId: string,
- startDate: Date,
- endDate: Date,
- dateString: string
- ): Promise => {
- const bucket = dateStringToBucket(dateString);
- const caTable = this.getCaTable(bucket, "pagespeed");
-
- const result = caTable
- ? await this.pool.query(
- `SELECT bucket AS bucket_date,
- avg_performance AS performance,
- avg_accessibility AS accessibility,
- avg_best_practices AS "bestPractices",
- avg_seo AS seo,
- sample_count::int AS "totalChecks"
- FROM ${caTable}
- WHERE monitor_id = $1 AND bucket >= $2 AND bucket <= $3
- ORDER BY bucket`,
- [monitorId, startDate, endDate]
- )
- : await this.pool.query(
- `SELECT
- time_bucket($1::interval, created_at) AS bucket_date,
- AVG(lighthouse_performance) AS performance,
- AVG(lighthouse_accessibility) AS accessibility,
- AVG(lighthouse_best_practices) AS "bestPractices",
- AVG(lighthouse_seo) AS seo,
- COUNT(*)::int AS "totalChecks"
- FROM checks
- WHERE monitor_id = $2 AND monitor_type = 'pagespeed' AND created_at >= $3 AND created_at <= $4
- GROUP BY bucket_date ORDER BY bucket_date`,
- [bucket, monitorId, startDate, endDate]
- );
-
- return {
- monitorType: "pagespeed" as const,
- groupedChecks: result.rows.map((row) => ({
- bucketDate: (row.bucket_date as Date).toISOString(),
- performance: Number(row.performance ?? 0),
- accessibility: Number(row.accessibility ?? 0),
- bestPractices: Number(row.bestPractices ?? 0),
- seo: Number(row.seo ?? 0),
- totalChecks: Number(row.totalChecks ?? 0),
- })),
- };
- };
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- private toEntity = (row: any, disk?: CheckDiskInfo[], errors?: CheckErrorInfo[], net?: CheckNetworkInterfaceInfo[]): Check => {
- const timings: GotTimings = {
- start: row.timing_start ?? 0,
- socket: row.timing_socket ?? 0,
- lookup: row.timing_lookup ?? 0,
- connect: row.timing_connect ?? 0,
- secureConnect: row.timing_secure_connect ?? 0,
- upload: row.timing_upload ?? 0,
- response: row.timing_response ?? 0,
- end: row.timing_end ?? 0,
- phases: {
- wait: row.phase_wait ?? 0,
- dns: row.phase_dns ?? 0,
- tcp: row.phase_tcp ?? 0,
- tls: row.phase_tls ?? 0,
- request: row.phase_request ?? 0,
- firstByte: row.phase_first_byte ?? 0,
- download: row.phase_download ?? 0,
- total: row.phase_total ?? 0,
- },
- };
-
- const cpu: CheckCpuInfo = {
- physical_core: row.cpu_physical_core ?? 0,
- logical_core: row.cpu_logical_core ?? 0,
- frequency: row.cpu_frequency ?? 0,
- current_frequency: row.cpu_current_frequency ?? 0,
- temperature: row.cpu_temperature ?? [],
- free_percent: row.cpu_free_percent ?? 0,
- usage_percent: row.cpu_usage_percent ?? 0,
- };
-
- const memory: CheckMemoryInfo = {
- total_bytes: Number(row.mem_total_bytes ?? 0),
- available_bytes: Number(row.mem_available_bytes ?? 0),
- used_bytes: Number(row.mem_used_bytes ?? 0),
- usage_percent: row.mem_usage_percent ?? 0,
- };
-
- const host: CheckHostInfo = {
- os: row.host_os ?? "",
- platform: row.host_platform ?? "",
- kernel_version: row.host_kernel_version ?? "",
- pretty_name: row.host_pretty_name ?? "",
- };
-
- const capture: CheckCaptureInfo = {
- version: row.capture_version ?? "",
- mode: row.capture_mode ?? "",
- };
-
- const audits: CheckAudits | undefined =
- row.audit_cls_score !== null
- ? {
- cls: { score: row.audit_cls_score, numericValue: row.audit_cls_value, displayValue: row.audit_cls_display },
- si: { score: row.audit_si_score, numericValue: row.audit_si_value, displayValue: row.audit_si_display },
- fcp: { score: row.audit_fcp_score, numericValue: row.audit_fcp_value, displayValue: row.audit_fcp_display },
- lcp: { score: row.audit_lcp_score, numericValue: row.audit_lcp_value, displayValue: row.audit_lcp_display },
- tbt: { score: row.audit_tbt_score, numericValue: row.audit_tbt_value, displayValue: row.audit_tbt_display },
- }
- : undefined;
-
- return {
- id: row.id,
- metadata: {
- monitorId: row.monitor_id,
- teamId: row.team_id,
- type: row.monitor_type,
- },
- status: row.status ?? false,
- responseTime: row.response_time ?? 0,
- timings,
- statusCode: row.status_code ?? 0,
- message: row.message ?? "",
- cpu,
- memory,
- disk: disk ?? [],
- host,
- errors: errors ?? [],
- capture,
- net: net ?? [],
- accessibility: row.lighthouse_accessibility ?? undefined,
- bestPractices: row.lighthouse_best_practices ?? undefined,
- seo: row.lighthouse_seo ?? undefined,
- performance: row.lighthouse_performance ?? undefined,
- audits,
- createdAt: row.created_at.toISOString(),
- updatedAt: row.updated_at.toISOString(),
- };
- };
-}
diff --git a/server/src/repositories/geo-checks/TimescaleGeoChecksRepository.ts b/server/src/repositories/geo-checks/TimescaleGeoChecksRepository.ts
deleted file mode 100644
index e4528f259..000000000
--- a/server/src/repositories/geo-checks/TimescaleGeoChecksRepository.ts
+++ /dev/null
@@ -1,274 +0,0 @@
-import type { Pool } from "pg";
-import type { IGeoChecksRepository, FlatGeoChecksQueryResult } from "./IGeoChecksRepository.js";
-import type { GeoCheck, GeoCheckResult, GroupedGeoCheck, GeoContinent, FlatGeoCheck, MonitorType } from "@/types/index.js";
-import { getDateForRange } from "@/utils/dataUtils.js";
-
-const dateStringToBucket = (dateString: string): string => {
- if (dateString.includes("%M")) return "1 minute";
- if (dateString.includes("%H")) return "1 hour";
- return "1 day";
-};
-
-export class TimescaleGeoChecksRepository implements IGeoChecksRepository {
- constructor(private pool: Pool) {}
-
- createGeoChecks = async (geoChecks: Omit[]): Promise => {
- const created: GeoCheck[] = [];
-
- for (const geoCheck of geoChecks) {
- const geoResult = await this.pool.query(
- `INSERT INTO geo_checks (monitor_id, team_id, monitor_type, expiry)
- VALUES ($1, $2, $3, $4)
- RETURNING id, monitor_id, team_id, monitor_type, expiry, created_at, updated_at`,
- [geoCheck.metadata.monitorId, geoCheck.metadata.teamId, geoCheck.metadata.type, geoCheck.expiry ? new Date(geoCheck.expiry) : null]
- );
- const row = geoResult.rows[0];
- if (!row) continue;
-
- for (const result of geoCheck.results) {
- await this.pool.query(
- `INSERT INTO geo_check_results (
- geo_check_id, geo_check_created_at,
- status, status_code,
- location_continent, location_region, location_country, location_state, location_city,
- location_longitude, location_latitude,
- timing_total, timing_dns, timing_tcp, timing_tls, timing_first_byte, timing_download
- ) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17)`,
- [
- row.id,
- row.created_at,
- result.status,
- result.statusCode,
- result.location.continent,
- result.location.region,
- result.location.country,
- result.location.state,
- result.location.city,
- result.location.longitude,
- result.location.latitude,
- result.timings.total,
- result.timings.dns,
- result.timings.tcp,
- result.timings.tls,
- result.timings.firstByte,
- result.timings.download,
- ]
- );
- }
-
- created.push(this.toGeoCheck(row, geoCheck.results));
- }
-
- return created;
- };
-
- findByMonitorId = async (
- monitorId: string,
- sortOrder: string,
- dateRange: string,
- page: number,
- rowsPerPage: number,
- continents?: GeoContinent[]
- ): Promise => {
- const conditions: string[] = ["gc.monitor_id = $1"];
- const values: unknown[] = [monitorId];
- let paramIndex = 2;
-
- const rangeDate = getDateForRange(dateRange);
- if (rangeDate) {
- conditions.push(`gc.created_at >= $${paramIndex++}`);
- values.push(rangeDate);
- }
-
- if (continents && continents.length > 0) {
- conditions.push(`r.location_continent = ANY($${paramIndex++})`);
- values.push(continents);
- }
-
- const where = conditions.join(" AND ");
- const direction = sortOrder === "asc" ? "ASC" : "DESC";
- const offset = page && rowsPerPage ? page * rowsPerPage : 0;
-
- const countValues = [...values];
- const dataValues = [...values, rowsPerPage, offset];
-
- const [countResult, dataResult] = await Promise.all([
- this.pool.query(
- `SELECT COUNT(*)::int AS count
- FROM geo_checks gc
- JOIN geo_check_results r ON r.geo_check_id = gc.id
- WHERE ${where}`,
- countValues
- ),
- this.pool.query(
- `SELECT gc.id, gc.monitor_id, gc.team_id, gc.monitor_type,
- r.location_continent, r.location_region, r.location_country, r.location_state, r.location_city,
- r.location_longitude, r.location_latitude,
- r.status, r.status_code,
- r.timing_total, r.timing_dns, r.timing_tcp, r.timing_tls, r.timing_first_byte, r.timing_download,
- gc.created_at, gc.updated_at
- FROM geo_checks gc
- JOIN geo_check_results r ON r.geo_check_id = gc.id
- WHERE ${where}
- ORDER BY gc.created_at ${direction}
- LIMIT $${paramIndex++} OFFSET $${paramIndex}`,
- dataValues
- ),
- ]);
-
- const geoChecks: FlatGeoCheck[] = dataResult.rows.map((row) => ({
- id: `${row.monitor_id}-${new Date(row.created_at).getTime()}-${row.location_continent}-${row.location_city}-${row.id.substring(0, 8)}`,
- monitorId: row.monitor_id,
- teamId: row.team_id,
- type: row.monitor_type,
- location: {
- continent: row.location_continent,
- region: row.location_region,
- country: row.location_country,
- state: row.location_state,
- city: row.location_city,
- longitude: row.location_longitude,
- latitude: row.location_latitude,
- },
- status: row.status,
- statusCode: row.status_code,
- timings: {
- total: row.timing_total ?? 0,
- dns: row.timing_dns ?? 0,
- tcp: row.timing_tcp ?? 0,
- tls: row.timing_tls ?? 0,
- firstByte: row.timing_first_byte ?? 0,
- download: row.timing_download ?? 0,
- },
- createdAt: row.created_at.toISOString(),
- updatedAt: row.updated_at.toISOString(),
- }));
-
- return { geoChecksCount: countResult.rows[0].count, geoChecks };
- };
-
- findByMonitorIdAndDateRange = async (monitorId: string, startDate: Date, endDate: Date): Promise => {
- const geoRows = await this.pool.query(
- `SELECT id, monitor_id, team_id, monitor_type, expiry, created_at, updated_at
- FROM geo_checks
- WHERE monitor_id = $1 AND created_at >= $2 AND created_at <= $3
- ORDER BY created_at DESC`,
- [monitorId, startDate, endDate]
- );
-
- const geoChecks: GeoCheck[] = [];
- for (const row of geoRows.rows) {
- const resultsRows = await this.pool.query(
- `SELECT status, status_code,
- location_continent, location_region, location_country, location_state, location_city,
- location_longitude, location_latitude,
- timing_total, timing_dns, timing_tcp, timing_tls, timing_first_byte, timing_download
- FROM geo_check_results
- WHERE geo_check_id = $1`,
- [row.id]
- );
-
- const results = resultsRows.rows.map(this.toGeoCheckResult);
- geoChecks.push(this.toGeoCheck(row, results));
- }
-
- return geoChecks;
- };
-
- findGroupedByMonitorIdAndDateRange = async (
- monitorId: string,
- startDate: Date,
- endDate: Date,
- dateFormat: string,
- continents?: GeoContinent[]
- ): Promise => {
- const bucket = dateStringToBucket(dateFormat);
-
- const conditions: string[] = ["gc.monitor_id = $2", "gc.created_at >= $3", "gc.created_at <= $4"];
- const values: unknown[] = [bucket, monitorId, startDate, endDate];
- let paramIndex = 5;
-
- if (continents && continents.length > 0) {
- conditions.push(`r.location_continent = ANY($${paramIndex++})`);
- values.push(continents);
- }
-
- const result = await this.pool.query(
- `SELECT
- time_bucket($1::interval, gc.created_at) AS bucket_date,
- r.location_continent AS continent,
- ROUND(AVG(r.timing_total)::numeric, 2) AS "avgResponseTime",
- COUNT(*)::int AS "totalChecks",
- ROUND((100.0 * COUNT(*) FILTER (WHERE r.status = TRUE) / COUNT(*))::numeric, 2) AS "uptimePercentage"
- FROM geo_checks gc
- JOIN geo_check_results r ON r.geo_check_id = gc.id
- WHERE ${conditions.join(" AND ")}
- GROUP BY bucket_date, r.location_continent
- ORDER BY bucket_date, r.location_continent`,
- values
- );
-
- return result.rows.map((row) => ({
- bucketDate: (row.bucket_date as Date).toISOString(),
- continent: row.continent as GeoContinent,
- avgResponseTime: Number(row.avgResponseTime ?? 0),
- totalChecks: Number(row.totalChecks ?? 0),
- uptimePercentage: Number(row.uptimePercentage ?? 0),
- }));
- };
-
- deleteByMonitorId = async (monitorId: string): Promise => {
- const result = await this.pool.query(`DELETE FROM geo_checks WHERE monitor_id = $1`, [monitorId]);
- return result.rowCount ?? 0;
- };
-
- deleteByTeamId = async (teamId: string): Promise => {
- const result = await this.pool.query(`DELETE FROM geo_checks WHERE team_id = $1`, [teamId]);
- return result.rowCount ?? 0;
- };
-
- deleteByMonitorIdsNotIn = async (monitorIds: string[]): Promise => {
- if (!monitorIds.length) {
- const result = await this.pool.query(`DELETE FROM geo_checks`);
- return result.rowCount ?? 0;
- }
- const result = await this.pool.query(`DELETE FROM geo_checks WHERE monitor_id != ALL($1)`, [monitorIds]);
- return result.rowCount ?? 0;
- };
-
- private toGeoCheckResult = (row: Record): GeoCheckResult => ({
- location: {
- continent: (row.location_continent as GeoContinent) ?? "",
- region: (row.location_region as string) ?? "",
- country: (row.location_country as string) ?? "",
- state: (row.location_state as string) ?? "",
- city: (row.location_city as string) ?? "",
- longitude: (row.location_longitude as number) ?? 0,
- latitude: (row.location_latitude as number) ?? 0,
- },
- status: (row.status as boolean) ?? false,
- statusCode: (row.status_code as number) ?? 0,
- timings: {
- total: (row.timing_total as number) ?? 0,
- dns: (row.timing_dns as number) ?? 0,
- tcp: (row.timing_tcp as number) ?? 0,
- tls: (row.timing_tls as number) ?? 0,
- firstByte: (row.timing_first_byte as number) ?? 0,
- download: (row.timing_download as number) ?? 0,
- },
- });
-
- private toGeoCheck = (row: Record, results: GeoCheckResult[]): GeoCheck => ({
- id: row.id as string,
- metadata: {
- monitorId: row.monitor_id as string,
- teamId: row.team_id as string,
- type: row.monitor_type as MonitorType,
- },
- results,
- expiry: row.expiry ? (row.expiry as Date).toISOString() : new Date(0).toISOString(),
- __v: 0,
- createdAt: (row.created_at as Date).toISOString(),
- updatedAt: (row.updated_at as Date).toISOString(),
- });
-}
diff --git a/server/src/repositories/incidents/TimescaleIncidentsRepository.ts b/server/src/repositories/incidents/TimescaleIncidentsRepository.ts
deleted file mode 100644
index 8a6ad7d22..000000000
--- a/server/src/repositories/incidents/TimescaleIncidentsRepository.ts
+++ /dev/null
@@ -1,294 +0,0 @@
-import type { Pool } from "pg";
-import type { Incident, IncidentSummary, IncidentResolutionType } from "@/types/incident.js";
-import type { IIncidentsRepository } from "@/repositories/incidents/IIncidentsRepository.js";
-import { AppError } from "@/utils/AppError.js";
-
-interface IncidentRow {
- id: string;
- monitor_id: string;
- team_id: string;
- start_time: Date;
- end_time: Date | null;
- status: boolean;
- message: string | null;
- status_code: number | null;
- resolution_type: IncidentResolutionType;
- resolved_by: string | null;
- resolved_by_email: string | null;
- comment: string | null;
- created_at: Date;
- updated_at: Date;
-}
-
-const COLUMNS = `id, monitor_id, team_id, start_time, end_time, status, message, status_code,
- resolution_type, resolved_by, resolved_by_email, comment, created_at, updated_at`;
-
-export class TimescaleIncidentsRepository implements IIncidentsRepository {
- constructor(private pool: Pool) {}
-
- create = async (incident: Partial): Promise => {
- const result = await this.pool.query(
- `INSERT INTO incidents (monitor_id, team_id, start_time, end_time, status, message, status_code, resolution_type, resolved_by, resolved_by_email, comment)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
- RETURNING ${COLUMNS}`,
- [
- incident.monitorId,
- incident.teamId,
- incident.startTime ? new Date(Number(incident.startTime) || incident.startTime) : new Date(),
- incident.endTime ? new Date(Number(incident.endTime) || incident.endTime) : null,
- incident.status ?? true,
- incident.message ?? null,
- incident.statusCode ?? null,
- incident.resolutionType ?? null,
- incident.resolvedBy ?? null,
- incident.resolvedByEmail ?? null,
- incident.comment ?? null,
- ]
- );
- const row = result.rows[0];
- if (!row) {
- throw new AppError({ message: "Failed to create incident", status: 500 });
- }
- return this.toEntity(row);
- };
-
- findById = async (incidentId: string, teamId: string): Promise => {
- const result = await this.pool.query(`SELECT ${COLUMNS} FROM incidents WHERE id = $1 AND team_id = $2`, [incidentId, teamId]);
- const row = result.rows[0];
- if (!row) {
- throw new AppError({ message: `Incident with id ${incidentId} not found`, status: 404 });
- }
- return this.toEntity(row);
- };
-
- findActiveByIncidentId = async (incidentId: string, teamId: string): Promise => {
- const result = await this.pool.query(`SELECT ${COLUMNS} FROM incidents WHERE id = $1 AND team_id = $2 AND status = TRUE`, [
- incidentId,
- teamId,
- ]);
- const row = result.rows[0];
- return row ? this.toEntity(row) : null;
- };
-
- findActiveByMonitorId = async (monitorId: string, teamId: string): Promise => {
- const result = await this.pool.query(`SELECT ${COLUMNS} FROM incidents WHERE monitor_id = $1 AND team_id = $2 AND status = TRUE`, [
- monitorId,
- teamId,
- ]);
- const row = result.rows[0];
- return row ? this.toEntity(row) : null;
- };
-
- findByTeamId = async (
- teamId: string,
- startDate: Date | undefined,
- page: number,
- rowsPerPage: number,
- sortOrder?: string,
- status?: boolean,
- monitorId?: string,
- resolutionType?: string
- ): Promise => {
- const { conditions, values, paramIndex } = this.buildWhere(teamId, startDate, status, monitorId, resolutionType);
- const direction = sortOrder === "asc" ? "ASC" : "DESC";
- const offset = page * rowsPerPage;
-
- const result = await this.pool.query(
- `SELECT ${COLUMNS} FROM incidents
- WHERE ${conditions.join(" AND ")}
- ORDER BY created_at ${direction}
- LIMIT $${paramIndex} OFFSET $${paramIndex + 1}`,
- [...values, rowsPerPage, offset]
- );
- return result.rows.map(this.toEntity);
- };
-
- countByTeamId = async (
- teamId: string,
- startDate: Date | undefined,
- status?: boolean,
- monitorId?: string,
- resolutionType?: string
- ): Promise => {
- const { conditions, values } = this.buildWhere(teamId, startDate, status, monitorId, resolutionType);
- const result = await this.pool.query(`SELECT COUNT(*)::int AS count FROM incidents WHERE ${conditions.join(" AND ")}`, values);
- return result.rows[0].count;
- };
-
- findSummaryByTeamId = async (teamId: string, limit?: number): Promise => {
- // Counts by status and resolution type
- const countsResult = await this.pool.query(
- `SELECT
- COUNT(*)::int AS total,
- COUNT(*) FILTER (WHERE status = TRUE)::int AS active,
- COUNT(*) FILTER (WHERE resolution_type = 'manual')::int AS manual,
- COUNT(*) FILTER (WHERE resolution_type = 'automatic')::int AS automatic
- FROM incidents WHERE team_id = $1`,
- [teamId]
- );
- const counts = countsResult.rows[0] ?? { total: 0, active: 0, manual: 0, automatic: 0 };
-
- // Average resolution time
- const resTimeResult = await this.pool.query(
- `SELECT AVG(EXTRACT(EPOCH FROM (end_time - start_time))) AS avg_seconds
- FROM incidents
- WHERE team_id = $1 AND status = FALSE AND end_time IS NOT NULL`,
- [teamId]
- );
- const avgSeconds = resTimeResult.rows[0]?.avg_seconds ?? 0;
- const avgResolutionTimeHours = Math.round((avgSeconds / 3600) * 100) / 100;
-
- // Top monitor by incident count
- const topMonitorResult = await this.pool.query(
- `SELECT i.monitor_id, m.name AS monitor_name, COUNT(*)::int AS count
- FROM incidents i
- LEFT JOIN monitors m ON m.id = i.monitor_id
- WHERE i.team_id = $1
- GROUP BY i.monitor_id, m.name
- ORDER BY count DESC
- LIMIT 1`,
- [teamId]
- );
-
- // Latest incidents
- const latestLimit = Math.max(1, Number.isFinite(Number(limit)) ? Number(limit) : 10);
- const latestResult = await this.pool.query(
- `SELECT i.id, i.monitor_id, m.name AS monitor_name, i.status, i.start_time, i.end_time,
- i.resolution_type, i.message, i.status_code, i.created_at
- FROM incidents i
- LEFT JOIN monitors m ON m.id = i.monitor_id
- WHERE i.team_id = $1
- ORDER BY i.created_at DESC
- LIMIT $2`,
- [teamId, latestLimit]
- );
-
- const topRow = topMonitorResult.rows[0];
-
- return {
- total: counts.total,
- totalActive: counts.active,
- totalManualResolutions: counts.manual,
- totalAutomaticResolutions: counts.automatic,
- avgResolutionTimeHours,
- topMonitor: topRow
- ? {
- monitorId: topRow.monitor_id,
- monitorName: topRow.monitor_name ?? null,
- incidentCount: topRow.count,
- }
- : null,
- latestIncidents: latestResult.rows.map((row) => ({
- id: row.id,
- monitorId: row.monitor_id,
- monitorName: row.monitor_name ?? null,
- status: row.status,
- startTime: row.start_time.toISOString(),
- endTime: row.end_time ? row.end_time.toISOString() : null,
- resolutionType: row.resolution_type ?? null,
- message: row.message ?? null,
- statusCode: row.status_code ?? null,
- createdAt: row.created_at.toISOString(),
- })),
- };
- };
-
- updateById = async (incidentId: string, teamId: string, patch: Partial): Promise => {
- const sets: string[] = [];
- const values: unknown[] = [];
- let paramIndex = 1;
-
- const fieldMap: [keyof Incident, string][] = [
- ["status", "status"],
- ["message", "message"],
- ["statusCode", "status_code"],
- ["endTime", "end_time"],
- ["resolutionType", "resolution_type"],
- ["resolvedBy", "resolved_by"],
- ["resolvedByEmail", "resolved_by_email"],
- ["comment", "comment"],
- ];
-
- for (const [key, column] of fieldMap) {
- if (patch[key] !== undefined) {
- const value = key === "endTime" && patch[key] ? new Date(Number(patch[key]) || (patch[key] as string)) : patch[key];
- sets.push(`${column} = $${paramIndex++}`);
- values.push(value);
- }
- }
-
- if (sets.length === 0) {
- return this.findById(incidentId, teamId);
- }
-
- sets.push(`updated_at = NOW()`);
- values.push(incidentId, teamId);
-
- const result = await this.pool.query(
- `UPDATE incidents SET ${sets.join(", ")} WHERE id = $${paramIndex++} AND team_id = $${paramIndex}
- RETURNING ${COLUMNS}`,
- values
- );
- const row = result.rows[0];
- if (!row) {
- throw new AppError({ message: `Failed to update incident with id ${incidentId}`, status: 500 });
- }
- return this.toEntity(row);
- };
-
- deleteByMonitorId = async (monitorId: string, teamId: string): Promise => {
- const result = await this.pool.query(`DELETE FROM incidents WHERE monitor_id = $1 AND team_id = $2`, [monitorId, teamId]);
- return result.rowCount ?? 0;
- };
-
- deleteByMonitorIdsNotIn = async (monitorIds: string[]): Promise => {
- if (!monitorIds.length) {
- const result = await this.pool.query(`DELETE FROM incidents`);
- return result.rowCount ?? 0;
- }
- const result = await this.pool.query(`DELETE FROM incidents WHERE monitor_id != ALL($1)`, [monitorIds]);
- return result.rowCount ?? 0;
- };
-
- private buildWhere = (teamId: string, startDate: Date | undefined, status?: boolean, monitorId?: string, resolutionType?: string) => {
- const conditions: string[] = ["team_id = $1"];
- const values: unknown[] = [teamId];
- let paramIndex = 2;
-
- if (startDate) {
- conditions.push(`created_at >= $${paramIndex++}`);
- values.push(startDate);
- }
- if (status !== undefined) {
- conditions.push(`status = $${paramIndex++}`);
- values.push(status);
- }
- if (monitorId) {
- conditions.push(`monitor_id = $${paramIndex++}`);
- values.push(monitorId);
- }
- if (resolutionType) {
- conditions.push(`resolution_type = $${paramIndex++}`);
- values.push(resolutionType);
- }
-
- return { conditions, values, paramIndex };
- };
-
- private toEntity = (row: IncidentRow): Incident => ({
- id: row.id,
- monitorId: row.monitor_id,
- teamId: row.team_id,
- startTime: row.start_time.toISOString(),
- endTime: row.end_time ? row.end_time.toISOString() : null,
- status: row.status,
- message: row.message ?? null,
- statusCode: row.status_code ?? null,
- resolutionType: row.resolution_type ?? null,
- resolvedBy: row.resolved_by ?? null,
- resolvedByEmail: row.resolved_by_email ?? null,
- comment: row.comment ?? null,
- createdAt: row.created_at.toISOString(),
- updatedAt: row.updated_at.toISOString(),
- });
-}
diff --git a/server/src/repositories/index.ts b/server/src/repositories/index.ts
index 84a9a7cda..fc5eefee9 100644
--- a/server/src/repositories/index.ts
+++ b/server/src/repositories/index.ts
@@ -1,54 +1,41 @@
export * from "@/repositories/monitors/IMonitorsRepository.js";
export { default as MongoMonitorsRepository } from "@/repositories/monitors/MongoMonitorsRepository.js";
-export { TimescaleMonitorsRepository } from "@/repositories/monitors/TimescaleMonitorsRepository.js";
export * from "@/repositories/checks/IChecksRepository.js";
export { default as MongoChecksRepository } from "@/repositories/checks/MongoChecksRepistory.js";
-export { TimescaleChecksRepository } from "@/repositories/checks/TimescaleChecksRepository.js";
export * from "@/repositories/monitor-stats/IMonitorStatsRepository.js";
export { default as MongoMonitorStatsRepository } from "@/repositories/monitor-stats/MongoMonitorStatsRepository.js";
-export { TimescaleMonitorStatsRepository } from "@/repositories/monitor-stats/TimescaleMonitorStatsRepository.js";
export * from "@/repositories/status-pages/IStatusPagesRepository.js";
export { default as MongoStatusPagesRepository } from "@/repositories/status-pages/MongoStatusPagesRepository.js";
-export { TimescaleStatusPagesRepository } from "@/repositories/status-pages/TimescaleStatusPagesRepository.js";
export * from "@/repositories/users/IUsersRepository.js";
export { default as MongoUsersRepository } from "@/repositories/users/MongoUsersRepository.js";
-export { TimescaleUsersRepository } from "@/repositories/users/TimescaleUsersRepository.js";
export * from "@/repositories/invites/IInvitesRepository.js";
export { default as MongoInvitesRepository } from "@/repositories/invites/MongoInviteRepository.js";
-export { TimescaleInvitesRepository } from "@/repositories/invites/TimescaleInvitesRepository.js";
export * from "@/repositories/recovery-tokens/IRecoveryTokensRepository.js";
export { default as MongoRecoveryTokensRepository } from "@/repositories/recovery-tokens/MongoRecoveryTokensRepository.js";
-export { TimescaleRecoveryTokensRepository } from "@/repositories/recovery-tokens/TimescaleRecoveryTokensRepository.js";
export * from "@/repositories/settings/ISettingsRepository.js";
export { default as MongoSettingsRepository } from "@/repositories/settings/MongoSettingsRepository.js";
-export { TimescaleSettingsRepository } from "@/repositories/settings/TimescaleSettingsRepository.js";
export * from "@/repositories/notifications/INotificationsRepository.js";
export { default as MongoNotificationsRepository } from "@/repositories/notifications/MongoNotificationsRepository.js";
-export { TimescaleNotificationsRepository } from "@/repositories/notifications/TimescaleNotificationsRepository.js";
export * from "@/repositories/incidents/IIncidentsRepository.js";
export { default as MongoIncidentsRepository } from "@/repositories/incidents/MongoIncidentsRepository.js";
-export { TimescaleIncidentsRepository } from "@/repositories/incidents/TimescaleIncidentsRepository.js";
export * from "@/repositories/teams/ITeamsRepository.js";
export { default as MongoTeamsRepository } from "@/repositories/teams/MongoTeamsRepository.js";
-export { TimescaleTeamsRepository } from "@/repositories/teams/TimescaleTeamsRepository.js";
export * from "@/repositories/maintenance-windows/IMaintenanceWindowsRepository.js";
export { default as MongoMaintenanceWindowsRepository } from "@/repositories/maintenance-windows/MongoMaintenanceWindowsRepository.js";
-export { TimescaleMaintenanceWindowsRepository } from "@/repositories/maintenance-windows/TimescaleMaintenanceWindowsRepository.js";
export * from "@/repositories/geo-checks/IGeoChecksRepository.js";
export { default as MongoGeoChecksRepository } from "@/repositories/geo-checks/MongoGeoChecksRepository.js";
export * from "@/repositories/dlq/IDLQRepository.js";
export { default as MongoDLQRepository } from "@/repositories/dlq/MongoDLQRepository.js";
-export { TimescaleGeoChecksRepository } from "@/repositories/geo-checks/TimescaleGeoChecksRepository.js";
diff --git a/server/src/repositories/invites/TimescaleInvitesRepository.ts b/server/src/repositories/invites/TimescaleInvitesRepository.ts
deleted file mode 100644
index 853047745..000000000
--- a/server/src/repositories/invites/TimescaleInvitesRepository.ts
+++ /dev/null
@@ -1,77 +0,0 @@
-import type { Pool } from "pg";
-import crypto from "crypto";
-import type { IInvitesRepository } from "@/repositories/invites/IInvitesRepository.js";
-import type { Invite } from "@/types/invite.js";
-import type { UserRole } from "@/types/user.js";
-import { AppError } from "@/utils/AppError.js";
-
-const parsePostgresArray = (value: unknown): UserRole[] => {
- if (typeof value !== "string") return [];
- const inner = value.replace(/^\{|\}$/g, "");
- if (inner === "") return [];
- return inner.split(",") as UserRole[];
-};
-
-interface InviteRow {
- id: string;
- email: string;
- team_id: string;
- roles: UserRole[];
- token: string;
- expiry: Date | null;
- created_at: Date;
- updated_at: Date;
-}
-
-const COLUMNS = `id, email, team_id, roles, token, expiry, created_at, updated_at`;
-
-export class TimescaleInvitesRepository implements IInvitesRepository {
- constructor(private pool: Pool) {}
-
- create = async (invite: Partial): Promise => {
- // Delete existing invites for this email
- await this.pool.query(`DELETE FROM invites WHERE email = $1`, [invite.email]);
-
- const token = crypto.randomBytes(32).toString("hex");
- const result = await this.pool.query(
- `INSERT INTO invites (email, team_id, roles, token, expiry)
- VALUES ($1, $2, $3, $4, $5)
- RETURNING ${COLUMNS}`,
- [invite.email, invite.teamId, invite.role ?? ["user"], token, invite.expiry ? new Date(invite.expiry) : null]
- );
- const row = result.rows[0];
- if (!row) {
- throw new AppError({ message: "Failed to create invite", status: 500 });
- }
- return this.toEntity(row);
- };
-
- findByToken = async (token: string): Promise => {
- const result = await this.pool.query(`SELECT ${COLUMNS} FROM invites WHERE token = $1`, [token]);
- const row = result.rows[0];
- if (!row) {
- throw new AppError({ message: "Invite not found", status: 404 });
- }
- return this.toEntity(row);
- };
-
- findByTokenAndDelete = async (token: string): Promise => {
- const result = await this.pool.query(`DELETE FROM invites WHERE token = $1 RETURNING ${COLUMNS}`, [token]);
- const row = result.rows[0];
- if (!row) {
- throw new AppError({ message: "Invite not found", status: 404 });
- }
- return this.toEntity(row);
- };
-
- private toEntity = (row: InviteRow): Invite => ({
- id: row.id,
- email: row.email,
- teamId: row.team_id,
- role: Array.isArray(row.roles) ? row.roles : parsePostgresArray(row.roles),
- token: row.token,
- expiry: row.expiry ? row.expiry.toISOString() : new Date(0).toISOString(),
- createdAt: row.created_at.toISOString(),
- updatedAt: row.updated_at.toISOString(),
- });
-}
diff --git a/server/src/repositories/maintenance-windows/TimescaleMaintenanceWindowsRepository.ts b/server/src/repositories/maintenance-windows/TimescaleMaintenanceWindowsRepository.ts
deleted file mode 100644
index cfe28198a..000000000
--- a/server/src/repositories/maintenance-windows/TimescaleMaintenanceWindowsRepository.ts
+++ /dev/null
@@ -1,197 +0,0 @@
-import type { Pool } from "pg";
-import type { IMaintenanceWindowsRepository } from "./IMaintenanceWindowsRepository.js";
-import type { MaintenanceWindow, DurationUnit } from "@/types/maintenanceWindow.js";
-import { AppError } from "@/utils/AppError.js";
-
-interface MaintenanceWindowRow {
- id: string;
- monitor_id: string;
- team_id: string;
- active: boolean;
- name: string;
- duration: number;
- duration_unit: DurationUnit;
- repeat: number;
- start_time: Date;
- end_time: Date;
- expiry: Date | null;
- created_at: Date;
- updated_at: Date;
-}
-
-const COLUMNS = `id, monitor_id, team_id, active, name, duration, duration_unit, repeat,
- start_time, end_time, expiry, created_at, updated_at`;
-
-export class TimescaleMaintenanceWindowsRepository implements IMaintenanceWindowsRepository {
- constructor(private pool: Pool) {}
-
- create = async (data: Partial): Promise => {
- const startTime = data.start ? new Date(data.start) : null;
- const endTime = data.end ? new Date(data.end) : null;
- // One-time windows expire at end time
- const expiry = data.repeat === 0 ? endTime : null;
-
- const result = await this.pool.query(
- `INSERT INTO maintenance_windows (monitor_id, team_id, active, name, duration, duration_unit, repeat, start_time, end_time, expiry)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
- RETURNING ${COLUMNS}`,
- [
- data.monitorId,
- data.teamId,
- data.active ?? true,
- data.name ?? null,
- data.duration ?? null,
- data.durationUnit ?? null,
- data.repeat ?? 0,
- startTime,
- endTime,
- expiry,
- ]
- );
- const row = result.rows[0];
- if (!row) {
- throw new AppError({ message: "Failed to create maintenance window", status: 500 });
- }
- return this.toEntity(row);
- };
-
- findById = async (id: string, teamId: string): Promise => {
- const result = await this.pool.query(`SELECT ${COLUMNS} FROM maintenance_windows WHERE id = $1 AND team_id = $2`, [
- id,
- teamId,
- ]);
- const row = result.rows[0];
- if (!row) {
- throw new AppError({ message: "Maintenance Window not found", status: 404 });
- }
- return this.toEntity(row);
- };
-
- findByMonitorId = async (monitorId: string, teamId: string): Promise => {
- const result = await this.pool.query(`SELECT ${COLUMNS} FROM maintenance_windows WHERE monitor_id = $1 AND team_id = $2`, [
- monitorId,
- teamId,
- ]);
- return result.rows.map(this.toEntity);
- };
-
- findByTeamId = async (
- teamId: string,
- page: number,
- rowsPerPage: number,
- field?: string,
- order?: string,
- active?: boolean
- ): Promise => {
- const conditions: string[] = ["team_id = $1"];
- const values: unknown[] = [teamId];
- let paramIndex = 2;
-
- if (active !== undefined) {
- conditions.push(`active = $${paramIndex++}`);
- values.push(active);
- }
-
- const fieldMap: Record = {
- createdAt: "created_at",
- name: "name",
- start: "start_time",
- end: "end_time",
- active: "active",
- };
- const sortColumn = field ? (fieldMap[field] ?? "created_at") : "created_at";
- const sortDirection = order === "asc" ? "ASC" : "DESC";
-
- const offset = page && rowsPerPage ? page * rowsPerPage : 0;
-
- const result = await this.pool.query(
- `SELECT ${COLUMNS} FROM maintenance_windows
- WHERE ${conditions.join(" AND ")}
- ORDER BY ${sortColumn} ${sortDirection}
- LIMIT $${paramIndex++} OFFSET $${paramIndex}`,
- [...values, rowsPerPage, offset]
- );
- return result.rows.map(this.toEntity);
- };
-
- updateById = async (id: string, teamId: string, patch: Partial): Promise => {
- const sets: string[] = [];
- const values: unknown[] = [];
- let paramIndex = 1;
-
- const fieldMap: [keyof MaintenanceWindow, string, boolean][] = [
- ["active", "active", false],
- ["name", "name", false],
- ["duration", "duration", false],
- ["durationUnit", "duration_unit", false],
- ["repeat", "repeat", false],
- ["start", "start_time", true],
- ["end", "end_time", true],
- ];
-
- for (const [key, column, isDate] of fieldMap) {
- if (patch[key] !== undefined) {
- sets.push(`${column} = $${paramIndex++}`);
- values.push(isDate ? new Date(patch[key] as string) : patch[key]);
- }
- }
-
- if (sets.length === 0) {
- return this.findById(id, teamId);
- }
-
- sets.push(`updated_at = NOW()`);
- values.push(id, teamId);
-
- const result = await this.pool.query(
- `UPDATE maintenance_windows SET ${sets.join(", ")} WHERE id = $${paramIndex++} AND team_id = $${paramIndex}
- RETURNING ${COLUMNS}`,
- values
- );
- const row = result.rows[0];
- if (!row) {
- throw new AppError({ message: "Maintenance window not found or could not be updated", status: 404 });
- }
- return this.toEntity(row);
- };
-
- deleteById = async (id: string, teamId: string): Promise => {
- const result = await this.pool.query(
- `DELETE FROM maintenance_windows WHERE id = $1 AND team_id = $2 RETURNING ${COLUMNS}`,
- [id, teamId]
- );
- const row = result.rows[0];
- if (!row) {
- throw new AppError({ message: "Maintenance window not found or could not be deleted", status: 404 });
- }
- return this.toEntity(row);
- };
-
- countByTeamId = async (teamId: string, active?: boolean): Promise => {
- const conditions: string[] = ["team_id = $1"];
- const values: unknown[] = [teamId];
-
- if (active !== undefined) {
- conditions.push(`active = $2`);
- values.push(active);
- }
-
- const result = await this.pool.query(`SELECT COUNT(*)::int AS count FROM maintenance_windows WHERE ${conditions.join(" AND ")}`, values);
- return result.rows[0].count;
- };
-
- private toEntity = (row: MaintenanceWindowRow): MaintenanceWindow => ({
- id: row.id,
- monitorId: row.monitor_id,
- teamId: row.team_id,
- active: row.active,
- name: row.name,
- duration: row.duration,
- durationUnit: row.duration_unit,
- repeat: row.repeat,
- start: row.start_time.toISOString(),
- end: row.end_time.toISOString(),
- createdAt: row.created_at.toISOString(),
- updatedAt: row.updated_at.toISOString(),
- });
-}
diff --git a/server/src/repositories/monitor-stats/TimescaleMonitorStatsRepository.ts b/server/src/repositories/monitor-stats/TimescaleMonitorStatsRepository.ts
deleted file mode 100644
index 308dc1b64..000000000
--- a/server/src/repositories/monitor-stats/TimescaleMonitorStatsRepository.ts
+++ /dev/null
@@ -1,163 +0,0 @@
-import type { Pool } from "pg";
-import type { IMonitorStatsRepository } from "./IMonitorStatsRepository.js";
-import type { MonitorStats, CheckResultInput } from "@/types/monitorStats.js";
-import { AppError } from "@/utils/AppError.js";
-
-interface MonitorStatsRow {
- id: string;
- monitor_id: string;
- avg_response_time: number;
- max_response_time: number;
- total_checks: number;
- total_up_checks: number;
- total_down_checks: number;
- uptime_percentage: number;
- last_check_timestamp: Date | null;
- last_response_time: number;
- time_of_last_failure: Date | null;
- created_at: Date;
- updated_at: Date;
-}
-
-const COLUMNS = `id, monitor_id, avg_response_time, max_response_time, total_checks, total_up_checks,
- total_down_checks, uptime_percentage, last_check_timestamp, last_response_time, time_of_last_failure,
- created_at, updated_at`;
-
-export class TimescaleMonitorStatsRepository implements IMonitorStatsRepository {
- constructor(private pool: Pool) {}
-
- create = async (data: Omit): Promise => {
- const result = await this.pool.query(
- `INSERT INTO monitor_stats (monitor_id, avg_response_time, max_response_time, total_checks, total_up_checks,
- total_down_checks, uptime_percentage, last_check_timestamp, last_response_time, time_of_last_failure)
- VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
- RETURNING ${COLUMNS}`,
- [
- data.monitorId,
- data.avgResponseTime,
- data.maxResponseTime,
- data.totalChecks,
- data.totalUpChecks,
- data.totalDownChecks,
- data.uptimePercentage,
- data.lastCheckTimestamp ? new Date(data.lastCheckTimestamp) : null,
- data.lastResponseTime,
- data.timeOfLastFailure ? new Date(data.timeOfLastFailure) : null,
- ]
- );
- const row = result.rows[0];
- if (!row) {
- throw new AppError({ message: "Failed to create monitor stats", status: 500 });
- }
- return this.toEntity(row);
- };
-
- findByMonitorId = async (monitorId: string): Promise => {
- const result = await this.pool.query(`SELECT ${COLUMNS} FROM monitor_stats WHERE monitor_id = $1`, [monitorId]);
- const row = result.rows[0];
- if (!row) {
- throw new AppError({ message: "Monitor stats not found", status: 404 });
- }
- return this.toEntity(row);
- };
-
- updateByMonitorId = async (monitorId: string, result: CheckResultInput): Promise => {
- const { status, responseTime, now } = result;
- const upDelta = status ? 1 : 0;
- const downDelta = status ? 0 : 1;
- const nowTs = new Date(now);
-
- // Atomic upsert via INSERT ... ON CONFLICT DO UPDATE
- const queryResult = await this.pool.query(
- `INSERT INTO monitor_stats (
- monitor_id,
- avg_response_time,
- max_response_time,
- total_checks,
- total_up_checks,
- total_down_checks,
- uptime_percentage,
- last_check_timestamp,
- last_response_time,
- time_of_last_failure
- ) VALUES (
- $1,
- $2,
- $2,
- 1,
- $3,
- $4,
- $3::float,
- $5,
- $2,
- CASE WHEN $6::boolean THEN $5 ELSE NULL END
- )
- ON CONFLICT (monitor_id) DO UPDATE SET
- total_checks = monitor_stats.total_checks + 1,
- total_up_checks = monitor_stats.total_up_checks + $3,
- total_down_checks = monitor_stats.total_down_checks + $4,
- avg_response_time = (monitor_stats.avg_response_time * monitor_stats.total_checks + $2)
- / (monitor_stats.total_checks + 1),
- uptime_percentage = (monitor_stats.total_up_checks + $3)::float
- / (monitor_stats.total_checks + 1),
- max_response_time = GREATEST(monitor_stats.max_response_time, $2),
- last_response_time = $2,
- last_check_timestamp = $5,
- time_of_last_failure = CASE
- WHEN $6::boolean AND monitor_stats.time_of_last_failure IS NULL THEN $5
- WHEN $6::boolean THEN monitor_stats.time_of_last_failure
- ELSE NULL
- END,
- updated_at = NOW()
- RETURNING ${COLUMNS}`,
- [monitorId, responseTime, upDelta, downDelta, nowTs, status]
- );
- const row = queryResult.rows[0];
- if (!row) {
- throw new AppError({ message: "Failed to apply check result to monitor stats", status: 500 });
- }
- return this.toEntity(row);
- };
-
- deleteByMonitorId = async (monitorId: string): Promise => {
- const result = await this.pool.query(`DELETE FROM monitor_stats WHERE monitor_id = $1 RETURNING ${COLUMNS}`, [monitorId]);
- const row = result.rows[0];
- if (!row) {
- throw new AppError({ message: "Monitor stats not found", status: 404 });
- }
- return this.toEntity(row);
- };
-
- deleteByMonitorIds = async (monitorIds: string[]): Promise => {
- if (!monitorIds.length) {
- return 0;
- }
- const result = await this.pool.query(`DELETE FROM monitor_stats WHERE monitor_id = ANY($1)`, [monitorIds]);
- return result.rowCount ?? 0;
- };
-
- deleteByMonitorIdsNotIn = async (monitorIds: string[]): Promise => {
- if (!monitorIds.length) {
- const result = await this.pool.query(`DELETE FROM monitor_stats`);
- return result.rowCount ?? 0;
- }
- const result = await this.pool.query(`DELETE FROM monitor_stats WHERE monitor_id != ALL($1)`, [monitorIds]);
- return result.rowCount ?? 0;
- };
-
- private toEntity = (row: MonitorStatsRow): MonitorStats => ({
- id: row.id,
- monitorId: row.monitor_id,
- avgResponseTime: row.avg_response_time,
- maxResponseTime: row.max_response_time,
- totalChecks: Number(row.total_checks),
- totalUpChecks: Number(row.total_up_checks),
- totalDownChecks: Number(row.total_down_checks),
- uptimePercentage: row.uptime_percentage,
- lastCheckTimestamp: row.last_check_timestamp ? row.last_check_timestamp.getTime() : 0,
- lastResponseTime: row.last_response_time,
- timeOfLastFailure: row.time_of_last_failure ? row.time_of_last_failure.getTime() : undefined,
- createdAt: row.created_at.toISOString(),
- updatedAt: row.updated_at.toISOString(),
- });
-}
diff --git a/server/src/repositories/monitors/TimescaleMonitorsRepository.ts b/server/src/repositories/monitors/TimescaleMonitorsRepository.ts
deleted file mode 100644
index 7cd0aa4f0..000000000
--- a/server/src/repositories/monitors/TimescaleMonitorsRepository.ts
+++ /dev/null
@@ -1,1093 +0,0 @@
-import type { Pool } from "pg";
-import type { Monitor, MonitorsSummary, MonitorStatus, MonitorType, MonitorMatchMethod, GeoContinent, CheckSnapshot } from "@/types/monitor.js";
-import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "./IMonitorsRepository.js";
-import { AppError } from "@/utils/AppError.js";
-
-interface MonitorRow {
- id: string;
- user_id: string;
- team_id: string;
- name: string;
- description: string | null;
- type: MonitorType;
- status: MonitorStatus;
- url: string | null;
- port: number | null;
- ignore_tls_errors: boolean;
- use_advanced_matching: boolean;
- json_path: string | null;
- expected_value: string | null;
- match_method: MonitorMatchMethod | null;
- secret: string | null;
- interval_ms: number;
- is_active: boolean;
- status_window: boolean[] | null;
- status_window_size: number;
- status_window_threshold: number;
- uptime_percentage: number | null;
- cpu_alert_threshold: number;
- cpu_alert_counter: number;
- memory_alert_threshold: number;
- memory_alert_counter: number;
- disk_alert_threshold: number;
- disk_alert_counter: number;
- temp_alert_threshold: number;
- temp_alert_counter: number;
- selected_disks: string[] | null;
- game_id: string | null;
- grpc_service_name: string | null;
- monitor_group: string | null;
- geo_check_enabled: boolean;
- geo_check_locations: GeoContinent[] | null;
- geo_check_interval_ms: number;
- created_at: Date;
- updated_at: Date;
-}
-
-const MONITOR_COLUMNS = `id, user_id, team_id, name, description, type, status, url, port,
- ignore_tls_errors, use_advanced_matching, json_path, expected_value, match_method, secret,
- interval_ms, is_active, status_window, status_window_size, status_window_threshold, uptime_percentage,
- cpu_alert_threshold, cpu_alert_counter, memory_alert_threshold, memory_alert_counter,
- disk_alert_threshold, disk_alert_counter, temp_alert_threshold, temp_alert_counter, selected_disks,
- game_id, grpc_service_name, monitor_group, geo_check_enabled, geo_check_locations, geo_check_interval_ms,
- created_at, updated_at`;
-
-export class TimescaleMonitorsRepository implements IMonitorsRepository {
- constructor(private pool: Pool) {}
-
- create = async (monitor: Monitor, teamId: string, userId: string): Promise => {
- const result = await this.pool.query(
- `INSERT INTO monitors (user_id, team_id, name, description, type, status, url, port,
- ignore_tls_errors, use_advanced_matching, json_path, expected_value, match_method, secret,
- interval_ms, is_active, status_window, status_window_size, status_window_threshold,
- cpu_alert_threshold, cpu_alert_counter, memory_alert_threshold, memory_alert_counter,
- disk_alert_threshold, disk_alert_counter, temp_alert_threshold, temp_alert_counter, selected_disks,
- game_id, grpc_service_name, monitor_group, geo_check_enabled, geo_check_locations, geo_check_interval_ms)
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17,$18,$19,$20,$21,$22,$23,$24,$25,$26,$27,$28,$29,$30,$31,$32,$33,$34)
- RETURNING ${MONITOR_COLUMNS}`,
- [
- userId,
- teamId,
- monitor.name,
- monitor.description ?? null,
- monitor.type,
- monitor.status ?? "initializing",
- monitor.url ?? null,
- monitor.port ?? null,
- monitor.ignoreTlsErrors ?? false,
- monitor.useAdvancedMatching ?? false,
- monitor.jsonPath ?? null,
- monitor.expectedValue ?? null,
- monitor.matchMethod || null,
- monitor.secret ?? null,
- monitor.interval ?? 60000,
- monitor.isActive ?? true,
- monitor.statusWindow ?? null,
- monitor.statusWindowSize ?? 5,
- monitor.statusWindowThreshold ?? 60,
- monitor.cpuAlertThreshold ?? 0,
- monitor.cpuAlertCounter ?? 0,
- monitor.memoryAlertThreshold ?? 0,
- monitor.memoryAlertCounter ?? 0,
- monitor.diskAlertThreshold ?? 0,
- monitor.diskAlertCounter ?? 0,
- monitor.tempAlertThreshold ?? 0,
- monitor.tempAlertCounter ?? 0,
- monitor.selectedDisks ?? [],
- monitor.gameId ?? null,
- monitor.grpcServiceName ?? null,
- monitor.group ?? null,
- monitor.geoCheckEnabled ?? false,
- monitor.geoCheckLocations ?? [],
- monitor.geoCheckInterval ?? 300000,
- ]
- );
- const row = result.rows[0];
- if (!row) {
- return null;
- }
-
- // Insert notification associations
- if (monitor.notifications?.length) {
- for (const notificationId of monitor.notifications) {
- await this.pool.query(`INSERT INTO monitor_notifications (monitor_id, notification_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`, [
- row.id,
- notificationId,
- ]);
- }
- }
-
- const entity = this.toEntity(row);
- entity.notifications = monitor.notifications ?? [];
- return entity;
- };
-
- createMonitors = async (monitors: Monitor[]): Promise => {
- if (!monitors.length) {
- return [];
- }
- const created: Monitor[] = [];
- for (const monitor of monitors) {
- const result = await this.create(monitor, monitor.teamId, monitor.userId);
- if (result) {
- created.push(result);
- }
- }
- return created;
- };
-
- findById = async (monitorId: string, teamId: string): Promise => {
- const result = await this.pool.query(`SELECT ${MONITOR_COLUMNS} FROM monitors WHERE id = $1 AND team_id = $2`, [monitorId, teamId]);
- const row = result.rows[0];
- if (!row) {
- throw new AppError({ message: `Monitor with ID ${monitorId} not found`, status: 404 });
- }
- const monitor = this.toEntity(row);
-
- // Populate latest check with child data
- const checkResult = await this.pool.query(`SELECT * FROM checks WHERE monitor_id = $1 ORDER BY created_at DESC LIMIT 1`, [monitorId]);
- if (checkResult.rows[0]) {
- const checkRow = checkResult.rows[0];
- const childData = await this.fetchCheckChildData(checkRow.id);
- monitor.recentChecks = [this.toCheckSnapshot(checkRow, childData)];
- }
-
- // Populate notifications
- monitor.notifications = await this.fetchNotificationIds([monitorId]).then((m) => m.get(monitorId) ?? []);
-
- return monitor;
- };
-
- findAll = async (): Promise => {
- const result = await this.pool.query(`SELECT ${MONITOR_COLUMNS} FROM monitors`);
- const monitors = result.rows.map(this.toEntity);
- if (monitors.length > 0) {
- const notifMap = await this.fetchNotificationIds(monitors.map((m) => m.id));
- for (const monitor of monitors) {
- monitor.notifications = notifMap.get(monitor.id) ?? [];
- }
- }
- return monitors;
- };
-
- findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise => {
- const { page = 0, rowsPerPage = 0, filter, field = "createdAt", order = "desc", type } = config ?? {};
-
- const conditions: string[] = ["team_id = $1"];
- const values: unknown[] = [teamId];
- let paramIndex = 2;
-
- if (type !== undefined) {
- if (Array.isArray(type)) {
- conditions.push(`type = ANY($${paramIndex++})`);
- values.push(type);
- } else {
- conditions.push(`type = $${paramIndex++}`);
- values.push(type);
- }
- }
-
- if (filter !== undefined) {
- switch (field) {
- case "name":
- conditions.push(`(name ILIKE $${paramIndex} OR url ILIKE $${paramIndex})`);
- values.push(`%${filter}%`);
- paramIndex++;
- break;
- case "isActive":
- conditions.push(`is_active = $${paramIndex++}`);
- values.push(filter === "true");
- break;
- case "status":
- conditions.push(`status = $${paramIndex++}`);
- values.push(filter);
- break;
- case "type":
- conditions.push(`type = $${paramIndex++}`);
- values.push(filter);
- break;
- default:
- break;
- }
- }
-
- const fieldMap: Record = {
- createdAt: "created_at",
- name: "name",
- status: "status",
- type: "type",
- isActive: "is_active",
- };
- const sortColumn = fieldMap[field] ?? "created_at";
- const sortDirection = order === "asc" ? "ASC" : "DESC";
-
- let query = `SELECT ${MONITOR_COLUMNS} FROM monitors WHERE ${conditions.join(" AND ")} ORDER BY ${sortColumn} ${sortDirection}`;
-
- if (rowsPerPage > 0) {
- const offset = Math.max(page, 0) * rowsPerPage;
- query += ` LIMIT $${paramIndex++} OFFSET $${paramIndex++}`;
- values.push(rowsPerPage, offset);
- }
-
- const result = await this.pool.query(query, values);
- const monitors = result.rows.map(this.toEntity);
-
- if (monitors.length === 0) return monitors;
-
- // Populate recentChecks — 25 latest checks per monitor in a single query
- const monitorIds = monitors.map((m) => m.id);
- const checksResult = await this.pool.query(
- `SELECT * FROM (
- SELECT *, ROW_NUMBER() OVER (PARTITION BY monitor_id ORDER BY created_at DESC) AS rn
- FROM checks
- WHERE monitor_id = ANY($1)
- ) sub WHERE rn <= 25
- ORDER BY monitor_id, created_at ASC`,
- [monitorIds]
- );
-
- // Group checks by monitor_id
- const checksMap = new Map();
- for (const row of checksResult.rows) {
- if (!checksMap.has(row.monitor_id)) checksMap.set(row.monitor_id, []);
- checksMap.get(row.monitor_id)!.push(row);
- }
-
- // Batch fetch child data for all checks in 3 queries
- const checkIds = checksResult.rows.map((r) => r.id);
- const diskMap = new Map[]>();
- const netMap = new Map[]>();
- const errorsMap = new Map[]>();
-
- if (checkIds.length > 0) {
- const [disksResult, netsResult, errsResult] = await Promise.all([
- this.pool.query(
- `SELECT check_id, device, mountpoint, total_bytes, free_bytes, used_bytes, usage_percent,
- total_inodes, free_inodes, used_inodes, inodes_usage_percent,
- read_bytes, write_bytes, read_time, write_time
- FROM check_disks WHERE check_id = ANY($1)`,
- [checkIds]
- ),
- this.pool.query(
- `SELECT check_id, name, bytes_sent, bytes_recv, packets_sent, packets_recv,
- err_in, err_out, drop_in, drop_out, fifo_in, fifo_out
- FROM check_network_interfaces WHERE check_id = ANY($1)`,
- [checkIds]
- ),
- this.pool.query(
- `SELECT check_id, metrics, error
- FROM check_errors WHERE check_id = ANY($1)`,
- [checkIds]
- ),
- ]);
-
- for (const d of disksResult.rows) {
- const key = d.check_id as string;
- if (!diskMap.has(key)) diskMap.set(key, []);
- diskMap.get(key)!.push(d);
- }
- for (const n of netsResult.rows) {
- const key = n.check_id as string;
- if (!netMap.has(key)) netMap.set(key, []);
- netMap.get(key)!.push(n);
- }
- for (const e of errsResult.rows) {
- const key = e.check_id as string;
- if (!errorsMap.has(key)) errorsMap.set(key, []);
- errorsMap.get(key)!.push(e);
- }
- }
-
- for (const monitor of monitors) {
- const rows = checksMap.get(monitor.id) ?? [];
- monitor.recentChecks = rows.map((row) => {
- const childData = {
- disk: (diskMap.get(row.id) ?? []).map((d) => ({
- device: d.device,
- mountpoint: d.mountpoint,
- total_bytes: Number(d.total_bytes),
- free_bytes: Number(d.free_bytes),
- used_bytes: Number(d.used_bytes),
- usage_percent: d.usage_percent as number,
- total_inodes: Number(d.total_inodes),
- free_inodes: Number(d.free_inodes),
- used_inodes: Number(d.used_inodes),
- inodes_usage_percent: d.inodes_usage_percent as number,
- read_bytes: Number(d.read_bytes),
- write_bytes: Number(d.write_bytes),
- read_time: Number(d.read_time),
- write_time: Number(d.write_time),
- })),
- net: (netMap.get(row.id) ?? []).map((n) => ({
- name: n.name as string,
- bytes_sent: Number(n.bytes_sent),
- bytes_recv: Number(n.bytes_recv),
- packets_sent: Number(n.packets_sent),
- packets_recv: Number(n.packets_recv),
- err_in: Number(n.err_in),
- err_out: Number(n.err_out),
- drop_in: Number(n.drop_in),
- drop_out: Number(n.drop_out),
- fifo_in: Number(n.fifo_in),
- fifo_out: Number(n.fifo_out),
- })),
- errors: (errorsMap.get(row.id) ?? []).map((e) => ({
- metric: (e.metrics as string[]) ?? [],
- err: (e.error as string) ?? "",
- })),
- };
- return this.toCheckSnapshot(row, childData);
- });
- }
-
- // Populate notifications in batch
- const notifMap = await this.fetchNotificationIds(monitorIds);
- for (const monitor of monitors) {
- monitor.notifications = notifMap.get(monitor.id) ?? [];
- }
-
- return monitors;
- };
-
- findByIds = async (monitorIds: string[]): Promise => {
- if (!monitorIds.length) {
- return [];
- }
- const result = await this.pool.query(`SELECT ${MONITOR_COLUMNS} FROM monitors WHERE id = ANY($1)`, [monitorIds]);
- const monitors = result.rows.map(this.toEntity);
- const notifMap = await this.fetchNotificationIds(monitorIds);
- for (const monitor of monitors) {
- monitor.notifications = notifMap.get(monitor.id) ?? [];
- }
- return monitors;
- };
-
- findByIdsWithChecks = async (monitorIds: string[], checksCount: number = 25): Promise => {
- if (!monitorIds.length) {
- return [];
- }
- const monitors = await this.findByIds(monitorIds);
-
- for (const monitor of monitors) {
- const checksResult = await this.pool.query(
- `SELECT * FROM checks
- WHERE monitor_id = $1
- ORDER BY created_at DESC
- LIMIT $2`,
- [monitor.id, checksCount]
- );
- monitor.recentChecks = await Promise.all(
- checksResult.rows.map(async (row) => {
- // Fetch child records for hardware monitors
- let disk: import("@/types/index.js").CheckDiskInfo[] = [];
- let net: import("@/types/index.js").CheckNetworkInterfaceInfo[] = [];
- let errors: import("@/types/index.js").CheckErrorInfo[] = [];
-
- if (monitor.type === "hardware") {
- const [diskResult, netResult, errorsResult] = await Promise.all([
- this.pool.query(
- `SELECT device, mountpoint, total_bytes, free_bytes, used_bytes, usage_percent,
- total_inodes, free_inodes, used_inodes, inodes_usage_percent,
- read_bytes, write_bytes, read_time, write_time
- FROM check_disks WHERE check_id = $1`,
- [row.id]
- ),
- this.pool.query(
- `SELECT name, bytes_sent, bytes_recv, packets_sent, packets_recv,
- err_in, err_out, drop_in, drop_out, fifo_in, fifo_out
- FROM check_network_interfaces WHERE check_id = $1`,
- [row.id]
- ),
- this.pool.query(`SELECT metrics, error FROM check_errors WHERE check_id = $1`, [row.id]),
- ]);
- disk = diskResult.rows.map((d) => ({
- device: d.device,
- mountpoint: d.mountpoint,
- total_bytes: Number(d.total_bytes),
- free_bytes: Number(d.free_bytes),
- used_bytes: Number(d.used_bytes),
- usage_percent: d.usage_percent,
- total_inodes: Number(d.total_inodes),
- free_inodes: Number(d.free_inodes),
- used_inodes: Number(d.used_inodes),
- inodes_usage_percent: d.inodes_usage_percent,
- read_bytes: Number(d.read_bytes),
- write_bytes: Number(d.write_bytes),
- read_time: Number(d.read_time),
- write_time: Number(d.write_time),
- }));
- net = netResult.rows.map((n) => ({
- name: n.name,
- bytes_sent: Number(n.bytes_sent),
- bytes_recv: Number(n.bytes_recv),
- packets_sent: Number(n.packets_sent),
- packets_recv: Number(n.packets_recv),
- err_in: Number(n.err_in),
- err_out: Number(n.err_out),
- drop_in: Number(n.drop_in),
- drop_out: Number(n.drop_out),
- fifo_in: Number(n.fifo_in),
- fifo_out: Number(n.fifo_out),
- }));
- errors = errorsResult.rows.map((e) => ({ metric: e.metrics ?? [], err: e.error ?? "" }));
- }
-
- return {
- id: row.id,
- status: row.status,
- responseTime: row.response_time ?? 0,
- statusCode: row.status_code ?? 0,
- message: row.message ?? "",
- timings:
- row.timing_start !== null
- ? {
- start: row.timing_start,
- socket: row.timing_socket,
- lookup: row.timing_lookup,
- connect: row.timing_connect,
- secureConnect: row.timing_secure_connect,
- upload: row.timing_upload,
- response: row.timing_response,
- end: row.timing_end,
- phases: {
- wait: row.phase_wait,
- dns: row.phase_dns,
- tcp: row.phase_tcp,
- tls: row.phase_tls,
- request: row.phase_request,
- firstByte: row.phase_first_byte,
- download: row.phase_download,
- total: row.phase_total,
- },
- }
- : undefined,
- cpu:
- row.cpu_usage_percent !== null
- ? {
- physical_core: row.cpu_physical_core,
- logical_core: row.cpu_logical_core,
- frequency: row.cpu_frequency,
- current_frequency: row.cpu_current_frequency,
- temperature: row.cpu_temperature ?? [],
- free_percent: row.cpu_free_percent,
- usage_percent: row.cpu_usage_percent,
- }
- : undefined,
- memory:
- row.mem_usage_percent !== null
- ? {
- total_bytes: Number(row.mem_total_bytes),
- available_bytes: Number(row.mem_available_bytes),
- used_bytes: Number(row.mem_used_bytes),
- usage_percent: row.mem_usage_percent,
- }
- : undefined,
- disk,
- host:
- row.host_os !== null
- ? {
- os: row.host_os,
- platform: row.host_platform,
- kernel_version: row.host_kernel_version,
- pretty_name: row.host_pretty_name,
- }
- : undefined,
- errors,
- capture:
- row.capture_version !== null
- ? {
- version: row.capture_version,
- mode: row.capture_mode,
- }
- : undefined,
- net,
- performance: row.lighthouse_performance ?? undefined,
- accessibility: row.lighthouse_accessibility ?? undefined,
- bestPractices: row.lighthouse_best_practices ?? undefined,
- seo: row.lighthouse_seo ?? undefined,
- audits:
- row.audit_cls_score !== null
- ? {
- cls: { score: row.audit_cls_score, numericValue: row.audit_cls_value, displayValue: row.audit_cls_display },
- si: { score: row.audit_si_score, numericValue: row.audit_si_value, displayValue: row.audit_si_display },
- fcp: { score: row.audit_fcp_score, numericValue: row.audit_fcp_value, displayValue: row.audit_fcp_display },
- lcp: { score: row.audit_lcp_score, numericValue: row.audit_lcp_value, displayValue: row.audit_lcp_display },
- tbt: { score: row.audit_tbt_score, numericValue: row.audit_tbt_value, displayValue: row.audit_tbt_display },
- }
- : undefined,
- createdAt: row.created_at.toISOString(),
- };
- })
- );
-
- const maintResult = await this.pool.query(
- `SELECT 1 FROM maintenance_windows
- WHERE monitor_id = $1 AND active = TRUE AND start_time <= NOW() AND end_time >= NOW()
- LIMIT 1`,
- [monitor.id]
- );
- if ((maintResult.rowCount ?? 0) > 0) {
- monitor.status = "maintenance";
- }
-
- const statsResult = await this.pool.query(`SELECT uptime_percentage FROM monitor_stats WHERE monitor_id = $1`, [monitor.id]);
- if (statsResult.rows[0]) {
- monitor.uptimePercentage = statsResult.rows[0].uptime_percentage;
- }
- }
-
- return monitors;
- };
-
- findMonitorCountByTeamIdAndType = async (teamId: string, config?: TeamQueryConfig): Promise => {
- const { type } = config ?? {};
- const conditions: string[] = ["team_id = $1"];
- const values: unknown[] = [teamId];
- let paramIndex = 2;
-
- if (type !== undefined) {
- if (Array.isArray(type)) {
- conditions.push(`type = ANY($${paramIndex++})`);
- values.push(type);
- } else {
- conditions.push(`type = $${paramIndex++}`);
- values.push(type);
- }
- }
-
- const result = await this.pool.query(`SELECT COUNT(*)::int AS count FROM monitors WHERE ${conditions.join(" AND ")}`, values);
- return result.rows[0].count;
- };
-
- updateById = async (monitorId: string, teamId: string, patch: Partial): Promise => {
- const sets: string[] = [];
- const values: unknown[] = [];
- let paramIndex = 1;
-
- const fieldMap: [keyof Monitor, string][] = [
- ["name", "name"],
- ["description", "description"],
- ["type", "type"],
- ["status", "status"],
- ["url", "url"],
- ["port", "port"],
- ["ignoreTlsErrors", "ignore_tls_errors"],
- ["useAdvancedMatching", "use_advanced_matching"],
- ["jsonPath", "json_path"],
- ["expectedValue", "expected_value"],
- ["matchMethod", "match_method"],
- ["secret", "secret"],
- ["interval", "interval_ms"],
- ["isActive", "is_active"],
- ["statusWindow", "status_window"],
- ["statusWindowSize", "status_window_size"],
- ["statusWindowThreshold", "status_window_threshold"],
- ["uptimePercentage", "uptime_percentage"],
- ["cpuAlertThreshold", "cpu_alert_threshold"],
- ["cpuAlertCounter", "cpu_alert_counter"],
- ["memoryAlertThreshold", "memory_alert_threshold"],
- ["memoryAlertCounter", "memory_alert_counter"],
- ["diskAlertThreshold", "disk_alert_threshold"],
- ["diskAlertCounter", "disk_alert_counter"],
- ["tempAlertThreshold", "temp_alert_threshold"],
- ["tempAlertCounter", "temp_alert_counter"],
- ["selectedDisks", "selected_disks"],
- ["gameId", "game_id"],
- ["grpcServiceName", "grpc_service_name"],
- ["group", "monitor_group"],
- ["geoCheckEnabled", "geo_check_enabled"],
- ["geoCheckLocations", "geo_check_locations"],
- ["geoCheckInterval", "geo_check_interval_ms"],
- ];
-
- for (const [key, column] of fieldMap) {
- if (patch[key] !== undefined) {
- sets.push(`${column} = $${paramIndex++}`);
- // Empty string matchMethod is stored as NULL (not a valid enum value)
- values.push(key === "matchMethod" && patch[key] === "" ? null : patch[key]);
- }
- }
-
- if (sets.length === 0) {
- return this.findById(monitorId, teamId);
- }
-
- sets.push(`updated_at = NOW()`);
- values.push(monitorId, teamId);
-
- const result = await this.pool.query(
- `UPDATE monitors SET ${sets.join(", ")} WHERE id = $${paramIndex++} AND team_id = $${paramIndex}
- RETURNING ${MONITOR_COLUMNS}`,
- values
- );
- const row = result.rows[0];
- if (!row) {
- throw new AppError({ message: `Failed to update monitor with id ${monitorId}`, status: 500 });
- }
-
- // Update notification associations if provided
- if (patch.notifications !== undefined) {
- await this.pool.query(`DELETE FROM monitor_notifications WHERE monitor_id = $1`, [monitorId]);
- for (const notificationId of patch.notifications) {
- await this.pool.query(`INSERT INTO monitor_notifications (monitor_id, notification_id) VALUES ($1, $2) ON CONFLICT DO NOTHING`, [
- monitorId,
- notificationId,
- ]);
- }
- }
-
- const entity = this.toEntity(row);
- entity.notifications = patch.notifications ?? (await this.fetchNotificationIds([monitorId]).then((m) => m.get(monitorId) ?? []));
- return entity;
- };
-
- updateStatusWindowAndChecks = async (
- monitorId: string,
- teamId: string,
- status: boolean,
- _checkSnapshot: CheckSnapshot,
- windowSize: number,
- _maxRecentChecks: number,
- statusPatch?: Partial
- ): Promise => {
- // In TimescaleDB, recentChecks live in the checks table (inserted by the buffer service),
- // so we only need to atomically update status_window and any statusPatch fields.
- const sets: string[] = [
- `status_window = (array_append(COALESCE(status_window, ARRAY[]::boolean[]), $1))[array_length(array_append(COALESCE(status_window, ARRAY[]::boolean[]), $1), 1) - $2 + 1:]`,
- `updated_at = NOW()`,
- ];
- const values: unknown[] = [status, windowSize];
- let paramIndex = 3;
-
- if (statusPatch) {
- const patchFieldMap: [keyof Monitor, string][] = [
- ["status", "status"],
- ["cpuAlertCounter", "cpu_alert_counter"],
- ["memoryAlertCounter", "memory_alert_counter"],
- ["diskAlertCounter", "disk_alert_counter"],
- ["tempAlertCounter", "temp_alert_counter"],
- ];
- for (const [key, column] of patchFieldMap) {
- if (statusPatch[key] !== undefined) {
- sets.push(`${column} = $${paramIndex++}`);
- values.push(statusPatch[key]);
- }
- }
- }
-
- values.push(monitorId, teamId);
- const result = await this.pool.query(
- `UPDATE monitors SET ${sets.join(", ")} WHERE id = $${paramIndex++} AND team_id = $${paramIndex}
- RETURNING ${MONITOR_COLUMNS}`,
- values
- );
- const row = result.rows[0];
- if (!row) {
- throw new AppError({ message: `Failed to update status and checks for monitor with id ${monitorId}`, status: 500 });
- }
- const entity = this.toEntity(row);
- entity.notifications = await this.fetchNotificationIds([monitorId]).then((m) => m.get(monitorId) ?? []);
- return entity;
- };
-
- togglePauseById = async (monitorId: string, teamId: string): Promise => {
- const result = await this.pool.query(
- `UPDATE monitors SET
- is_active = NOT is_active,
- status = CASE WHEN status = 'paused' THEN 'initializing'::monitor_status ELSE 'paused'::monitor_status END,
- updated_at = NOW()
- WHERE id = $1 AND team_id = $2
- RETURNING ${MONITOR_COLUMNS}`,
- [monitorId, teamId]
- );
- const row = result.rows[0];
- if (!row) {
- throw new AppError({ message: `Monitor with ID ${monitorId} not found for the given team.`, status: 404 });
- }
- const entity = this.toEntity(row);
- entity.notifications = await this.fetchNotificationIds([monitorId]).then((m) => m.get(monitorId) ?? []);
- return entity;
- };
-
- deleteById = async (monitorId: string, teamId: string): Promise => {
- // Fetch notifications before delete (FK cascade will remove join rows)
- const notifs = await this.fetchNotificationIds([monitorId]).then((m) => m.get(monitorId) ?? []);
- const result = await this.pool.query(`DELETE FROM monitors WHERE id = $1 AND team_id = $2 RETURNING ${MONITOR_COLUMNS}`, [
- monitorId,
- teamId,
- ]);
- const row = result.rows[0];
- if (!row) {
- throw new AppError({ message: `Monitor with ID ${monitorId} not found for the given team.`, status: 404 });
- }
- const entity = this.toEntity(row);
- entity.notifications = notifs;
- return entity;
- };
-
- deleteByTeamId = async (teamId: string): Promise<{ monitors: Monitor[]; deletedCount: number }> => {
- // Fetch notifications before delete
- const monitorsResult = await this.pool.query(`SELECT ${MONITOR_COLUMNS} FROM monitors WHERE team_id = $1`, [teamId]);
- const monitorIds = monitorsResult.rows.map((r) => r.id);
- const notifMap = monitorIds.length > 0 ? await this.fetchNotificationIds(monitorIds) : new Map();
-
- const result = await this.pool.query(`DELETE FROM monitors WHERE team_id = $1 RETURNING ${MONITOR_COLUMNS}`, [teamId]);
- const monitors = result.rows.map((row) => {
- const entity = this.toEntity(row);
- entity.notifications = notifMap.get(row.id) ?? [];
- return entity;
- });
- return { monitors, deletedCount: result.rowCount ?? 0 };
- };
-
- findMonitorsSummaryByTeamId = async (teamId: string, config?: SummaryConfig): Promise => {
- const conditions: string[] = ["team_id = $1"];
- const values: unknown[] = [teamId];
- let paramIndex = 2;
-
- if (config?.type !== undefined) {
- if (Array.isArray(config.type)) {
- conditions.push(`type = ANY($${paramIndex++})`);
- values.push(config.type);
- } else {
- conditions.push(`type = $${paramIndex++}`);
- values.push(config.type);
- }
- }
-
- const result = await this.pool.query(
- `SELECT
- COUNT(*)::int AS "totalMonitors",
- COUNT(*) FILTER (WHERE status = 'up')::int AS "upMonitors",
- COUNT(*) FILTER (WHERE status = 'down')::int AS "downMonitors",
- COUNT(*) FILTER (WHERE status = 'paused')::int AS "pausedMonitors",
- COUNT(*) FILTER (WHERE status = 'initializing')::int AS "initializingMonitors",
- COUNT(*) FILTER (WHERE status = 'maintenance')::int AS "maintenanceMonitors",
- COUNT(*) FILTER (WHERE status = 'breached')::int AS "breachedMonitors"
- FROM monitors WHERE ${conditions.join(" AND ")}`,
- values
- );
-
- return (
- result.rows[0] ?? {
- totalMonitors: 0,
- upMonitors: 0,
- downMonitors: 0,
- pausedMonitors: 0,
- initializingMonitors: 0,
- maintenanceMonitors: 0,
- breachedMonitors: 0,
- }
- );
- };
-
- findGroupsByTeamId = async (teamId: string): Promise => {
- const result = await this.pool.query(
- `SELECT DISTINCT monitor_group FROM monitors
- WHERE team_id = $1 AND monitor_group IS NOT NULL AND monitor_group != ''
- ORDER BY monitor_group`,
- [teamId]
- );
- return result.rows.map((row) => row.monitor_group);
- };
-
- removeNotificationFromMonitors = async (notificationId: string): Promise => {
- await this.pool.query(`DELETE FROM monitor_notifications WHERE notification_id = $1`, [notificationId]);
- };
-
- updateNotifications = async (
- teamId: string,
- monitorIds: string[],
- notificationIds: string[],
- action: "add" | "remove" | "set"
- ): Promise => {
- if (!monitorIds.length) {
- return 0;
- }
-
- // Verify monitors belong to team
- const monitorCheck = await this.pool.query(`SELECT id FROM monitors WHERE id = ANY($1) AND team_id = $2`, [monitorIds, teamId]);
- const validMonitorIds = monitorCheck.rows.map((row) => row.id);
-
- if (!validMonitorIds.length) {
- return 0;
- }
-
- let modified = 0;
-
- switch (action) {
- case "set": {
- // Track which monitors actually change
- const existingSet = await this.pool.query(`SELECT monitor_id, notification_id FROM monitor_notifications WHERE monitor_id = ANY($1)`, [
- validMonitorIds,
- ]);
- const existingByMonitor = new Map>();
- for (const row of existingSet.rows) {
- if (!existingByMonitor.has(row.monitor_id)) existingByMonitor.set(row.monitor_id, new Set());
- existingByMonitor.get(row.monitor_id)!.add(row.notification_id);
- }
-
- await this.pool.query(`DELETE FROM monitor_notifications WHERE monitor_id = ANY($1)`, [validMonitorIds]);
- for (const monitorId of validMonitorIds) {
- const existing = existingByMonitor.get(monitorId);
- const newSet = new Set(notificationIds);
- const changed = !existing || existing.size !== newSet.size || [...newSet].some((id) => !existing.has(id));
- if (changed) modified++;
-
- for (const notificationId of notificationIds) {
- await this.pool.query(
- `INSERT INTO monitor_notifications (monitor_id, notification_id)
- VALUES ($1, $2)
- ON CONFLICT DO NOTHING`,
- [monitorId, notificationId]
- );
- }
- }
- break;
- }
- case "add": {
- // Count monitors where at least one new notification was added
- const monitorsModified = new Set();
- for (const monitorId of validMonitorIds) {
- for (const notificationId of notificationIds) {
- const res = await this.pool.query(
- `INSERT INTO monitor_notifications (monitor_id, notification_id)
- VALUES ($1, $2)
- ON CONFLICT DO NOTHING`,
- [monitorId, notificationId]
- );
- if ((res.rowCount ?? 0) > 0) {
- monitorsModified.add(monitorId);
- }
- }
- }
- modified = monitorsModified.size;
- break;
- }
- case "remove": {
- // Count distinct monitors that will be affected before deleting
- const affectedResult = await this.pool.query(
- `SELECT COUNT(DISTINCT monitor_id)::int AS count FROM monitor_notifications
- WHERE monitor_id = ANY($1) AND notification_id = ANY($2)`,
- [validMonitorIds, notificationIds]
- );
- modified = affectedResult.rows[0]?.count ?? 0;
- await this.pool.query(
- `DELETE FROM monitor_notifications
- WHERE monitor_id = ANY($1) AND notification_id = ANY($2)`,
- [validMonitorIds, notificationIds]
- );
- break;
- }
- default:
- throw new AppError({ message: `Invalid action: ${action}`, status: 400 });
- }
-
- return modified;
- };
-
- deleteByTeamIdsNotIn = async (teamIds: string[]): Promise => {
- if (!teamIds.length) {
- const result = await this.pool.query(`DELETE FROM monitors`);
- return result.rowCount ?? 0;
- }
- const result = await this.pool.query(`DELETE FROM monitors WHERE team_id != ALL($1)`, [teamIds]);
- return result.rowCount ?? 0;
- };
-
- findAllMonitorIds = async (): Promise => {
- const result = await this.pool.query(`SELECT id FROM monitors`);
- return result.rows.map((row) => row.id);
- };
-
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
- private toCheckSnapshot = (row: any, childData?: { disk: any[]; net: any[]; errors: any[] }) => ({
- id: row.id,
- status: row.status,
- responseTime: row.response_time ?? 0,
- statusCode: row.status_code ?? 0,
- message: row.message ?? "",
- timings:
- row.timing_start !== null
- ? {
- start: row.timing_start,
- socket: row.timing_socket,
- lookup: row.timing_lookup,
- connect: row.timing_connect,
- secureConnect: row.timing_secure_connect,
- upload: row.timing_upload,
- response: row.timing_response,
- end: row.timing_end,
- phases: {
- wait: row.phase_wait,
- dns: row.phase_dns,
- tcp: row.phase_tcp,
- tls: row.phase_tls,
- request: row.phase_request,
- firstByte: row.phase_first_byte,
- download: row.phase_download,
- total: row.phase_total,
- },
- }
- : undefined,
- cpu:
- row.cpu_usage_percent !== null
- ? {
- physical_core: row.cpu_physical_core,
- logical_core: row.cpu_logical_core,
- frequency: row.cpu_frequency,
- current_frequency: row.cpu_current_frequency,
- temperature: row.cpu_temperature ?? [],
- free_percent: row.cpu_free_percent,
- usage_percent: row.cpu_usage_percent,
- }
- : undefined,
- memory:
- row.mem_usage_percent !== null
- ? {
- total_bytes: Number(row.mem_total_bytes),
- available_bytes: Number(row.mem_available_bytes),
- used_bytes: Number(row.mem_used_bytes),
- usage_percent: row.mem_usage_percent,
- }
- : undefined,
- disk: childData?.disk ?? [],
- host:
- row.host_os !== null
- ? {
- os: row.host_os,
- platform: row.host_platform,
- kernel_version: row.host_kernel_version,
- pretty_name: row.host_pretty_name,
- }
- : undefined,
- errors: childData?.errors ?? [],
- capture:
- row.capture_version !== null
- ? {
- version: row.capture_version,
- mode: row.capture_mode,
- }
- : undefined,
- net: childData?.net ?? [],
- performance: row.lighthouse_performance ?? undefined,
- accessibility: row.lighthouse_accessibility ?? undefined,
- bestPractices: row.lighthouse_best_practices ?? undefined,
- seo: row.lighthouse_seo ?? undefined,
- audits:
- row.audit_cls_score !== null
- ? {
- cls: { score: row.audit_cls_score, numericValue: row.audit_cls_value, displayValue: row.audit_cls_display },
- si: { score: row.audit_si_score, numericValue: row.audit_si_value, displayValue: row.audit_si_display },
- fcp: { score: row.audit_fcp_score, numericValue: row.audit_fcp_value, displayValue: row.audit_fcp_display },
- lcp: { score: row.audit_lcp_score, numericValue: row.audit_lcp_value, displayValue: row.audit_lcp_display },
- tbt: { score: row.audit_tbt_score, numericValue: row.audit_tbt_value, displayValue: row.audit_tbt_display },
- }
- : undefined,
- createdAt: row.created_at.toISOString(),
- });
-
- private fetchNotificationIds = async (monitorIds: string[]): Promise