Compare commits
201
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc7688bc13 | ||
|
|
ff114b3ab2 | ||
|
|
233966b73d | ||
|
|
45d4779b45 | ||
|
|
d608b5ea24 | ||
|
|
1ad750e2ef | ||
|
|
4d1af5e2b4 | ||
|
|
b515d688f4 | ||
|
|
ff92b307bb | ||
|
|
e4ffe0e267 | ||
|
|
2b121355c8 | ||
|
|
e7db938baa | ||
|
|
1b746d0f0a | ||
|
|
6886fc75de | ||
|
|
5c5f84a5b2 | ||
|
|
a12154523f | ||
|
|
6c8adf8594 | ||
|
|
a0f73e4e8d | ||
|
|
63efef65c7 | ||
|
|
dca09daf35 | ||
|
|
db833b441a | ||
|
|
bb55f3b3eb | ||
|
|
88e0660762 | ||
|
|
a8c172c77f | ||
|
|
b355da4aed | ||
|
|
77a4bf9c36 | ||
|
|
6a0f4372c6 | ||
|
|
db378120be | ||
|
|
ba5368f942 | ||
|
|
7329306ccf | ||
|
|
365987c287 | ||
|
|
09d6147f75 | ||
|
|
86ab1f2146 | ||
|
|
934f85f505 | ||
|
|
96beda5c47 | ||
|
|
504d6157e5 | ||
|
|
628bca3e29 | ||
|
|
2c76484bad | ||
|
|
22498756cd | ||
|
|
7c61ca490c | ||
|
|
96f9360280 | ||
|
|
eb4a92c84f | ||
|
|
c80720c8c9 | ||
|
|
42e82af9bb | ||
|
|
7636d2d090 | ||
|
|
72728748f8 | ||
|
|
4222d39a8b | ||
|
|
fb69e29adf | ||
|
|
0d90083e63 | ||
|
|
8ab049aa6c | ||
|
|
69b6955ced | ||
|
|
04dfff4ebd | ||
|
|
15b0deb0bd | ||
|
|
a895912b99 | ||
|
|
22d2626a93 | ||
|
|
214fc4899c | ||
|
|
e6088fe06f | ||
|
|
60b1d95b74 | ||
|
|
94ca9896db | ||
|
|
13a7ae82e3 | ||
|
|
de3af64254 | ||
|
|
317d531775 | ||
|
|
2b2db2df6f | ||
|
|
c558c4b635 | ||
|
|
be29cce788 | ||
|
|
e87879970a | ||
|
|
4c94a7c21e | ||
|
|
40968c370b | ||
|
|
a864bc0c05 | ||
|
|
62e86434c2 | ||
|
|
07957d3a1f | ||
|
|
bc088c3589 | ||
|
|
5ea86d8e71 | ||
|
|
0944dba386 | ||
|
|
86acc0c30a | ||
|
|
dcdde845db | ||
|
|
3039fb9b69 | ||
|
|
729600a86f | ||
|
|
51853cb360 | ||
|
|
f7db732d67 | ||
|
|
9e686a2319 | ||
|
|
da6f03ba59 | ||
|
|
276b11603e | ||
|
|
f09f0630dc | ||
|
|
1defa52ae8 | ||
|
|
f43b7584a6 | ||
|
|
ee74584699 | ||
|
|
2422fcc83b | ||
|
|
0ecc2e21e5 | ||
|
|
fe242b1977 | ||
|
|
d1f1d31ea2 | ||
|
|
eacbb227c3 | ||
|
|
c24b41ecd9 | ||
|
|
94cf4f17d1 | ||
|
|
0a6d876f4f | ||
|
|
ec129e3604 | ||
|
|
833e2b63ad | ||
|
|
bc2bab87ba | ||
|
|
ee5f7427e7 | ||
|
|
d5968b841e | ||
|
|
26bac6d35a | ||
|
|
3e010f1c0d | ||
|
|
1a504a3cd9 | ||
|
|
120c5cd472 | ||
|
|
cdf17bf600 | ||
|
|
7144c20c5b | ||
|
|
7456609e35 | ||
|
|
05c293d689 | ||
|
|
80f3168837 | ||
|
|
332300dde4 | ||
|
|
d4a8023a76 | ||
|
|
c7e449d18c | ||
|
|
0495bef469 | ||
|
|
6fb7ad63f6 | ||
|
|
8322adbb26 | ||
|
|
4470c87d42 | ||
|
|
95bda35ec6 | ||
|
|
928ab7551e | ||
|
|
465cea21bc | ||
|
|
404bd5d06b | ||
|
|
05814854df | ||
|
|
a4a3249b5b | ||
|
|
1e6cf858c4 | ||
|
|
533a8911cd | ||
|
|
2fb7516ca1 | ||
|
|
6d2008e8d3 | ||
|
|
7332b15242 | ||
|
|
d618a861e3 | ||
|
|
f69752ccb4 | ||
|
|
210881a375 | ||
|
|
d8ec8d7775 | ||
|
|
bfd3eee94d | ||
|
|
bfbb4c390a | ||
|
|
1285a641e8 | ||
|
|
78566c0226 | ||
|
|
f5ba387261 | ||
|
|
48b415a67c | ||
|
|
b53f0e56ea | ||
|
|
07f828a01a | ||
|
|
b09203664d | ||
|
|
5a12105c3d | ||
|
|
22f79144d8 | ||
|
|
f749ed46a6 | ||
|
|
5c23ebe315 | ||
|
|
115fcc67f6 | ||
|
|
622446c308 | ||
|
|
294a149fd1 | ||
|
|
d3d00353a1 | ||
|
|
894b6a7a7d | ||
|
|
3a9f9f0ce5 | ||
|
|
45618a2ab0 | ||
|
|
d80e232a10 | ||
|
|
1706012e54 | ||
|
|
db9d5cf5e4 | ||
|
|
52a6b4c9fa | ||
|
|
9646e7dbdc | ||
|
|
d6ceedc2dd | ||
|
|
501664509b | ||
|
|
2ef184c58a | ||
|
|
22f917034d | ||
|
|
81f623b632 | ||
|
|
d3605b0149 | ||
|
|
48e71d4345 | ||
|
|
411270d256 | ||
|
|
6404a06999 | ||
|
|
a2bc2d8442 | ||
|
|
e9cb302d6e | ||
|
|
fa8764fde2 | ||
|
|
84ca5db0fa | ||
|
|
8ec285ed41 | ||
|
|
d070a33d7f | ||
|
|
aa61e0f321 | ||
|
|
88a39fe502 | ||
|
|
a6b92d3c51 | ||
|
|
1af609a569 | ||
|
|
61cc0c2bef | ||
|
|
e4a228e61e | ||
|
|
842fa92cec | ||
|
|
9466090332 | ||
|
|
01f2aa294e | ||
|
|
365b6cc60a | ||
|
|
24d571a8db | ||
|
|
d41c5e9ebf | ||
|
|
3dd0e0a998 | ||
|
|
bcff3e15a8 | ||
|
|
1ad39fcac6 | ||
|
|
943ef92053 | ||
|
|
f9a4659cb2 | ||
|
|
0fc338db2f | ||
|
|
5578fdf17f | ||
|
|
2a1645baf2 | ||
|
|
8483a7bc55 | ||
|
|
27dd565bc6 | ||
|
|
c5f0316990 | ||
|
|
a40a76fb3c | ||
|
|
4d7de2d3a1 | ||
|
|
9c17fbe79c | ||
|
|
8eca81c8be | ||
|
|
eb2d8b8d3b | ||
|
|
4827f8faec | ||
|
|
9e06758997 |
No files matched your search
+105
-105
@@ -1,120 +1,120 @@
|
|||||||
name: POEditor Translation Synchronization
|
# name: POEditor Translation Synchronization
|
||||||
|
|
||||||
on:
|
# on:
|
||||||
# For manual triggering
|
# # For manual triggering
|
||||||
workflow_dispatch:
|
# workflow_dispatch:
|
||||||
inputs:
|
# inputs:
|
||||||
languages:
|
# languages:
|
||||||
description: "Languages to synchronize (comma separated, e.g.: tr,en,es)"
|
# description: "Languages to synchronize (comma separated, e.g.: tr,en,es)"
|
||||||
required: false
|
# required: false
|
||||||
default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn,th"
|
# default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn,th"
|
||||||
format:
|
# format:
|
||||||
description: "Export format (key_value_json or json)"
|
# description: "Export format (key_value_json or json)"
|
||||||
required: false
|
# required: false
|
||||||
default: "key_value_json"
|
# default: "key_value_json"
|
||||||
|
|
||||||
permissions:
|
# permissions:
|
||||||
contents: write
|
# contents: write
|
||||||
pull-requests: write
|
# pull-requests: write
|
||||||
|
|
||||||
jobs:
|
# jobs:
|
||||||
sync-translations:
|
# sync-translations:
|
||||||
runs-on: ubuntu-latest
|
# runs-on: ubuntu-latest
|
||||||
permissions:
|
# permissions:
|
||||||
contents: write
|
# contents: write
|
||||||
issues: write
|
# issues: write
|
||||||
pull-requests: write
|
# pull-requests: write
|
||||||
|
|
||||||
steps:
|
# steps:
|
||||||
- name: Checkout code
|
# - name: Checkout code
|
||||||
uses: actions/checkout@v3
|
# uses: actions/checkout@v3
|
||||||
with:
|
# with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
# token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
fetch-depth: 0
|
# fetch-depth: 0
|
||||||
|
|
||||||
- name: Setup Node.js
|
# - name: Setup Node.js
|
||||||
uses: actions/setup-node@v3
|
# uses: actions/setup-node@v3
|
||||||
with:
|
# with:
|
||||||
node-version: "22"
|
# node-version: "22"
|
||||||
|
|
||||||
- name: Create package.json for scripts
|
# - name: Create package.json for scripts
|
||||||
run: |
|
# run: |
|
||||||
mkdir -p .github/scripts
|
# mkdir -p .github/scripts
|
||||||
cat > .github/scripts/package.json << EOF
|
# cat > .github/scripts/package.json << EOF
|
||||||
{
|
# {
|
||||||
"name": "poeditor-scripts",
|
# "name": "poeditor-scripts",
|
||||||
"version": "1.0.0",
|
# "version": "1.0.0",
|
||||||
"private": true,
|
# "private": true,
|
||||||
"type": "module",
|
# "type": "module",
|
||||||
"dependencies": {
|
# "dependencies": {
|
||||||
"axios": "^1.6.0",
|
# "axios": "^1.6.0",
|
||||||
"fs-extra": "^11.1.1"
|
# "fs-extra": "^11.1.1"
|
||||||
}
|
# }
|
||||||
}
|
# }
|
||||||
EOF
|
# EOF
|
||||||
|
|
||||||
- name: Install dependencies
|
# - name: Install dependencies
|
||||||
run: |
|
# run: |
|
||||||
cd .github/scripts
|
# cd .github/scripts
|
||||||
npm install
|
# npm install
|
||||||
|
|
||||||
- name: Download translations from POEditor
|
# - name: Download translations from POEditor
|
||||||
env:
|
# env:
|
||||||
POEDITOR_API: ${{ secrets.POEDITOR_API }}
|
# POEDITOR_API: ${{ secrets.POEDITOR_API }}
|
||||||
POEDITOR_PROJECT_ID: ${{ secrets.POEDITOR_PROJECT_ID }}
|
# POEDITOR_PROJECT_ID: ${{ secrets.POEDITOR_PROJECT_ID }}
|
||||||
LANGUAGES: ${{ github.event.inputs.languages || 'tr,en' }}
|
# LANGUAGES: ${{ github.event.inputs.languages || 'tr,en' }}
|
||||||
EXPORT_FORMAT: ${{ github.event.inputs.format || 'key_value_json' }}
|
# EXPORT_FORMAT: ${{ github.event.inputs.format || 'key_value_json' }}
|
||||||
run: |
|
# run: |
|
||||||
mkdir -p temp
|
# mkdir -p temp
|
||||||
node .github/scripts/download-translations.js
|
# node .github/scripts/download-translations.js
|
||||||
|
|
||||||
- name: Verify translation files
|
# - name: Verify translation files
|
||||||
run: |
|
# run: |
|
||||||
echo "Verifying translation files..."
|
# echo "Verifying translation files..."
|
||||||
for file in temp/*.json; do
|
# for file in temp/*.json; do
|
||||||
echo "Checking $file"
|
# echo "Checking $file"
|
||||||
if [ ! -s "$file" ]; then
|
# if [ ! -s "$file" ]; then
|
||||||
echo "Error: $file is empty or does not exist"
|
# echo "Error: $file is empty or does not exist"
|
||||||
exit 1
|
# exit 1
|
||||||
fi
|
# fi
|
||||||
# Validate JSON format
|
# # Validate JSON format
|
||||||
cat "$file" | jq . > /dev/null || { echo "Error: $file is not valid JSON"; exit 1; }
|
# cat "$file" | jq . > /dev/null || { echo "Error: $file is not valid JSON"; exit 1; }
|
||||||
done
|
# done
|
||||||
echo "All translation files are valid"
|
# echo "All translation files are valid"
|
||||||
|
|
||||||
- name: Copy translations to project
|
# - name: Copy translations to project
|
||||||
run: |
|
# run: |
|
||||||
mkdir -p client/src/locales
|
# mkdir -p client/src/locales
|
||||||
cp -r temp/* client/src/locales/
|
# cp -r temp/* client/src/locales/
|
||||||
echo "Translation files copied to client/src/locales/"
|
# echo "Translation files copied to client/src/locales/"
|
||||||
|
|
||||||
- name: Get current date
|
# - name: Get current date
|
||||||
id: date
|
# id: date
|
||||||
run: echo "date=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT
|
# run: echo "date=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
- name: Format client code
|
# - name: Format client code
|
||||||
run: |
|
# run: |
|
||||||
cd client
|
# cd client
|
||||||
npm ci
|
# npm ci
|
||||||
npm run format
|
# npm run format
|
||||||
|
|
||||||
- name: Create Pull Request
|
# - name: Create Pull Request
|
||||||
uses: peter-evans/create-pull-request@v7
|
# uses: peter-evans/create-pull-request@v7
|
||||||
with:
|
# with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
# token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
commit-message: "feat: translations updated from POEditor"
|
# commit-message: "feat: translations updated from POEditor"
|
||||||
title: "🌐 Updated Translations from POEditor"
|
# title: "🌐 Updated Translations from POEditor"
|
||||||
body: |
|
# body: |
|
||||||
This PR contains the latest translations from POEditor.
|
# This PR contains the latest translations from POEditor.
|
||||||
|
|
||||||
📅 Update Date: ${{ steps.date.outputs.date }}
|
# 📅 Update Date: ${{ steps.date.outputs.date }}
|
||||||
🔄 Updated Languages: ${{ github.event.inputs.languages || 'tr,en' }}
|
# 🔄 Updated Languages: ${{ github.event.inputs.languages || 'tr,en' }}
|
||||||
|
|
||||||
⚠️ Please review the translations and approve the PR if everything looks correct.
|
# ⚠️ Please review the translations and approve the PR if everything looks correct.
|
||||||
branch: translation-update-${{ github.run_number }}
|
# branch: translation-update-${{ github.run_number }}
|
||||||
delete-branch: true
|
# delete-branch: true
|
||||||
base: develop
|
# base: develop
|
||||||
add-paths: |
|
# add-paths: |
|
||||||
client/src/locales/*.json
|
# client/src/locales/*.json
|
||||||
committer: GitHub Action <github-actions[bot]@users.noreply.github.com>
|
# committer: GitHub Action <github-actions[bot]@users.noreply.github.com>
|
||||||
author: GitHub Action <github-actions[bot]@users.noreply.github.com>
|
# author: GitHub Action <github-actions[bot]@users.noreply.github.com>
|
||||||
@@ -3,8 +3,10 @@
|
|||||||
.VSCodeCounter
|
.VSCodeCounter
|
||||||
*.sh
|
*.sh
|
||||||
mongo
|
mongo
|
||||||
|
timescaledb
|
||||||
node_modules/
|
node_modules/
|
||||||
docs/architecture
|
docs/architecture
|
||||||
docs/reviews
|
docs/reviews
|
||||||
docs/todo
|
docs/todo
|
||||||
docs/frontend
|
docs/frontend
|
||||||
|
docs/timescale
|
||||||
@@ -29,7 +29,6 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul
|
|||||||
- [📦 Demo](#demo)
|
- [📦 Demo](#demo)
|
||||||
- [🔗 User's guide](#users-guide)
|
- [🔗 User's guide](#users-guide)
|
||||||
- [🛠️ Installation](#installation)
|
- [🛠️ Installation](#installation)
|
||||||
- [🏁 Translations](#translations)
|
|
||||||
- [🚀 Performance](#performance)
|
- [🚀 Performance](#performance)
|
||||||
- [💚 Questions & Ideas](#questions--ideas)
|
- [💚 Questions & Ideas](#questions--ideas)
|
||||||
- [🧩 Features](#features)
|
- [🧩 Features](#features)
|
||||||
@@ -48,11 +47,15 @@ You can see the latest build of [Checkmate](https://checkmate-demo.bluewavelabs.
|
|||||||
|
|
||||||
Usage instructions can be found [here](https://checkmate.so/docs).
|
Usage instructions can be found [here](https://checkmate.so/docs).
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
- [Docker](https://www.docker.com/) installed
|
||||||
|
- [Git](https://git-scm.com/) installed
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
See installation instructions in [Checkmate documentation portal](https://checkmate.so/docs).
|
See installation instructions in [Checkmate documentation portal](https://checkmate.so/docs).
|
||||||
|
|
||||||
Alternatively, you can also use [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
|
Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa), [Cloudzy](https://cloudzy.com/marketplace/checkmate) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
|
||||||
|
|
||||||
### Using a Custom CA
|
### Using a Custom CA
|
||||||
|
|
||||||
@@ -60,11 +63,6 @@ If you need to monitor internal HTTPS endpoints with certificates from private C
|
|||||||
|
|
||||||
For more documentation, see the [docs directory](./docs/).
|
For more documentation, see the [docs directory](./docs/).
|
||||||
|
|
||||||
|
|
||||||
## Translations
|
|
||||||
|
|
||||||
If you would like to use Checkmate in your language, please [go to this page](https://poeditor.com/join/project/lRUoGZFCsJ) and register for the language you would like to translate Checkmate to.
|
|
||||||
|
|
||||||
## Performance
|
## Performance
|
||||||
|
|
||||||
Thanks to extensive optimizations, Checkmate operates with an exceptionally small memory footprint, requiring minimal memory and CPU resources. Here’s the memory usage of a Node.js instance running on a server that monitors 323 servers every minute:
|
Thanks to extensive optimizations, Checkmate operates with an exceptionally small memory footprint, requiring minimal memory and CPU resources. Here’s the memory usage of a Node.js instance running on a server that monitors 323 servers every minute:
|
||||||
@@ -80,7 +78,7 @@ You can see the memory footprint of MongoDB and Redis on the same server (398Mb
|
|||||||
If you have any questions, suggestions or comments, you have several options:
|
If you have any questions, suggestions or comments, you have several options:
|
||||||
|
|
||||||
- [Discord channel](https://discord.gg/NAb6H3UTjK) (preferred)
|
- [Discord channel](https://discord.gg/NAb6H3UTjK) (preferred)
|
||||||
- [GitHub Discussions](https://github.com/bluewave-labs/bluewave-uptime/discussions) (we check here from time to time)
|
- [GitHub Discussions](https://github.com/bluewave-labs/Checkmate/discussions) (we check here from time to time)
|
||||||
|
|
||||||
Feel free to ask questions or share your ideas - we'd love to hear from you!
|
Feel free to ask questions or share your ideas - we'd love to hear from you!
|
||||||
|
|
||||||
@@ -167,7 +165,7 @@ Here's how you can contribute:
|
|||||||
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
|
<img src="https://contrib.rocks/image?repo=bluewave-labs/checkmate" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
[](https://star-history.com/#bluewave-labs/bluewave-uptime&Date)
|
[](https://star-history.com/#bluewave-labs/Checkmate&Date)
|
||||||
|
|
||||||
## Our sponsors
|
## Our sponsors
|
||||||
|
|
||||||
|
|||||||
Generated
+3
-22
@@ -83,7 +83,6 @@
|
|||||||
"node_modules/@babel/core": {
|
"node_modules/@babel/core": {
|
||||||
"version": "7.29.0",
|
"version": "7.29.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.29.0",
|
"@babel/code-frame": "^7.29.0",
|
||||||
"@babel/generator": "^7.29.0",
|
"@babel/generator": "^7.29.0",
|
||||||
@@ -354,7 +353,6 @@
|
|||||||
"node_modules/@emotion/react": {
|
"node_modules/@emotion/react": {
|
||||||
"version": "11.14.0",
|
"version": "11.14.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.18.3",
|
"@babel/runtime": "^7.18.3",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
@@ -392,7 +390,6 @@
|
|||||||
"node_modules/@emotion/styled": {
|
"node_modules/@emotion/styled": {
|
||||||
"version": "11.14.1",
|
"version": "11.14.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.18.3",
|
"@babel/runtime": "^7.18.3",
|
||||||
"@emotion/babel-plugin": "^11.13.5",
|
"@emotion/babel-plugin": "^11.13.5",
|
||||||
@@ -1162,7 +1159,6 @@
|
|||||||
"node_modules/@mui/material": {
|
"node_modules/@mui/material": {
|
||||||
"version": "7.3.7",
|
"version": "7.3.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.28.4",
|
"@babel/runtime": "^7.28.4",
|
||||||
"@mui/core-downloads-tracker": "^7.3.7",
|
"@mui/core-downloads-tracker": "^7.3.7",
|
||||||
@@ -1267,7 +1263,6 @@
|
|||||||
"node_modules/@mui/system": {
|
"node_modules/@mui/system": {
|
||||||
"version": "7.3.7",
|
"version": "7.3.7",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.28.4",
|
"@babel/runtime": "^7.28.4",
|
||||||
"@mui/private-theming": "^7.3.7",
|
"@mui/private-theming": "^7.3.7",
|
||||||
@@ -2308,7 +2303,6 @@
|
|||||||
"version": "24.5.2",
|
"version": "24.5.2",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.12.0"
|
"undici-types": "~7.12.0"
|
||||||
}
|
}
|
||||||
@@ -2324,7 +2318,6 @@
|
|||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "18.3.27",
|
"version": "18.3.27",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/prop-types": "*",
|
"@types/prop-types": "*",
|
||||||
"csstype": "^3.2.2"
|
"csstype": "^3.2.2"
|
||||||
@@ -2446,7 +2439,6 @@
|
|||||||
"version": "8.15.0",
|
"version": "8.15.0",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -2771,7 +2763,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.9.0",
|
"baseline-browser-mapping": "^2.9.0",
|
||||||
"caniuse-lite": "^1.0.30001759",
|
"caniuse-lite": "^1.0.30001759",
|
||||||
@@ -3203,8 +3194,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/dayjs": {
|
"node_modules/dayjs": {
|
||||||
"version": "1.11.13",
|
"version": "1.11.13",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/debug": {
|
"node_modules/debug": {
|
||||||
"version": "4.4.3",
|
"version": "4.4.3",
|
||||||
@@ -3586,7 +3576,6 @@
|
|||||||
"version": "8.57.1",
|
"version": "8.57.1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.2.0",
|
"@eslint-community/eslint-utils": "^4.2.0",
|
||||||
"@eslint-community/regexpp": "^4.6.1",
|
"@eslint-community/regexpp": "^4.6.1",
|
||||||
@@ -4331,7 +4320,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/runtime": "^7.27.6"
|
"@babel/runtime": "^7.27.6"
|
||||||
},
|
},
|
||||||
@@ -5115,7 +5103,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz",
|
"resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz",
|
||||||
"integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==",
|
"integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==",
|
||||||
"license": "BSD-3-Clause",
|
"license": "BSD-3-Clause",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@mapbox/geojson-rewind": "^0.5.2",
|
"@mapbox/geojson-rewind": "^0.5.2",
|
||||||
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
"@mapbox/jsonlint-lines-primitives": "^2.0.2",
|
||||||
@@ -5763,7 +5750,6 @@
|
|||||||
"node_modules/react-dom": {
|
"node_modules/react-dom": {
|
||||||
"version": "18.3.1",
|
"version": "18.3.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.1.0",
|
"loose-envify": "^1.1.0",
|
||||||
"scheduler": "^0.23.2"
|
"scheduler": "^0.23.2"
|
||||||
@@ -5775,7 +5761,6 @@
|
|||||||
"node_modules/react-hook-form": {
|
"node_modules/react-hook-form": {
|
||||||
"version": "7.71.1",
|
"version": "7.71.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.0.0"
|
"node": ">=18.0.0"
|
||||||
},
|
},
|
||||||
@@ -5975,8 +5960,7 @@
|
|||||||
},
|
},
|
||||||
"node_modules/redux": {
|
"node_modules/redux": {
|
||||||
"version": "5.0.1",
|
"version": "5.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT"
|
||||||
"peer": true
|
|
||||||
},
|
},
|
||||||
"node_modules/redux-persist": {
|
"node_modules/redux-persist": {
|
||||||
"version": "6.0.0",
|
"version": "6.0.0",
|
||||||
@@ -6122,11 +6106,10 @@
|
|||||||
"license": "Unlicense"
|
"license": "Unlicense"
|
||||||
},
|
},
|
||||||
"node_modules/rollup": {
|
"node_modules/rollup": {
|
||||||
"version": "4.59.0",
|
"version": "4.57.1",
|
||||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz",
|
||||||
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
"integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/estree": "1.0.8"
|
"@types/estree": "1.0.8"
|
||||||
},
|
},
|
||||||
@@ -6915,7 +6898,6 @@
|
|||||||
"version": "5.9.3",
|
"version": "5.9.3",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -7051,7 +7033,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-6.4.1.tgz",
|
||||||
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
"integrity": "sha512-+Oxm7q9hDoLMyJOYfUYBuHQo+dkAloi33apOPP56pzj+vsdJDzr+j1NISE5pyaAuKL4A3UD34qd0lx5+kfKp2g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.4.4",
|
"fdir": "^6.4.4",
|
||||||
|
|||||||
@@ -0,0 +1,258 @@
|
|||||||
|
/**
|
||||||
|
* Locale Migration Script
|
||||||
|
*
|
||||||
|
* Restructures all non-English locale files to match en.json's nested key structure.
|
||||||
|
* Recovers translations via safe key-matching strategies, uses English placeholders
|
||||||
|
* for unmatched keys.
|
||||||
|
*
|
||||||
|
* Usage: npx tsx client/scripts/migrate-locales.ts
|
||||||
|
*
|
||||||
|
* SAFETY: en.json is NEVER modified. Originals are backed up before overwriting.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const LOCALES_DIR = path.resolve(import.meta.dirname, "../src/locales");
|
||||||
|
const BACKUP_DIR = path.join(LOCALES_DIR, "backup");
|
||||||
|
const REPORT_PATH = path.join(LOCALES_DIR, "migration-report.json");
|
||||||
|
|
||||||
|
// ── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Flatten a nested object into { "a.b.c": value } pairs */
|
||||||
|
function flattenKeys(obj: Record<string, unknown>, prefix = ""): Record<string, string> {
|
||||||
|
const result: Record<string, string> = {};
|
||||||
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
|
const fullPath = prefix ? `${prefix}.${key}` : key;
|
||||||
|
if (typeof value === "object" && value !== null && !Array.isArray(value)) {
|
||||||
|
Object.assign(result, flattenKeys(value as Record<string, unknown>, fullPath));
|
||||||
|
} else {
|
||||||
|
result[fullPath] = String(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Set a value at a dot-separated path in a nested object */
|
||||||
|
function setNestedValue(
|
||||||
|
obj: Record<string, unknown>,
|
||||||
|
keyPath: string,
|
||||||
|
value: string
|
||||||
|
): void {
|
||||||
|
const parts = keyPath.split(".");
|
||||||
|
let current = obj;
|
||||||
|
for (let i = 0; i < parts.length - 1; i++) {
|
||||||
|
const part = parts[i];
|
||||||
|
if (!(part in current) || typeof current[part] !== "object") {
|
||||||
|
current[part] = {};
|
||||||
|
}
|
||||||
|
current = current[part] as Record<string, unknown>;
|
||||||
|
}
|
||||||
|
current[parts[parts.length - 1]] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Build mapping indexes from en.json ───────────────────────────────────────
|
||||||
|
|
||||||
|
function buildIndexes(enFlat: Record<string, string>) {
|
||||||
|
const enKeys = Object.keys(enFlat);
|
||||||
|
|
||||||
|
// last segment -> en key paths
|
||||||
|
const lastSegIndex: Record<string, string[]> = {};
|
||||||
|
// last 2 segments -> en key paths
|
||||||
|
const suffixIndex: Record<string, string[]> = {};
|
||||||
|
|
||||||
|
for (const keyPath of enKeys) {
|
||||||
|
const parts = keyPath.split(".");
|
||||||
|
const lastSeg = parts[parts.length - 1].toLowerCase();
|
||||||
|
if (!lastSegIndex[lastSeg]) lastSegIndex[lastSeg] = [];
|
||||||
|
lastSegIndex[lastSeg].push(keyPath);
|
||||||
|
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
const suffix = parts.slice(-2).join(".").toLowerCase();
|
||||||
|
if (!suffixIndex[suffix]) suffixIndex[suffix] = [];
|
||||||
|
suffixIndex[suffix].push(keyPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { lastSegIndex, suffixIndex };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Mapping logic ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
type MatchStrategy = "exact" | "suffix-2" | "unique-name" | "english-placeholder";
|
||||||
|
|
||||||
|
interface KeyMatch {
|
||||||
|
enKey: string;
|
||||||
|
value: string;
|
||||||
|
strategy: MatchStrategy;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMigratedLocale(
|
||||||
|
enFlat: Record<string, string>,
|
||||||
|
langFlat: Record<string, string>,
|
||||||
|
indexes: ReturnType<typeof buildIndexes>
|
||||||
|
): { matches: KeyMatch[]; result: Record<string, unknown> } {
|
||||||
|
const { lastSegIndex, suffixIndex } = indexes;
|
||||||
|
const matches: KeyMatch[] = [];
|
||||||
|
const result: Record<string, unknown> = {};
|
||||||
|
|
||||||
|
// Build reverse lookup: for each orphan key in lang, index by last segment and suffix
|
||||||
|
const langByLastSeg: Record<string, { key: string; value: string }[]> = {};
|
||||||
|
const langBySuffix: Record<string, { key: string; value: string }[]> = {};
|
||||||
|
|
||||||
|
for (const [langKey, langValue] of Object.entries(langFlat)) {
|
||||||
|
const parts = langKey.split(".");
|
||||||
|
const lastSeg = parts[parts.length - 1].toLowerCase();
|
||||||
|
if (!langByLastSeg[lastSeg]) langByLastSeg[lastSeg] = [];
|
||||||
|
langByLastSeg[lastSeg].push({ key: langKey, value: langValue });
|
||||||
|
|
||||||
|
if (parts.length >= 2) {
|
||||||
|
const suffix = parts.slice(-2).join(".").toLowerCase();
|
||||||
|
if (!langBySuffix[suffix]) langBySuffix[suffix] = [];
|
||||||
|
langBySuffix[suffix].push({ key: langKey, value: langValue });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const enKey of Object.keys(enFlat)) {
|
||||||
|
const enValue = enFlat[enKey];
|
||||||
|
const parts = enKey.split(".");
|
||||||
|
const lastSeg = parts[parts.length - 1].toLowerCase();
|
||||||
|
const suffix = parts.length >= 2 ? parts.slice(-2).join(".").toLowerCase() : null;
|
||||||
|
|
||||||
|
// Strategy 1: Exact path match
|
||||||
|
if (langFlat[enKey] !== undefined) {
|
||||||
|
const value = langFlat[enKey];
|
||||||
|
setNestedValue(result, enKey, value);
|
||||||
|
matches.push({ enKey, value, strategy: "exact" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 2: 2-segment suffix match (lang key suffix matches en key suffix, both unique)
|
||||||
|
if (suffix) {
|
||||||
|
const enCandidates = suffixIndex[suffix];
|
||||||
|
const langCandidates = langBySuffix[suffix];
|
||||||
|
if (
|
||||||
|
enCandidates &&
|
||||||
|
enCandidates.length === 1 &&
|
||||||
|
langCandidates &&
|
||||||
|
langCandidates.length === 1
|
||||||
|
) {
|
||||||
|
const value = langCandidates[0].value;
|
||||||
|
setNestedValue(result, enKey, value);
|
||||||
|
matches.push({ enKey, value, strategy: "suffix-2" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Strategy 3: Unique last-segment match
|
||||||
|
const enCandidates = lastSegIndex[lastSeg];
|
||||||
|
const langCandidates = langByLastSeg[lastSeg];
|
||||||
|
if (
|
||||||
|
enCandidates &&
|
||||||
|
enCandidates.length === 1 &&
|
||||||
|
langCandidates &&
|
||||||
|
langCandidates.length === 1
|
||||||
|
) {
|
||||||
|
const value = langCandidates[0].value;
|
||||||
|
setNestedValue(result, enKey, value);
|
||||||
|
matches.push({ enKey, value, strategy: "unique-name" });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No match — use English placeholder
|
||||||
|
setNestedValue(result, enKey, enValue);
|
||||||
|
matches.push({ enKey, value: enValue, strategy: "english-placeholder" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return { matches, result };
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Main ─────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function main() {
|
||||||
|
// Load en.json
|
||||||
|
const enPath = path.join(LOCALES_DIR, "en.json");
|
||||||
|
const en = JSON.parse(fs.readFileSync(enPath, "utf8"));
|
||||||
|
const enFlat = flattenKeys(en);
|
||||||
|
const enKeyCount = Object.keys(enFlat).length;
|
||||||
|
const indexes = buildIndexes(enFlat);
|
||||||
|
|
||||||
|
console.log(`English: ${enKeyCount} keys (source of truth — NOT modified)\n`);
|
||||||
|
|
||||||
|
// Find all non-English locale files
|
||||||
|
const localeFiles = fs
|
||||||
|
.readdirSync(LOCALES_DIR)
|
||||||
|
.filter(
|
||||||
|
(f: string) =>
|
||||||
|
f.endsWith(".json") && f !== "en.json" && f !== "migration-report.json"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (localeFiles.length === 0) {
|
||||||
|
console.log("No locale files to migrate.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create backup directory
|
||||||
|
if (!fs.existsSync(BACKUP_DIR)) {
|
||||||
|
fs.mkdirSync(BACKUP_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const report: Record<
|
||||||
|
string,
|
||||||
|
{
|
||||||
|
total: number;
|
||||||
|
exact: number;
|
||||||
|
suffix: number;
|
||||||
|
uniqueName: number;
|
||||||
|
placeholder: number;
|
||||||
|
}
|
||||||
|
> = {};
|
||||||
|
|
||||||
|
for (const file of localeFiles) {
|
||||||
|
const lang = file.replace(".json", "");
|
||||||
|
const filePath = path.join(LOCALES_DIR, file);
|
||||||
|
|
||||||
|
// Backup original
|
||||||
|
const backupPath = path.join(BACKUP_DIR, file);
|
||||||
|
fs.copyFileSync(filePath, backupPath);
|
||||||
|
|
||||||
|
// Load and flatten
|
||||||
|
const langData = JSON.parse(fs.readFileSync(filePath, "utf8"));
|
||||||
|
const langFlat = flattenKeys(langData);
|
||||||
|
|
||||||
|
// Migrate
|
||||||
|
const { matches, result } = buildMigratedLocale(enFlat, langFlat, indexes);
|
||||||
|
|
||||||
|
// Write migrated file
|
||||||
|
fs.writeFileSync(filePath, JSON.stringify(result, null, "\t") + "\n", "utf8");
|
||||||
|
|
||||||
|
// Compute stats
|
||||||
|
const stats = {
|
||||||
|
total: enKeyCount,
|
||||||
|
exact: matches.filter((m) => m.strategy === "exact").length,
|
||||||
|
suffix: matches.filter((m) => m.strategy === "suffix-2").length,
|
||||||
|
uniqueName: matches.filter((m) => m.strategy === "unique-name").length,
|
||||||
|
placeholder: matches.filter((m) => m.strategy === "english-placeholder").length,
|
||||||
|
};
|
||||||
|
report[lang] = stats;
|
||||||
|
|
||||||
|
const translated = stats.exact + stats.suffix + stats.uniqueName;
|
||||||
|
const pct = ((translated / enKeyCount) * 100).toFixed(1);
|
||||||
|
console.log(
|
||||||
|
`${lang.padEnd(8)} ${String(translated).padStart(4)} translated (${pct}%) | ` +
|
||||||
|
`exact: ${stats.exact} suffix: ${stats.suffix} name: ${stats.uniqueName} ` +
|
||||||
|
`placeholder: ${stats.placeholder}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write report
|
||||||
|
fs.writeFileSync(REPORT_PATH, JSON.stringify(report, null, "\t") + "\n", "utf8");
|
||||||
|
|
||||||
|
const totalLangs = localeFiles.length;
|
||||||
|
console.log(
|
||||||
|
`\nMigrated ${totalLangs} locale files. Originals backed up to locales/backup/`
|
||||||
|
);
|
||||||
|
console.log(`Report written to ${REPORT_PATH}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -57,7 +57,8 @@ export const HeatmapResponseTimeTooltip = ({
|
|||||||
{formatDateWithTz(check?.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)}
|
{formatDateWithTz(check?.createdAt, "ddd, MMMM D, YYYY, HH:mm A", uiTimezone)}
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography>
|
<Typography>
|
||||||
{t("common.labels.responseTime")}: {check.originalResponseTime.toFixed()} ms
|
{t("common.labels.responseTime")}:{" "}
|
||||||
|
{check?.originalResponseTime?.toFixed() ?? "N/A"} ms
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography textTransform={"capitalize"}>
|
<Typography textTransform={"capitalize"}>
|
||||||
Status:{" "}
|
Status:{" "}
|
||||||
|
|||||||
@@ -1,28 +1,76 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { notificationSchema } from "@/Validation/notifications";
|
import { notificationSchema } from "@/Validation/notifications";
|
||||||
|
import type { NotificationFormData } from "@/Validation/notifications";
|
||||||
import type { Notification } from "@/Types/Notification";
|
import type { Notification } from "@/Types/Notification";
|
||||||
|
|
||||||
interface UseNotificationFormOptions {
|
interface UseNotificationFormOptions {
|
||||||
data?: Notification | null;
|
data?: Notification | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildDefaults(data: Notification | null): NotificationFormData {
|
||||||
|
if (data?.type === "matrix") {
|
||||||
|
return {
|
||||||
|
type: "matrix",
|
||||||
|
notificationName: data.notificationName || "",
|
||||||
|
homeserverUrl: data.homeserverUrl || "",
|
||||||
|
roomId: data.roomId || "",
|
||||||
|
accessToken: data.accessToken || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (data?.type === "telegram") {
|
||||||
|
return {
|
||||||
|
type: "telegram",
|
||||||
|
notificationName: data.notificationName || "",
|
||||||
|
address: data.address || "",
|
||||||
|
accessToken: data.accessToken || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (data?.type === "slack") {
|
||||||
|
return {
|
||||||
|
type: "slack",
|
||||||
|
notificationName: data.notificationName || "",
|
||||||
|
address: data.address || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (data?.type === "discord") {
|
||||||
|
return {
|
||||||
|
type: "discord",
|
||||||
|
notificationName: data.notificationName || "",
|
||||||
|
address: data.address || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (data?.type === "webhook") {
|
||||||
|
return {
|
||||||
|
type: "webhook",
|
||||||
|
notificationName: data.notificationName || "",
|
||||||
|
address: data.address || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (data?.type === "pager_duty") {
|
||||||
|
return {
|
||||||
|
type: "pager_duty",
|
||||||
|
notificationName: data.notificationName || "",
|
||||||
|
address: data.address || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (data?.type === "teams") {
|
||||||
|
return {
|
||||||
|
type: "teams",
|
||||||
|
notificationName: data.notificationName || "",
|
||||||
|
address: data.address || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// Default: email (covers both data === null and data.type === "email")
|
||||||
|
return {
|
||||||
|
type: "email",
|
||||||
|
notificationName: data?.notificationName || "",
|
||||||
|
address: data?.address || "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export const useNotificationForm = ({ data = null }: UseNotificationFormOptions = {}) => {
|
export const useNotificationForm = ({ data = null }: UseNotificationFormOptions = {}) => {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const defaults =
|
const defaults = buildDefaults(data);
|
||||||
data?.type === "matrix"
|
|
||||||
? {
|
|
||||||
type: "matrix" as const,
|
|
||||||
notificationName: data.notificationName || "",
|
|
||||||
homeserverUrl: data.homeserverUrl || "",
|
|
||||||
roomId: data.roomId || "",
|
|
||||||
accessToken: data.accessToken || "",
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
type: (data?.type || "email") as Exclude<Notification["type"], "matrix">,
|
|
||||||
notificationName: data?.notificationName || "",
|
|
||||||
address: data?.address || "",
|
|
||||||
};
|
|
||||||
|
|
||||||
return { schema: notificationSchema, defaults };
|
return { schema: notificationSchema, defaults };
|
||||||
}, [data]);
|
}, [data]);
|
||||||
};
|
};
|
||||||
@@ -1,14 +1,14 @@
|
|||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
import { settingsSchema } from "@/Validation/settings";
|
import { settingsSchema } from "@/Validation/settings";
|
||||||
import type { Settings } from "@/Types/Settings";
|
import type { Settings } from "@/Types/Settings";
|
||||||
import type { SettingsFormData } from "@/Validation/settings";
|
import type { SettingsFormInput } from "@/Validation/settings";
|
||||||
|
|
||||||
interface UseSettingsFormOptions {
|
interface UseSettingsFormOptions {
|
||||||
data?: Settings | null;
|
data?: Settings | null;
|
||||||
}
|
}
|
||||||
export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) => {
|
export const useSettingsForm = ({ data = null }: UseSettingsFormOptions = {}) => {
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const defaults: SettingsFormData = {
|
const defaults: SettingsFormInput = {
|
||||||
systemEmailIgnoreTLS: data?.systemEmailIgnoreTLS ?? false,
|
systemEmailIgnoreTLS: data?.systemEmailIgnoreTLS ?? false,
|
||||||
systemEmailRequireTLS: data?.systemEmailRequireTLS ?? false,
|
systemEmailRequireTLS: data?.systemEmailRequireTLS ?? false,
|
||||||
systemEmailRejectUnauthorized: data?.systemEmailRejectUnauthorized ?? true,
|
systemEmailRejectUnauthorized: data?.systemEmailRejectUnauthorized ?? true,
|
||||||
|
|||||||
@@ -27,12 +27,14 @@ export const useStatusPageForm = ({
|
|||||||
companyName: data?.companyName || "",
|
companyName: data?.companyName || "",
|
||||||
url: data?.url || generateDefaultUrl(),
|
url: data?.url || generateDefaultUrl(),
|
||||||
timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
timezone: data?.timezone || Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
|
type: data?.type || ["uptime"],
|
||||||
color: data?.color || "#4169E1",
|
color: data?.color || "#4169E1",
|
||||||
monitors: data?.monitors || [],
|
monitors: data?.monitors || [],
|
||||||
isPublished: data?.isPublished ?? false,
|
isPublished: data?.isPublished ?? false,
|
||||||
showCharts: data?.showCharts ?? true,
|
showCharts: data?.showCharts ?? true,
|
||||||
showUptimePercentage: data?.showUptimePercentage ?? true,
|
showUptimePercentage: data?.showUptimePercentage ?? true,
|
||||||
showAdminLoginLink: data?.showAdminLoginLink ?? false,
|
showAdminLoginLink: data?.showAdminLoginLink ?? false,
|
||||||
|
showInfrastructure: data?.showInfrastructure ?? false,
|
||||||
customCSS: data?.customCSS || "",
|
customCSS: data?.customCSS || "",
|
||||||
logo: transformLogo(data?.logo),
|
logo: transformLogo(data?.logo),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -302,49 +302,49 @@ const CreateMonitorPage = () => {
|
|||||||
>
|
>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="http"
|
value="http"
|
||||||
label={t("pages.createMonitor.form.type.optionHttp")}
|
label={t("pages.common.monitors.monitorTypes.optionHttp")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionHttpDescription"
|
"pages.createMonitor.form.type.optionHttpDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="ping"
|
value="ping"
|
||||||
label={t("pages.createMonitor.form.type.optionPing")}
|
label={t("pages.common.monitors.monitorTypes.optionPing")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionPingDescription"
|
"pages.createMonitor.form.type.optionPingDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="docker"
|
value="docker"
|
||||||
label={t("pages.createMonitor.form.type.optionDocker")}
|
label={t("pages.common.monitors.monitorTypes.optionDocker")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionDockerDescription"
|
"pages.createMonitor.form.type.optionDockerDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="port"
|
value="port"
|
||||||
label={t("pages.createMonitor.form.type.optionPort")}
|
label={t("pages.common.monitors.monitorTypes.optionPort")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionPortDescription"
|
"pages.createMonitor.form.type.optionPortDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="game"
|
value="game"
|
||||||
label={t("pages.createMonitor.form.type.optionGame")}
|
label={t("pages.common.monitors.monitorTypes.optionGame")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionGameDescription"
|
"pages.createMonitor.form.type.optionGameDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="grpc"
|
value="grpc"
|
||||||
label={t("pages.createMonitor.form.type.optionGrpc")}
|
label={t("pages.common.monitors.monitorTypes.optionGrpc")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionGrpcDescription"
|
"pages.createMonitor.form.type.optionGrpcDescription"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<RadioWithDescription
|
<RadioWithDescription
|
||||||
value="websocket"
|
value="websocket"
|
||||||
label={t("pages.createMonitor.form.type.optionWebSocket")}
|
label={t("pages.common.monitors.monitorTypes.optionWebSocket")}
|
||||||
description={t(
|
description={t(
|
||||||
"pages.createMonitor.form.type.optionWebSocketDescription"
|
"pages.createMonitor.form.type.optionWebSocketDescription"
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ const NotificationsCreatePage = () => {
|
|||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
{watchedType !== "matrix" && (
|
{watchedType !== "matrix" && watchedType !== "telegram" && (
|
||||||
<ConfigBox
|
<ConfigBox
|
||||||
title={addressConfig.title}
|
title={addressConfig.title}
|
||||||
subtitle={addressConfig.description}
|
subtitle={addressConfig.description}
|
||||||
@@ -155,7 +155,7 @@ const NotificationsCreatePage = () => {
|
|||||||
<Controller
|
<Controller
|
||||||
name="address"
|
name="address"
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={defaults.address}
|
defaultValue={"address" in defaults ? defaults.address : ""}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...field}
|
{...field}
|
||||||
@@ -171,6 +171,49 @@ const NotificationsCreatePage = () => {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{watchedType === "telegram" && (
|
||||||
|
<ConfigBox
|
||||||
|
title={t("pages.notifications.form.telegram.title")}
|
||||||
|
subtitle={t("pages.notifications.form.telegram.description")}
|
||||||
|
rightContent={
|
||||||
|
<Stack spacing={theme.spacing(8)}>
|
||||||
|
<Controller
|
||||||
|
name="accessToken"
|
||||||
|
control={control}
|
||||||
|
defaultValue={"accessToken" in defaults ? defaults.accessToken : ""}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
fieldLabel={t("pages.notifications.form.telegram.optionBotToken")}
|
||||||
|
placeholder={t(
|
||||||
|
"pages.notifications.form.telegram.placeholderBotToken"
|
||||||
|
)}
|
||||||
|
fullWidth
|
||||||
|
error={!!fieldState.error}
|
||||||
|
helperText={fieldState.error?.message ?? ""}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Controller
|
||||||
|
name="address"
|
||||||
|
control={control}
|
||||||
|
render={({ field, fieldState }) => (
|
||||||
|
<TextField
|
||||||
|
{...field}
|
||||||
|
type="text"
|
||||||
|
fieldLabel={t("pages.notifications.form.telegram.optionChatId")}
|
||||||
|
placeholder={t("pages.notifications.form.telegram.placeholderChatId")}
|
||||||
|
fullWidth
|
||||||
|
error={!!fieldState.error}
|
||||||
|
helperText={fieldState.error?.message ?? ""}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{watchedType === "matrix" && (
|
{watchedType === "matrix" && (
|
||||||
<ConfigBox
|
<ConfigBox
|
||||||
title={t("pages.notifications.form.matrix.title")}
|
title={t("pages.notifications.form.matrix.title")}
|
||||||
@@ -180,7 +223,7 @@ const NotificationsCreatePage = () => {
|
|||||||
<Controller
|
<Controller
|
||||||
name="homeserverUrl"
|
name="homeserverUrl"
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={defaults.homeserverUrl}
|
defaultValue={"homeserverUrl" in defaults ? defaults.homeserverUrl : ""}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...field}
|
{...field}
|
||||||
@@ -196,7 +239,7 @@ const NotificationsCreatePage = () => {
|
|||||||
<Controller
|
<Controller
|
||||||
name="roomId"
|
name="roomId"
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={defaults.roomId}
|
defaultValue={"roomId" in defaults ? defaults.roomId : ""}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...field}
|
{...field}
|
||||||
@@ -212,7 +255,7 @@ const NotificationsCreatePage = () => {
|
|||||||
<Controller
|
<Controller
|
||||||
name="accessToken"
|
name="accessToken"
|
||||||
control={control}
|
control={control}
|
||||||
defaultValue={defaults.accessToken}
|
defaultValue={"accessToken" in defaults ? defaults.accessToken : ""}
|
||||||
render={({ field, fieldState }) => (
|
render={({ field, fieldState }) => (
|
||||||
<TextField
|
<TextField
|
||||||
{...field}
|
{...field}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import { useGet, usePatch, usePost, useLazyGet } from "@/Hooks/UseApi";
|
|||||||
import { useToast } from "@/Hooks/UseToast";
|
import { useToast } from "@/Hooks/UseToast";
|
||||||
import { useSettingsForm } from "@/Hooks/useSettingsForm";
|
import { useSettingsForm } from "@/Hooks/useSettingsForm";
|
||||||
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
|
||||||
import type { SettingsFormData } from "@/Validation/settings";
|
import type { SettingsFormData, SettingsFormInput } from "@/Validation/settings";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
import { TextField, Button, FieldLabel, SliderWithLabel } from "@/Components/inputs";
|
import { TextField, Button, FieldLabel, SliderWithLabel } from "@/Components/inputs";
|
||||||
@@ -89,7 +89,7 @@ export const SettingsPage = () => {
|
|||||||
// Initialize form with schema and defaults
|
// Initialize form with schema and defaults
|
||||||
const { schema, defaults } = useSettingsForm({ data: fetchedSettings?.settings });
|
const { schema, defaults } = useSettingsForm({ data: fetchedSettings?.settings });
|
||||||
|
|
||||||
const form = useForm<SettingsFormData>({
|
const form = useForm<SettingsFormInput, unknown, SettingsFormData>({
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
defaultValues: defaults,
|
defaultValues: defaults,
|
||||||
mode: "onChange",
|
mode: "onChange",
|
||||||
@@ -256,28 +256,7 @@ export const SettingsPage = () => {
|
|||||||
delete (dataToSend as any).systemEmailPassword;
|
delete (dataToSend as any).systemEmailPassword;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert undefined to empty string for backend unsetting
|
const result = await patch("/settings", dataToSend as SettingsFormData);
|
||||||
const processedData = Object.entries(dataToSend).reduce((acc, [key, value]) => {
|
|
||||||
const typedKey = key as keyof SettingsFormData;
|
|
||||||
if (value === undefined) {
|
|
||||||
(acc as any)[typedKey] = "";
|
|
||||||
} else if (typeof value === "object" && value !== null) {
|
|
||||||
// Handle nested objects like globalThresholds
|
|
||||||
// Convert 0 to "" for unsetting thresholds
|
|
||||||
(acc as any)[typedKey] = Object.entries(value).reduce(
|
|
||||||
(nested, [nestedKey, nestedValue]) => ({
|
|
||||||
...nested,
|
|
||||||
[nestedKey]: nestedValue === undefined ? "" : nestedValue,
|
|
||||||
}),
|
|
||||||
{}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
(acc as any)[typedKey] = value;
|
|
||||||
}
|
|
||||||
return acc;
|
|
||||||
}, {} as Partial<SettingsFormData>);
|
|
||||||
|
|
||||||
const result = await patch("/settings", processedData as SettingsFormData);
|
|
||||||
|
|
||||||
if (result?.success) {
|
if (result?.success) {
|
||||||
// Update API key state from response
|
// Update API key state from response
|
||||||
|
|||||||
@@ -21,19 +21,28 @@ import {
|
|||||||
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme } from "@mui/material/styles";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import { useForm, Controller } from "react-hook-form";
|
import { useForm, Controller } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useStatusPageForm } from "@/Hooks/useStatusPageForm";
|
import { useStatusPageForm } from "@/Hooks/useStatusPageForm";
|
||||||
import type { StatusPageFormData } from "@/Validation/statusPage";
|
import type { StatusPageFormData } from "@/Validation/statusPage";
|
||||||
import { useGet, usePost, usePut, useDelete } from "@/Hooks/UseApi";
|
import { useGet, usePost, usePut, useDelete } from "@/Hooks/UseApi";
|
||||||
import type { Monitor } from "@/Types/Monitor";
|
import type { Monitor } from "@/Types/Monitor";
|
||||||
import type { StatusPageResponse } from "@/Types/StatusPage";
|
import type { MonitorDisplayType, StatusPageResponse } from "@/Types/StatusPage";
|
||||||
|
import { getMonitorTypeLabel } from "@/Types/StatusPage";
|
||||||
import timezones from "@/Utils/timezones.json";
|
import timezones from "@/Utils/timezones.json";
|
||||||
import { useNavigate, useParams } from "react-router-dom";
|
import { useNavigate, useParams } from "react-router-dom";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { HeaderConfigStatusControls } from "./Components/HeaderConfigStatusControls";
|
import { HeaderConfigStatusControls } from "./Components/HeaderConfigStatusControls";
|
||||||
|
|
||||||
|
const monitorsUrl = (() => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
["http", "ping", "port", "docker", "game", "grpc", "websocket", "hardware"].forEach(
|
||||||
|
(type) => params.append("type", type)
|
||||||
|
);
|
||||||
|
return `/monitors/team?${params.toString()}`;
|
||||||
|
})();
|
||||||
|
|
||||||
interface TimezoneOption {
|
interface TimezoneOption {
|
||||||
_id: string;
|
_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -49,11 +58,11 @@ const CreateStatusPage = () => {
|
|||||||
|
|
||||||
// Fetch existing status page data when configuring
|
// Fetch existing status page data when configuring
|
||||||
const { data: statusPageData, isLoading: isLoadingStatusPage } =
|
const { data: statusPageData, isLoading: isLoadingStatusPage } =
|
||||||
useGet<StatusPageResponse>(isCreate ? null : `/status-page/${url}?type=uptime`);
|
useGet<StatusPageResponse>(
|
||||||
|
isCreate ? null : `/status-page/${url}?type=uptime&type=infrastructure`
|
||||||
|
);
|
||||||
|
|
||||||
const { data: monitorsResponse } = useGet<Monitor[]>(
|
const { data: monitorsResponse } = useGet<Monitor[]>(monitorsUrl);
|
||||||
"/monitors/team?type=http&type=ping&type=port&type=docker"
|
|
||||||
);
|
|
||||||
const monitors = monitorsResponse ?? [];
|
const monitors = monitorsResponse ?? [];
|
||||||
|
|
||||||
const { post, loading: isSubmittingPost } = usePost();
|
const { post, loading: isSubmittingPost } = usePost();
|
||||||
@@ -81,6 +90,24 @@ const CreateStatusPage = () => {
|
|||||||
reset(defaults);
|
reset(defaults);
|
||||||
}, [defaults, reset]);
|
}, [defaults, reset]);
|
||||||
|
|
||||||
|
const watchedMonitorIds: string[] = form.watch("monitors") ?? [];
|
||||||
|
const computedTypes: MonitorDisplayType[] = useMemo(() => {
|
||||||
|
const selectedMonitors = (watchedMonitorIds ?? [])
|
||||||
|
.map((id) => monitors.find((m) => m.id === id))
|
||||||
|
.filter((m): m is Monitor => m !== undefined);
|
||||||
|
|
||||||
|
const typesSet = new Set<MonitorDisplayType>();
|
||||||
|
selectedMonitors.forEach((m) => {
|
||||||
|
typesSet.add(m.type === "hardware" ? "infrastructure" : "uptime");
|
||||||
|
});
|
||||||
|
|
||||||
|
return typesSet.size ? Array.from(typesSet) : ["uptime"];
|
||||||
|
}, [watchedMonitorIds, monitors]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.setValue("type", computedTypes);
|
||||||
|
}, [computedTypes]);
|
||||||
|
|
||||||
const onError = (errors: any) => {
|
const onError = (errors: any) => {
|
||||||
logger.debug("Status page validation errors", errors);
|
logger.debug("Status page validation errors", errors);
|
||||||
};
|
};
|
||||||
@@ -103,7 +130,6 @@ const CreateStatusPage = () => {
|
|||||||
|
|
||||||
const onSubmit = async (data: StatusPageFormData) => {
|
const onSubmit = async (data: StatusPageFormData) => {
|
||||||
const fd = new FormData();
|
const fd = new FormData();
|
||||||
fd.append("type", "uptime");
|
|
||||||
fd.append("isPublished", String(data.isPublished));
|
fd.append("isPublished", String(data.isPublished));
|
||||||
if (data.companyName) fd.append("companyName", data.companyName);
|
if (data.companyName) fd.append("companyName", data.companyName);
|
||||||
if (data.url) fd.append("url", data.url);
|
if (data.url) fd.append("url", data.url);
|
||||||
@@ -112,11 +138,16 @@ const CreateStatusPage = () => {
|
|||||||
fd.append("showCharts", String(data.showCharts));
|
fd.append("showCharts", String(data.showCharts));
|
||||||
fd.append("showUptimePercentage", String(data.showUptimePercentage));
|
fd.append("showUptimePercentage", String(data.showUptimePercentage));
|
||||||
fd.append("showAdminLoginLink", String(data.showAdminLoginLink));
|
fd.append("showAdminLoginLink", String(data.showAdminLoginLink));
|
||||||
|
fd.append("showInfrastructure", String(data.showInfrastructure));
|
||||||
|
|
||||||
data.monitors.forEach((monitorId) => {
|
data.monitors.forEach((monitorId) => {
|
||||||
fd.append("monitors[]", monitorId);
|
fd.append("monitors[]", monitorId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
data.type.forEach((type) => {
|
||||||
|
fd.append("type[]", type);
|
||||||
|
});
|
||||||
|
|
||||||
// Handle logo upload
|
// Handle logo upload
|
||||||
if (data.logo === null) {
|
if (data.logo === null) {
|
||||||
// Signal to remove the logo
|
// Signal to remove the logo
|
||||||
@@ -303,7 +334,9 @@ const CreateStatusPage = () => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<GripVertical size={20} />
|
<GripVertical size={20} />
|
||||||
<Typography flexGrow={1}>{monitor.name}</Typography>
|
<Typography
|
||||||
|
flexGrow={1}
|
||||||
|
>{`${monitor.name} (${getMonitorTypeLabel(monitor.type, t)})`}</Typography>
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@@ -425,6 +458,23 @@ const CreateStatusPage = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
<Controller
|
||||||
|
name="showInfrastructure"
|
||||||
|
control={control}
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormControlLabel
|
||||||
|
control={
|
||||||
|
<Checkbox
|
||||||
|
checked={field.value}
|
||||||
|
onChange={field.onChange}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={t(
|
||||||
|
"pages.statusPages.form.features.option.showInfrastructure.label"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
{/* <Controller
|
{/* <Controller
|
||||||
name="showUptimePercentage"
|
name="showUptimePercentage"
|
||||||
control={control}
|
control={control}
|
||||||
|
|||||||
@@ -0,0 +1,246 @@
|
|||||||
|
import Stack from "@mui/material/Stack";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
|
import { Gauge } from "@/Components/design-elements";
|
||||||
|
import { useTheme } from "@mui/material/styles";
|
||||||
|
import { useTranslation } from "react-i18next";
|
||||||
|
import prettyBytes from "pretty-bytes";
|
||||||
|
import type { Monitor } from "@/Types/Monitor";
|
||||||
|
import Grid from "@mui/material/Grid";
|
||||||
|
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
|
||||||
|
|
||||||
|
const GAUGE_RADIUS = 60;
|
||||||
|
const GAUGE_STROKE_WIDTH = 12;
|
||||||
|
const PERCENTAGE_MULTIPLIER = 100;
|
||||||
|
|
||||||
|
interface StatusPageMonitor extends Monitor {
|
||||||
|
checks?: Monitor["recentChecks"];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MetricDetail {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MetricConfig {
|
||||||
|
key: string;
|
||||||
|
label: string;
|
||||||
|
hasData: boolean;
|
||||||
|
progress: number;
|
||||||
|
details: MetricDetail[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MetricDetailRow = ({ label, value }: MetricDetail) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
justifyContent="space-between"
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color={theme.palette.text.secondary}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="body2">{value}</Typography>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface MetricItemProps {
|
||||||
|
label: string;
|
||||||
|
progress: number;
|
||||||
|
details?: MetricDetail[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const MetricItem = ({ label, progress, details }: MetricItemProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
size={isSmall ? 12 : 4}
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column",
|
||||||
|
alignItems: "center",
|
||||||
|
textAlign: "center",
|
||||||
|
gap: theme.spacing(SPACING.LG),
|
||||||
|
padding: theme.spacing(LAYOUT.XS),
|
||||||
|
borderRight: isSmall ? "none" : `1px solid ${theme.palette.divider}`,
|
||||||
|
borderBottom: isSmall ? `1px solid ${theme.palette.divider}` : "none",
|
||||||
|
"&:last-child": {
|
||||||
|
borderRight: "none",
|
||||||
|
borderBottom: "none",
|
||||||
|
paddingBottom: theme.spacing(SPACING.LG),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
display="flex"
|
||||||
|
flexDirection="column"
|
||||||
|
alignItems="center"
|
||||||
|
>
|
||||||
|
<Gauge
|
||||||
|
progress={progress}
|
||||||
|
radius={GAUGE_RADIUS}
|
||||||
|
strokeWidth={GAUGE_STROKE_WIDTH}
|
||||||
|
/>
|
||||||
|
<Typography variant="body2">{label}</Typography>
|
||||||
|
</Box>
|
||||||
|
{details && details.length > 0 && (
|
||||||
|
<Box
|
||||||
|
width="100%"
|
||||||
|
paddingX={theme.spacing(LAYOUT.LG)}
|
||||||
|
>
|
||||||
|
{details.map((detail) => (
|
||||||
|
<MetricDetailRow
|
||||||
|
key={detail.label}
|
||||||
|
label={detail.label}
|
||||||
|
value={detail.value}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
type LatestCheck = NonNullable<Monitor["recentChecks"]>[number];
|
||||||
|
|
||||||
|
const buildCpuMetric = (
|
||||||
|
check: LatestCheck,
|
||||||
|
t: (key: string) => string
|
||||||
|
): MetricConfig | null => {
|
||||||
|
if (!check.cpu || typeof check.cpu.usage_percent !== "number") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const usagePercent = (check.cpu.usage_percent ?? 0) * PERCENTAGE_MULTIPLIER;
|
||||||
|
return {
|
||||||
|
key: "cpu",
|
||||||
|
label: t("pages.statusPages.monitorsList.infrastructure.cpu"),
|
||||||
|
hasData: true,
|
||||||
|
progress: usagePercent,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
label: t("pages.statusPages.monitorsList.infrastructure.usage"),
|
||||||
|
value: `${usagePercent.toFixed(2)}%`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildMemoryMetric = (
|
||||||
|
check: LatestCheck,
|
||||||
|
t: (key: string) => string
|
||||||
|
): MetricConfig | null => {
|
||||||
|
if (
|
||||||
|
!check.memory ||
|
||||||
|
typeof check.memory.usage_percent !== "number" ||
|
||||||
|
typeof check.memory.used_bytes !== "number" ||
|
||||||
|
typeof check.memory.total_bytes !== "number"
|
||||||
|
) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
key: "memory",
|
||||||
|
label: t("pages.statusPages.monitorsList.infrastructure.memory"),
|
||||||
|
hasData: true,
|
||||||
|
progress: (check.memory.usage_percent ?? 0) * PERCENTAGE_MULTIPLIER,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
label: t("pages.statusPages.monitorsList.infrastructure.used"),
|
||||||
|
value: prettyBytes(check.memory.used_bytes ?? 0),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("pages.statusPages.monitorsList.infrastructure.total"),
|
||||||
|
value: prettyBytes(check.memory.total_bytes ?? 0),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const buildDiskMetric = (
|
||||||
|
check: LatestCheck,
|
||||||
|
t: (key: string) => string
|
||||||
|
): MetricConfig | null => {
|
||||||
|
if (!check.disk || check.disk.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const disks = check.disk;
|
||||||
|
const avgUsagePercent =
|
||||||
|
(disks.reduce((acc, disk) => acc + (disk?.usage_percent ?? 0), 0) / disks.length) *
|
||||||
|
PERCENTAGE_MULTIPLIER;
|
||||||
|
const totalUsedBytes = disks.reduce((acc, disk) => acc + (disk?.used_bytes ?? 0), 0);
|
||||||
|
const totalTotalBytes = disks.reduce((acc, disk) => acc + (disk?.total_bytes ?? 0), 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
key: "disk",
|
||||||
|
label: t("pages.statusPages.monitorsList.infrastructure.disk"),
|
||||||
|
hasData: true,
|
||||||
|
progress: avgUsagePercent,
|
||||||
|
details: [
|
||||||
|
{
|
||||||
|
label: t("pages.statusPages.monitorsList.infrastructure.used"),
|
||||||
|
value: prettyBytes(totalUsedBytes),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: t("pages.statusPages.monitorsList.infrastructure.total"),
|
||||||
|
value: prettyBytes(totalTotalBytes),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const InfrastructureMetrics = ({ monitor }: { monitor: StatusPageMonitor }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const latestCheck = monitor.recentChecks?.[0] ?? monitor.checks?.[0];
|
||||||
|
|
||||||
|
if (!latestCheck) {
|
||||||
|
return (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color={theme.palette.text.secondary}
|
||||||
|
>
|
||||||
|
{t("pages.statusPages.monitorsList.noData")}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metrics: MetricConfig[] = [
|
||||||
|
buildCpuMetric(latestCheck, t),
|
||||||
|
buildMemoryMetric(latestCheck, t),
|
||||||
|
buildDiskMetric(latestCheck, t),
|
||||||
|
].filter((m): m is MetricConfig => m !== null);
|
||||||
|
|
||||||
|
if (metrics.length === 0) {
|
||||||
|
return (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color={theme.palette.text.secondary}
|
||||||
|
>
|
||||||
|
{t("pages.statusPages.monitorsList.noData")}
|
||||||
|
</Typography>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Grid
|
||||||
|
container
|
||||||
|
alignItems="center"
|
||||||
|
padding={theme.spacing(LAYOUT.XS)}
|
||||||
|
>
|
||||||
|
{metrics.map(({ key, label, progress, details }) => (
|
||||||
|
<MetricItem
|
||||||
|
key={key}
|
||||||
|
label={label}
|
||||||
|
progress={progress}
|
||||||
|
details={details}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,13 +5,16 @@ import Typography from "@mui/material/Typography";
|
|||||||
import { HistogramResponseTime, HeatmapResponseTime } from "@/Components/common";
|
import { HistogramResponseTime, HeatmapResponseTime } from "@/Components/common";
|
||||||
import { StatusLabel, BaseBox } from "@/Components/design-elements";
|
import { StatusLabel, BaseBox } from "@/Components/design-elements";
|
||||||
import { SwitchComponent } from "@/Components/inputs";
|
import { SwitchComponent } from "@/Components/inputs";
|
||||||
|
import { InfrastructureMetrics } from "@/Pages/StatusPage/Status/Components/InfrastructureMetrics";
|
||||||
|
|
||||||
import { useTheme } from "@mui/material/styles";
|
import { useTheme, type Theme } from "@mui/material/styles";
|
||||||
import { useSelector } from "react-redux";
|
import { useSelector } from "react-redux";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { Monitor } from "@/Types/Monitor";
|
import type { Monitor } from "@/Types/Monitor";
|
||||||
import type { StatusPage } from "@/Types/StatusPage";
|
import type { StatusPage } from "@/Types/StatusPage";
|
||||||
|
import { getMonitorTypeLabel } from "@/Types/StatusPage";
|
||||||
import type { RootState } from "@/Types/state";
|
import type { RootState } from "@/Types/state";
|
||||||
|
import { LAYOUT, SPACING } from "@/Utils/Theme/constants";
|
||||||
|
|
||||||
interface StatusPageMonitor extends Monitor {
|
interface StatusPageMonitor extends Monitor {
|
||||||
checks?: Monitor["recentChecks"];
|
checks?: Monitor["recentChecks"];
|
||||||
@@ -22,29 +25,124 @@ interface MonitorsListProps {
|
|||||||
monitors: StatusPageMonitor[];
|
monitors: StatusPageMonitor[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
|
const getMonitorBadgeStyles = (monitorType: string, theme: Theme) => {
|
||||||
|
const bg =
|
||||||
|
monitorType === "hardware" ? theme.palette.info.light : theme.palette.success.light;
|
||||||
|
return {
|
||||||
|
backgroundColor: bg,
|
||||||
|
color: theme.palette.background.paper,
|
||||||
|
padding: `${theme.spacing(SPACING.SM)} ${theme.spacing(SPACING.LG)}`,
|
||||||
|
borderRadius: theme.shape.borderRadius,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const MonitorHeader = ({
|
||||||
|
monitor,
|
||||||
|
showURL,
|
||||||
|
}: {
|
||||||
|
monitor: StatusPageMonitor;
|
||||||
|
showURL: boolean;
|
||||||
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const showURL = useSelector((state: RootState) => state.ui?.showURL);
|
return (
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
justifyContent="space-between"
|
||||||
|
gap={theme.spacing(LAYOUT.XS)}
|
||||||
|
mb={theme.spacing(LAYOUT.XS)}
|
||||||
|
>
|
||||||
|
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
|
||||||
|
<Stack
|
||||||
|
direction="row"
|
||||||
|
alignItems="center"
|
||||||
|
gap={theme.spacing(SPACING.LG)}
|
||||||
|
mb={theme.spacing(SPACING.SM)}
|
||||||
|
>
|
||||||
|
<Typography
|
||||||
|
variant="h6"
|
||||||
|
sx={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
|
||||||
|
>
|
||||||
|
{monitor.name}
|
||||||
|
</Typography>
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
sx={getMonitorBadgeStyles(monitor.type ?? "", theme)}
|
||||||
|
>
|
||||||
|
{getMonitorTypeLabel(monitor.type, t)}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
{showURL && monitor.url && (
|
||||||
|
<Typography
|
||||||
|
variant="body2"
|
||||||
|
color={theme.palette.text.secondary}
|
||||||
|
sx={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}
|
||||||
|
>
|
||||||
|
{monitor.url}
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
<StatusLabel status={monitor.status} />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MonitorContent = ({
|
||||||
|
monitor,
|
||||||
|
statusPage,
|
||||||
|
chartType,
|
||||||
|
}: {
|
||||||
|
monitor: StatusPageMonitor;
|
||||||
|
statusPage: StatusPage;
|
||||||
|
chartType: string;
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
if (monitor.type === "hardware") {
|
||||||
|
if (statusPage.showInfrastructure === false) return null;
|
||||||
|
return <InfrastructureMetrics monitor={monitor} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (statusPage.showCharts === false) return null;
|
||||||
|
|
||||||
|
const checks = monitor.checks?.slice().reverse() ?? [];
|
||||||
|
return (
|
||||||
|
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1, mb: theme.spacing(SPACING.LG) }}>
|
||||||
|
{chartType === "histogram" ? (
|
||||||
|
<HistogramResponseTime
|
||||||
|
height={{ xs: 50, md: 100 }}
|
||||||
|
gap={{ xs: theme.spacing(SPACING.SM), md: theme.spacing(LAYOUT.SM) }}
|
||||||
|
checks={checks}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<HeatmapResponseTime checks={checks} />
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const showURL = useSelector((state: RootState) => state.ui?.showURL);
|
||||||
const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram");
|
const [chartType, setChartType] = useState<"histogram" | "heatmap">("histogram");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack gap={theme.spacing(8)}>
|
<Stack gap={theme.spacing(LAYOUT.MD)}>
|
||||||
{statusPage.showCharts !== false && (
|
{statusPage.showCharts && (
|
||||||
<Stack
|
<Stack
|
||||||
direction={"row"}
|
direction="row"
|
||||||
alignItems={"center"}
|
alignItems="center"
|
||||||
|
gap={theme.spacing(LAYOUT.SM)}
|
||||||
>
|
>
|
||||||
<Typography>{t("pages.statusPages.monitorsList.chartTypeHeatmap")}</Typography>
|
<Typography>{t("pages.statusPages.monitorsList.chartTypeHeatmap")}</Typography>
|
||||||
<SwitchComponent
|
<SwitchComponent
|
||||||
dualOption
|
dualOption
|
||||||
value={chartType}
|
value={chartType}
|
||||||
checked={chartType === "histogram"}
|
checked={chartType === "histogram"}
|
||||||
onChange={(e) => {
|
onChange={(e) => setChartType(e.target.checked ? "histogram" : "heatmap")}
|
||||||
setChartType(e.target.checked ? "histogram" : "heatmap");
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
<Typography>
|
<Typography>
|
||||||
{t("pages.statusPages.monitorsList.chartTypeHistogram")}
|
{t("pages.statusPages.monitorsList.chartTypeHistogram")}
|
||||||
@@ -52,59 +150,22 @@ export const MonitorsList = ({ statusPage, monitors }: MonitorsListProps) => {
|
|||||||
</Stack>
|
</Stack>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{monitors?.map((monitor) => {
|
{monitors.map((monitor) => (
|
||||||
return (
|
<BaseBox
|
||||||
<BaseBox
|
key={monitor.id}
|
||||||
key={monitor.id}
|
padding={theme.spacing(LAYOUT.MD)}
|
||||||
padding={theme.spacing(4)}
|
>
|
||||||
>
|
<MonitorHeader
|
||||||
<Stack
|
monitor={monitor}
|
||||||
direction="row"
|
showURL={showURL}
|
||||||
alignItems="center"
|
/>
|
||||||
justifyContent="space-between"
|
<MonitorContent
|
||||||
gap={theme.spacing(4)}
|
monitor={monitor}
|
||||||
mb={statusPage.showCharts !== false ? theme.spacing(4) : 0}
|
statusPage={statusPage}
|
||||||
>
|
chartType={chartType}
|
||||||
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
|
/>
|
||||||
<Typography
|
</BaseBox>
|
||||||
variant="h6"
|
))}
|
||||||
sx={{
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{monitor.name}
|
|
||||||
</Typography>
|
|
||||||
{showURL && (
|
|
||||||
<Typography
|
|
||||||
variant="body2"
|
|
||||||
color="text.secondary"
|
|
||||||
>
|
|
||||||
{monitor.url}
|
|
||||||
</Typography>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
<StatusLabel status={monitor.status} />
|
|
||||||
</Stack>
|
|
||||||
{statusPage.showCharts !== false && (
|
|
||||||
<Box sx={{ overflow: "hidden", minWidth: 0, flex: 1 }}>
|
|
||||||
{chartType === "histogram" ? (
|
|
||||||
<HistogramResponseTime
|
|
||||||
height={{ xs: 50, md: 100 }}
|
|
||||||
gap={{ xs: theme.spacing(0.5), md: theme.spacing(5) }}
|
|
||||||
checks={monitor?.checks?.slice().reverse() ?? []}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<HeatmapResponseTime
|
|
||||||
checks={monitor?.checks?.slice().reverse() ?? []}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</BaseBox>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -23,16 +23,22 @@ const StatusPageView = () => {
|
|||||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||||
const isPublic = location.pathname.startsWith("/status/public");
|
const isPublic = location.pathname.startsWith("/status/public");
|
||||||
|
|
||||||
const apiUrl = url ? `/status-page/${url}?type=uptime` : null;
|
const apiUrl = url ? `/status-page/${url}?type=uptime&type=infrastructure` : null;
|
||||||
|
|
||||||
const { data, isLoading, error } = useGet<StatusPageResponse>(apiUrl);
|
const { data, isLoading, error } = useGet<StatusPageResponse>(
|
||||||
|
apiUrl,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
refreshInterval: 10000,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
const statusPage = data?.statusPage;
|
const statusPage = data?.statusPage;
|
||||||
const monitors = data?.monitors ?? [];
|
const monitors = data?.monitors ?? [];
|
||||||
|
|
||||||
if (!statusPage) return null;
|
if (!statusPage) return null;
|
||||||
|
|
||||||
if (monitors?.length === 0) {
|
if (monitors.length === 0) {
|
||||||
return (
|
return (
|
||||||
<BasePage
|
<BasePage
|
||||||
loading={isLoading}
|
loading={isLoading}
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ export const StatusPagesTable = ({
|
|||||||
{
|
{
|
||||||
id: "type",
|
id: "type",
|
||||||
content: t("common.table.headers.type"),
|
content: t("common.table.headers.type"),
|
||||||
render: (row) => row.type,
|
render: (row) => row.type.join(", "),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "status",
|
id: "status",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ export const NotificationChannels = [
|
|||||||
"pager_duty",
|
"pager_duty",
|
||||||
"matrix",
|
"matrix",
|
||||||
"teams",
|
"teams",
|
||||||
|
"telegram",
|
||||||
] as const;
|
] as const;
|
||||||
export type NotificationChannel = (typeof NotificationChannels)[number];
|
export type NotificationChannel = (typeof NotificationChannels)[number];
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,31 @@
|
|||||||
import type { Monitor } from "@/Types/Monitor";
|
import type { Monitor, MonitorType } from "@/Types/Monitor";
|
||||||
|
export type MonitorDisplayType = "uptime" | "infrastructure";
|
||||||
|
|
||||||
|
export const MONITOR_TYPE_KEYS: Partial<Record<MonitorType, string>> = {
|
||||||
|
http: "pages.common.monitors.monitorTypes.optionHttp",
|
||||||
|
ping: "pages.common.monitors.monitorTypes.optionPing",
|
||||||
|
docker: "pages.common.monitors.monitorTypes.optionDocker",
|
||||||
|
port: "pages.common.monitors.monitorTypes.optionPort",
|
||||||
|
game: "pages.common.monitors.monitorTypes.optionGame",
|
||||||
|
grpc: "pages.common.monitors.monitorTypes.optionGrpc",
|
||||||
|
websocket: "pages.common.monitors.monitorTypes.optionWebSocket",
|
||||||
|
hardware: "pages.common.monitors.monitorTypes.optionHardware",
|
||||||
|
pagespeed: "pages.common.monitors.monitorTypes.optionPagespeed",
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMonitorTypeLabel = (
|
||||||
|
type: MonitorType,
|
||||||
|
t: (key: string) => string
|
||||||
|
): string => {
|
||||||
|
const key = MONITOR_TYPE_KEYS[type];
|
||||||
|
return key ? t(key) : type;
|
||||||
|
};
|
||||||
|
|
||||||
export interface StatusPage {
|
export interface StatusPage {
|
||||||
id: string;
|
id: string;
|
||||||
userId: string;
|
userId: string;
|
||||||
teamId: string;
|
teamId: string;
|
||||||
type: string;
|
type: MonitorDisplayType[];
|
||||||
companyName: string;
|
companyName: string;
|
||||||
url: string;
|
url: string;
|
||||||
timezone?: string;
|
timezone?: string;
|
||||||
@@ -20,6 +41,7 @@ export interface StatusPage {
|
|||||||
showCharts: boolean;
|
showCharts: boolean;
|
||||||
showUptimePercentage: boolean;
|
showUptimePercentage: boolean;
|
||||||
showAdminLoginLink: boolean;
|
showAdminLoginLink: boolean;
|
||||||
|
showInfrastructure: boolean;
|
||||||
customCSS: string;
|
customCSS: string;
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
|
|||||||
@@ -50,6 +50,12 @@ const teamsSchema = baseSchema.extend({
|
|||||||
address: z.string().min(1, "Webhook URL is required").url("Please enter a valid URL"),
|
address: z.string().min(1, "Webhook URL is required").url("Please enter a valid URL"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const telegramSchema = baseSchema.extend({
|
||||||
|
type: z.literal("telegram"),
|
||||||
|
address: z.string().min(1, "Chat ID is required"),
|
||||||
|
accessToken: z.string().min(1, "Bot token is required"),
|
||||||
|
});
|
||||||
|
|
||||||
export const notificationSchema = z.discriminatedUnion("type", [
|
export const notificationSchema = z.discriminatedUnion("type", [
|
||||||
emailSchema,
|
emailSchema,
|
||||||
slackSchema,
|
slackSchema,
|
||||||
@@ -58,6 +64,7 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
|||||||
pagerDutySchema,
|
pagerDutySchema,
|
||||||
matrixSchema,
|
matrixSchema,
|
||||||
teamsSchema,
|
teamsSchema,
|
||||||
|
telegramSchema,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export type NotificationFormData = z.infer<typeof notificationSchema>;
|
export type NotificationFormData = z.infer<typeof notificationSchema>;
|
||||||
@@ -7,7 +7,7 @@ export const settingsSchema = z.object({
|
|||||||
systemEmailRejectUnauthorized: z.boolean(),
|
systemEmailRejectUnauthorized: z.boolean(),
|
||||||
systemEmailConnectionHost: z
|
systemEmailConnectionHost: z
|
||||||
.string()
|
.string()
|
||||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||||
.optional(),
|
.optional(),
|
||||||
systemEmailSecure: z.boolean().optional(),
|
systemEmailSecure: z.boolean().optional(),
|
||||||
systemEmailPool: z.boolean().optional(),
|
systemEmailPool: z.boolean().optional(),
|
||||||
@@ -19,31 +19,30 @@ export const settingsSchema = z.object({
|
|||||||
.max(CHECK_TTL_SENTINEL, `Maximum ${CHECK_TTL_SENTINEL}`),
|
.max(CHECK_TTL_SENTINEL, `Maximum ${CHECK_TTL_SENTINEL}`),
|
||||||
pagespeedApiKey: z
|
pagespeedApiKey: z
|
||||||
.string()
|
.string()
|
||||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||||
.optional(),
|
.optional(),
|
||||||
systemEmailHost: z
|
systemEmailHost: z
|
||||||
.string()
|
.string()
|
||||||
.regex(/^[a-zA-Z0-9.-]*$/, "Invalid hostname or IP address")
|
.regex(/^[a-zA-Z0-9.-]*$/, "Invalid hostname or IP address")
|
||||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||||
.optional(),
|
.optional(),
|
||||||
systemEmailPort: z.number().int().min(1).max(65535).optional(),
|
systemEmailPort: z.number().int().min(1).max(65535).optional(),
|
||||||
systemEmailAddress: z
|
systemEmailAddress: z
|
||||||
.string()
|
|
||||||
.email("Please enter a valid email address")
|
.email("Please enter a valid email address")
|
||||||
.or(z.literal(""))
|
.or(z.literal(""))
|
||||||
.transform((val) => (val === "" ? undefined : val.toLowerCase().trim()))
|
.transform((val) => (val === "" ? null : val.toLowerCase().trim()))
|
||||||
.optional(),
|
.optional(),
|
||||||
systemEmailUser: z
|
systemEmailUser: z
|
||||||
.string()
|
.string()
|
||||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||||
.optional(),
|
.optional(),
|
||||||
systemEmailPassword: z
|
systemEmailPassword: z
|
||||||
.string()
|
.string()
|
||||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||||
.optional(),
|
.optional(),
|
||||||
systemEmailTLSServername: z
|
systemEmailTLSServername: z
|
||||||
.string()
|
.string()
|
||||||
.transform((val) => (val.trim() === "" ? undefined : val.trim()))
|
.transform((val) => (val.trim() === "" ? null : val.trim()))
|
||||||
.optional(),
|
.optional(),
|
||||||
globalThresholds: z.object({
|
globalThresholds: z.object({
|
||||||
cpu: z.number().int().min(1).max(100),
|
cpu: z.number().int().min(1).max(100),
|
||||||
@@ -53,4 +52,5 @@ export const settingsSchema = z.object({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export type SettingsFormInput = z.input<typeof settingsSchema>;
|
||||||
export type SettingsFormData = z.infer<typeof settingsSchema>;
|
export type SettingsFormData = z.infer<typeof settingsSchema>;
|
||||||
@@ -14,12 +14,16 @@ export const statusPageSchema = z.object({
|
|||||||
"URL can only contain lowercase letters, numbers, and hyphens"
|
"URL can only contain lowercase letters, numbers, and hyphens"
|
||||||
),
|
),
|
||||||
timezone: z.string().optional(),
|
timezone: z.string().optional(),
|
||||||
|
type: z
|
||||||
|
.array(z.enum(["uptime", "infrastructure"]))
|
||||||
|
.min(1, "At least one type is required"),
|
||||||
color: z.string().min(1, "Color is required"),
|
color: z.string().min(1, "Color is required"),
|
||||||
monitors: z.array(z.string()).min(1, "At least one monitor is required"),
|
monitors: z.array(z.string()).min(1, "At least one monitor is required"),
|
||||||
isPublished: z.boolean(),
|
isPublished: z.boolean(),
|
||||||
showCharts: z.boolean(),
|
showCharts: z.boolean(),
|
||||||
showUptimePercentage: z.boolean(),
|
showUptimePercentage: z.boolean(),
|
||||||
showAdminLoginLink: z.boolean(),
|
showAdminLoginLink: z.boolean(),
|
||||||
|
showInfrastructure: z.boolean(),
|
||||||
customCSS: z.string().optional(),
|
customCSS: z.string().optional(),
|
||||||
logo: z
|
logo: z
|
||||||
.object({
|
.object({
|
||||||
|
|||||||
@@ -288,7 +288,13 @@ const logoImageValidation = joi
|
|||||||
.optional(); // Make entire object optional
|
.optional(); // Make entire object optional
|
||||||
|
|
||||||
const statusPageValidation = joi.object({
|
const statusPageValidation = joi.object({
|
||||||
type: joi.string().valid("uptime").required(),
|
type: joi
|
||||||
|
.alternatives()
|
||||||
|
.try(
|
||||||
|
joi.string().valid("uptime", "infrastructure"),
|
||||||
|
joi.array().items(joi.string().valid("uptime", "infrastructure"))
|
||||||
|
)
|
||||||
|
.required(),
|
||||||
isPublished: joi.bool(),
|
isPublished: joi.bool(),
|
||||||
companyName: joi
|
companyName: joi
|
||||||
.string()
|
.string()
|
||||||
@@ -317,6 +323,7 @@ const statusPageValidation = joi.object({
|
|||||||
showUptimePercentage: joi.boolean(),
|
showUptimePercentage: joi.boolean(),
|
||||||
showCharts: joi.boolean(),
|
showCharts: joi.boolean(),
|
||||||
showAdminLoginLink: joi.boolean(),
|
showAdminLoginLink: joi.boolean(),
|
||||||
|
showInfrastructure: joi.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const settingsValidation = joi.object({
|
const settingsValidation = joi.object({
|
||||||
|
|||||||
+1281
-1084
File diff suppressed because it is too large.
Load diff
+1281
-1084
File diff suppressed because it is too large.
Load diff
+1271
-1106
File diff suppressed because it is too large.
Load diff
@@ -401,12 +401,23 @@
|
|||||||
},
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"down": "down",
|
"down": "down",
|
||||||
"breached": "breached",
|
"breached": "threshold exceeded",
|
||||||
"initializing": "initializing",
|
"initializing": "initializing",
|
||||||
"maintenance": "maintenance",
|
"maintenance": "maintenance",
|
||||||
"paused": "paused",
|
"paused": "paused",
|
||||||
"total": "total",
|
"total": "total",
|
||||||
"up": "up"
|
"up": "up"
|
||||||
|
},
|
||||||
|
"monitorTypes": {
|
||||||
|
"optionDocker": "Docker",
|
||||||
|
"optionGame": "Game",
|
||||||
|
"optionHttp": "HTTP(S)",
|
||||||
|
"optionPing": "Ping",
|
||||||
|
"optionPort": "Port",
|
||||||
|
"optionPagespeed": "PageSpeed",
|
||||||
|
"optionHardware": "Infrastructure",
|
||||||
|
"optionGrpc": "gRPC",
|
||||||
|
"optionWebSocket": "WebSocket"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -534,19 +545,12 @@
|
|||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"description": "Select the type of check to perform",
|
"description": "Select the type of check to perform",
|
||||||
"optionDocker": "Docker",
|
|
||||||
"optionDockerDescription": "Use Docker to monitor if a container is running.",
|
"optionDockerDescription": "Use Docker to monitor if a container is running.",
|
||||||
"optionGame": "Game",
|
|
||||||
"optionGameDescription": "Monitor if a specific game server is online.",
|
"optionGameDescription": "Monitor if a specific game server is online.",
|
||||||
"optionGrpc": "gRPC",
|
|
||||||
"optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.",
|
"optionGrpcDescription": "Monitor gRPC services using the standard Health Checking Protocol.",
|
||||||
"optionWebSocket": "WebSocket",
|
|
||||||
"optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.",
|
"optionWebSocketDescription": "Monitor WebSocket endpoints for connection health and response time.",
|
||||||
"optionHttp": "HTTP(S)",
|
|
||||||
"optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
|
"optionHttpDescription": "Use HTTP(S) to monitor your website or API endpoint.",
|
||||||
"optionPing": "Ping",
|
|
||||||
"optionPingDescription": "Use ICMP Ping to monitor if a server is online.",
|
"optionPingDescription": "Use ICMP Ping to monitor if a server is online.",
|
||||||
"optionPort": "Port",
|
|
||||||
"optionPortDescription": "Monitor if a specific port on a server is open.",
|
"optionPortDescription": "Monitor if a specific port on a server is open.",
|
||||||
"title": "Type"
|
"title": "Type"
|
||||||
},
|
},
|
||||||
@@ -947,6 +951,14 @@
|
|||||||
"description": "Select the type of notification channel to create.",
|
"description": "Select the type of notification channel to create.",
|
||||||
"optionType": "Type",
|
"optionType": "Type",
|
||||||
"title": "Channel type"
|
"title": "Channel type"
|
||||||
|
},
|
||||||
|
"telegram": {
|
||||||
|
"title": "Telegram configuration",
|
||||||
|
"description": "Configure your Telegram bot to send notifications.",
|
||||||
|
"optionBotToken": "Bot token",
|
||||||
|
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
|
||||||
|
"optionChatId": "Chat ID",
|
||||||
|
"placeholderChatId": "-1001234567890"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"table": {
|
"table": {
|
||||||
@@ -1178,7 +1190,22 @@
|
|||||||
},
|
},
|
||||||
"monitorsList": {
|
"monitorsList": {
|
||||||
"chartTypeHeatmap": "Heatmap",
|
"chartTypeHeatmap": "Heatmap",
|
||||||
"chartTypeHistogram": "Histogram"
|
"chartTypeHistogram": "Histogram",
|
||||||
|
"noData": "No data available",
|
||||||
|
"infrastructure": {
|
||||||
|
"title": "Infrastructure",
|
||||||
|
"cpu": "CPU",
|
||||||
|
"memory": "RAM",
|
||||||
|
"memoryText": "Memory",
|
||||||
|
"disk": "Disk",
|
||||||
|
"usage": "Usage",
|
||||||
|
"used": "Used",
|
||||||
|
"total": "Total"
|
||||||
|
},
|
||||||
|
"uptime": {
|
||||||
|
"title": "Uptime",
|
||||||
|
"responseTime": "Response time"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"statusBar": {
|
"statusBar": {
|
||||||
"allDown": "All systems are down",
|
"allDown": "All systems are down",
|
||||||
@@ -1270,6 +1297,9 @@
|
|||||||
},
|
},
|
||||||
"showAdminLoginLink": {
|
"showAdminLoginLink": {
|
||||||
"label": "Show admin login link"
|
"label": "Show admin login link"
|
||||||
|
},
|
||||||
|
"showInfrastructure": {
|
||||||
|
"label": "Show infrastructure metrics"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1281
-1084
File diff suppressed because it is too large.
Load diff
+1281
-1084
File diff suppressed because it is too large.
Load diff
+1271
-1106
File diff suppressed because it is too large.
Load diff
+1271
-1085
File diff suppressed because it is too large.
Load diff
+1271
-1106
File diff suppressed because it is too large.
Load diff
+1271
-1106
File diff suppressed because it is too large.
Load diff
+1271
-1106
File diff suppressed because it is too large.
Load diff
+1281
-1084
File diff suppressed because it is too large.
Load diff
+1268
-801
File diff suppressed because it is too large.
Load diff
+1268
-801
File diff suppressed because it is too large.
Load diff
+1271
-1098
File diff suppressed because it is too large.
Load diff
+1299
-1088
File diff suppressed because it is too large.
Load diff
@@ -32,11 +32,14 @@ export default [
|
|||||||
languageOptions: {
|
languageOptions: {
|
||||||
parser: tseslint.parser,
|
parser: tseslint.parser,
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
projectService: true,
|
projectService: {
|
||||||
|
allowDefaultProject: ["jest.config.ts"],
|
||||||
|
},
|
||||||
tsconfigRootDir: import.meta.dirname,
|
tsconfigRootDir: import.meta.dirname,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
|
"@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_" }], // Ignore unused variables that start with '_'
|
||||||
"@typescript-eslint/no-explicit-any": "warn", // Warn on 'any' types
|
"@typescript-eslint/no-explicit-any": "warn", // Warn on 'any' types
|
||||||
"mocha/max-top-level-suites": "warn", // Warn if there are too many top-level suites instead of failing
|
"mocha/max-top-level-suites": "warn", // Warn if there are too many top-level suites instead of failing
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -9,8 +9,7 @@ const config: Config = {
|
|||||||
},
|
},
|
||||||
moduleNameMapper: {
|
moduleNameMapper: {
|
||||||
"^@/validation/(.*)\\.js$": "<rootDir>/src/validation/$1.js",
|
"^@/validation/(.*)\\.js$": "<rootDir>/src/validation/$1.js",
|
||||||
"^@/utils/(AppError)\\.js$": "<rootDir>/src/utils/$1.ts",
|
"^@/utils/(.*)\\.js$": "<rootDir>/src/utils/$1.ts",
|
||||||
"^@/utils/(.*)\\.js$": "<rootDir>/src/utils/$1.js",
|
|
||||||
"^@/(.*)\\.ts$": "<rootDir>/src/$1.ts",
|
"^@/(.*)\\.ts$": "<rootDir>/src/$1.ts",
|
||||||
"^@/(.*)\\.js$": "<rootDir>/src/$1.ts",
|
"^@/(.*)\\.js$": "<rootDir>/src/$1.ts",
|
||||||
"^@/(.*)$": "<rootDir>/src/$1",
|
"^@/(.*)$": "<rootDir>/src/$1",
|
||||||
|
|||||||
+174
-137
@@ -510,42 +510,24 @@
|
|||||||
"summary": "Get monitors by team",
|
"summary": "Get monitors by team",
|
||||||
"description": "Get monitors filtered by team with optional parameters",
|
"description": "Get monitors filtered by team with optional parameters",
|
||||||
"parameters": [
|
"parameters": [
|
||||||
{
|
|
||||||
"name": "status",
|
|
||||||
"in": "query",
|
|
||||||
"description": "Filter by monitor status (up/down)",
|
|
||||||
"schema": {
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "type",
|
"name": "type",
|
||||||
"in": "query",
|
"in": "query",
|
||||||
"description": "Filter by monitor type",
|
"description": "Filter by monitor type",
|
||||||
"schema": {
|
"schema": {
|
||||||
"type": "string",
|
"oneOf": [
|
||||||
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"]
|
{
|
||||||
}
|
"type": "string",
|
||||||
},
|
"enum": ["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"]
|
||||||
{
|
},
|
||||||
"name": "page",
|
{
|
||||||
"in": "query",
|
"type": "array",
|
||||||
"description": "Page number for pagination",
|
"items": {
|
||||||
"schema": {
|
"type": "string",
|
||||||
"type": "integer",
|
"enum": ["http", "ping", "pagespeed", "docker", "hardware", "port", "game", "grpc", "websocket"]
|
||||||
"minimum": 1,
|
}
|
||||||
"default": 1
|
}
|
||||||
}
|
]
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "rowsPerPage",
|
|
||||||
"in": "query",
|
|
||||||
"description": "Number of monitors per page",
|
|
||||||
"schema": {
|
|
||||||
"type": "integer",
|
|
||||||
"minimum": 1,
|
|
||||||
"maximum": 100,
|
|
||||||
"default": 10
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -555,15 +537,6 @@
|
|||||||
"schema": {
|
"schema": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
{
|
|
||||||
"name": "field",
|
|
||||||
"in": "query",
|
|
||||||
"description": "Field to filter on",
|
|
||||||
"schema": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["name", "url", "description"]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
@@ -756,7 +729,7 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"put": {
|
"patch": {
|
||||||
"tags": ["monitors"],
|
"tags": ["monitors"],
|
||||||
"summary": "Update monitor",
|
"summary": "Update monitor",
|
||||||
"description": "Update an existing monitor",
|
"description": "Update an existing monitor",
|
||||||
@@ -3568,22 +3541,19 @@
|
|||||||
},
|
},
|
||||||
"CreateMonitorRequest": {
|
"CreateMonitorRequest": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"required": ["name", "description", "type", "url", "interval"],
|
"required": ["name", "type", "url"],
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1,
|
|
||||||
"maxLength": 100,
|
|
||||||
"example": "My Website Monitor"
|
"example": "My Website Monitor"
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"maxLength": 500,
|
|
||||||
"example": "Monitors the main website homepage"
|
"example": "Monitors the main website homepage"
|
||||||
},
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port"],
|
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket"],
|
||||||
"example": "http"
|
"example": "http"
|
||||||
},
|
},
|
||||||
"url": {
|
"url": {
|
||||||
@@ -3593,14 +3563,34 @@
|
|||||||
},
|
},
|
||||||
"interval": {
|
"interval": {
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"minimum": 30,
|
|
||||||
"maximum": 3600,
|
|
||||||
"example": 300,
|
"example": 300,
|
||||||
"description": "Check interval in seconds"
|
"description": "Check interval in seconds"
|
||||||
},
|
},
|
||||||
|
"statusWindowSize": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 20,
|
||||||
|
"default": 5
|
||||||
|
},
|
||||||
|
"statusWindowThreshold": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 100,
|
||||||
|
"default": 60
|
||||||
|
},
|
||||||
"isActive": {
|
"isActive": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"ignoreTlsErrors": {
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"default": true
|
"default": false
|
||||||
|
},
|
||||||
|
"useAdvancedMatching": {
|
||||||
|
"type": "boolean",
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "integer"
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@@ -3609,51 +3599,62 @@
|
|||||||
},
|
},
|
||||||
"description": "Array of notification IDs to associate with this monitor"
|
"description": "Array of notification IDs to associate with this monitor"
|
||||||
},
|
},
|
||||||
"httpOptions": {
|
"secret": {
|
||||||
"type": "object",
|
"type": "string"
|
||||||
"properties": {
|
|
||||||
"method": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["GET", "POST", "PUT", "DELETE", "HEAD"],
|
|
||||||
"default": "GET"
|
|
||||||
},
|
|
||||||
"headers": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"body": {
|
|
||||||
"type": "string",
|
|
||||||
"description": "Request body for POST/PUT requests"
|
|
||||||
},
|
|
||||||
"timeout": {
|
|
||||||
"type": "integer",
|
|
||||||
"minimum": 1000,
|
|
||||||
"maximum": 30000,
|
|
||||||
"default": 5000,
|
|
||||||
"description": "Request timeout in milliseconds"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"assertions": {
|
"jsonPath": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expectedValue": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"matchMethod": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["equal", "include", "regex", ""]
|
||||||
|
},
|
||||||
|
"cpuAlertThreshold": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"memoryAlertThreshold": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"diskAlertThreshold": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"tempAlertThreshold": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"gameId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"grpcServiceName": {
|
||||||
|
"type": "string",
|
||||||
|
"default": ""
|
||||||
|
},
|
||||||
|
"selectedDisks": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "string"
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["status-code", "response-time", "body-contains", "header-contains"]
|
|
||||||
},
|
|
||||||
"comparison": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["equals", "not-equals", "greater-than", "less-than", "contains", "not-contains"]
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"group": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"maxLength": 50
|
||||||
|
},
|
||||||
|
"geoCheckEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"geoCheckLocations": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["northAmerica", "southAmerica", "europe", "asia", "africa", "oceania"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"geoCheckInterval": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 300000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -3661,21 +3662,32 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"name": {
|
"name": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"minLength": 1,
|
"enum": ["http", "ping", "pagespeed", "hardware", "docker", "port", "game", "grpc", "websocket", "unknown"]
|
||||||
"maxLength": 100
|
},
|
||||||
|
"url": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"statusWindowSize": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 20,
|
||||||
|
"default": 5
|
||||||
|
},
|
||||||
|
"statusWindowThreshold": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 100,
|
||||||
|
"default": 60
|
||||||
},
|
},
|
||||||
"description": {
|
"description": {
|
||||||
"type": "string",
|
"type": "string"
|
||||||
"maxLength": 500
|
|
||||||
},
|
},
|
||||||
"interval": {
|
"interval": {
|
||||||
"type": "integer",
|
"type": "integer"
|
||||||
"minimum": 30,
|
|
||||||
"maximum": 3600
|
|
||||||
},
|
|
||||||
"isActive": {
|
|
||||||
"type": "boolean"
|
|
||||||
},
|
},
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
@@ -3683,47 +3695,72 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"httpOptions": {
|
"secret": {
|
||||||
"type": "object",
|
"type": "string"
|
||||||
"properties": {
|
|
||||||
"method": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["GET", "POST", "PUT", "DELETE", "HEAD"]
|
|
||||||
},
|
|
||||||
"headers": {
|
|
||||||
"type": "object",
|
|
||||||
"additionalProperties": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"body": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"timeout": {
|
|
||||||
"type": "integer",
|
|
||||||
"minimum": 1000,
|
|
||||||
"maximum": 30000
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"assertions": {
|
"ignoreTlsErrors": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"useAdvancedMatching": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"jsonPath": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"expectedValue": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"matchMethod": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["equal", "include", "regex", ""]
|
||||||
|
},
|
||||||
|
"port": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 1,
|
||||||
|
"maximum": 65535
|
||||||
|
},
|
||||||
|
"cpuAlertThreshold": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"memoryAlertThreshold": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"diskAlertThreshold": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"tempAlertThreshold": {
|
||||||
|
"type": "number"
|
||||||
|
},
|
||||||
|
"gameId": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"grpcServiceName": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"selectedDisks": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "string"
|
||||||
"properties": {
|
|
||||||
"type": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["status-code", "response-time", "body-contains", "header-contains"]
|
|
||||||
},
|
|
||||||
"comparison": {
|
|
||||||
"type": "string",
|
|
||||||
"enum": ["equals", "not-equals", "greater-than", "less-than", "contains", "not-contains"]
|
|
||||||
},
|
|
||||||
"value": {
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"group": {
|
||||||
|
"type": "string",
|
||||||
|
"nullable": true,
|
||||||
|
"maxLength": 50
|
||||||
|
},
|
||||||
|
"geoCheckEnabled": {
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"geoCheckLocations": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["northAmerica", "southAmerica", "europe", "asia", "africa", "oceania"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"geoCheckInterval": {
|
||||||
|
"type": "integer",
|
||||||
|
"minimum": 300000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Generated
+267
-200
@@ -37,10 +37,11 @@
|
|||||||
"mongoose": "^8.3.3",
|
"mongoose": "^8.3.3",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"nodemailer": "8.0.1",
|
"nodemailer": "8.0.1",
|
||||||
|
"pg": "8.20.0",
|
||||||
"ping": "0.4.4",
|
"ping": "0.4.4",
|
||||||
"sharp": "0.33.5",
|
"sharp": "0.33.5",
|
||||||
"ssl-checker": "2.0.10",
|
"ssl-checker": "2.0.10",
|
||||||
"super-simple-scheduler": "1.4.5",
|
"super-simple-scheduler": "1.9.1",
|
||||||
"swagger-ui-express": "5.0.1",
|
"swagger-ui-express": "5.0.1",
|
||||||
"winston": "^3.13.0",
|
"winston": "^3.13.0",
|
||||||
"ws": "^8.19.0",
|
"ws": "^8.19.0",
|
||||||
@@ -62,12 +63,12 @@
|
|||||||
"@types/multer": "^2.0.0",
|
"@types/multer": "^2.0.0",
|
||||||
"@types/nodemailer": "7.0.1",
|
"@types/nodemailer": "7.0.1",
|
||||||
"@types/papaparse": "^5.5.2",
|
"@types/papaparse": "^5.5.2",
|
||||||
|
"@types/pg": "8.20.0",
|
||||||
"@types/ping": "0.4.4",
|
"@types/ping": "0.4.4",
|
||||||
"@types/swagger-ui-express": "4.1.8",
|
"@types/swagger-ui-express": "4.1.8",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||||
"@typescript-eslint/parser": "^8.56.1",
|
"@typescript-eslint/parser": "^8.56.1",
|
||||||
"c8": "10.1.3",
|
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.17.0",
|
||||||
"eslint-plugin-mocha": "^10.5.0",
|
"eslint-plugin-mocha": "^10.5.0",
|
||||||
"esm": "3.2.25",
|
"esm": "3.2.25",
|
||||||
@@ -799,40 +800,20 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@aws-sdk/xml-builder": {
|
"node_modules/@aws-sdk/xml-builder": {
|
||||||
"version": "3.972.10",
|
"version": "3.972.16",
|
||||||
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.10.tgz",
|
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.16.tgz",
|
||||||
"integrity": "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA==",
|
"integrity": "sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@smithy/types": "^4.13.0",
|
"@smithy/types": "^4.13.1",
|
||||||
"fast-xml-parser": "5.4.1",
|
"fast-xml-parser": "5.5.8",
|
||||||
"tslib": "^2.6.2"
|
"tslib": "^2.6.2"
|
||||||
},
|
},
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=20.0.0"
|
"node": ">=20.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@aws-sdk/xml-builder/node_modules/fast-xml-parser": {
|
|
||||||
"version": "5.4.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.4.1.tgz",
|
|
||||||
"integrity": "sha512-BQ30U1mKkvXQXXkAGcuyUA/GA26oEB7NzOtsxCDtyu62sjGw5QraKFhx2Em3WQNjPw9PG6MQ9yuIIgkSDfGu5A==",
|
|
||||||
"dev": true,
|
|
||||||
"funding": [
|
|
||||||
{
|
|
||||||
"type": "github",
|
|
||||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"fast-xml-builder": "^1.0.0",
|
|
||||||
"strnum": "^2.1.2"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"fxparser": "src/cli/cli.js"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@aws/lambda-invoke-store": {
|
"node_modules/@aws/lambda-invoke-store": {
|
||||||
"version": "0.2.3",
|
"version": "0.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz",
|
||||||
@@ -873,6 +854,7 @@
|
|||||||
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
"integrity": "sha512-H3mcG6ZDLTlYfaSNi0iOKkigqMFvkTKlGUYlD8GW7nNOYRrevuA46iTypPyv+06V3fEmvvazfntkBU34L0azAw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/code-frame": "^7.28.6",
|
"@babel/code-frame": "^7.28.6",
|
||||||
"@babel/generator": "^7.28.6",
|
"@babel/generator": "^7.28.6",
|
||||||
@@ -1375,16 +1357,6 @@
|
|||||||
"integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==",
|
"integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==",
|
||||||
"license": "Apache-2.0"
|
"license": "Apache-2.0"
|
||||||
},
|
},
|
||||||
"node_modules/@bcoe/v8-coverage": {
|
|
||||||
"version": "1.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
|
|
||||||
"integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/@colors/colors": {
|
"node_modules/@colors/colors": {
|
||||||
"version": "1.6.0",
|
"version": "1.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
||||||
@@ -1502,6 +1474,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
},
|
},
|
||||||
@@ -1546,6 +1519,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18"
|
"node": ">=18"
|
||||||
}
|
}
|
||||||
@@ -3850,9 +3824,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@smithy/types": {
|
"node_modules/@smithy/types": {
|
||||||
"version": "4.13.0",
|
"version": "4.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.0.tgz",
|
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz",
|
||||||
"integrity": "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==",
|
"integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -4337,6 +4311,7 @@
|
|||||||
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
|
"integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@types/body-parser": "*",
|
"@types/body-parser": "*",
|
||||||
"@types/express-serve-static-core": "^5.0.0",
|
"@types/express-serve-static-core": "^5.0.0",
|
||||||
@@ -4497,6 +4472,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
|
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz",
|
||||||
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
"integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"undici-types": "~7.16.0"
|
"undici-types": "~7.16.0"
|
||||||
}
|
}
|
||||||
@@ -4522,6 +4498,18 @@
|
|||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/@types/ping": {
|
||||||
"version": "0.4.4",
|
"version": "0.4.4",
|
||||||
"resolved": "https://registry.npmjs.org/@types/ping/-/ping-0.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/@types/ping/-/ping-0.4.4.tgz",
|
||||||
@@ -4734,6 +4722,7 @@
|
|||||||
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.56.1",
|
"@typescript-eslint/scope-manager": "8.56.1",
|
||||||
"@typescript-eslint/types": "8.56.1",
|
"@typescript-eslint/types": "8.56.1",
|
||||||
@@ -4942,9 +4931,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||||
"version": "5.0.4",
|
"version": "5.0.5",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||||
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
|
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5347,6 +5336,7 @@
|
|||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -5884,9 +5874,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/brace-expansion": {
|
"node_modules/brace-expansion": {
|
||||||
"version": "1.1.12",
|
"version": "1.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -5925,6 +5915,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"baseline-browser-mapping": "^2.8.19",
|
"baseline-browser-mapping": "^2.8.19",
|
||||||
"caniuse-lite": "^1.0.30001751",
|
"caniuse-lite": "^1.0.30001751",
|
||||||
@@ -6051,40 +6042,6 @@
|
|||||||
"node": ">= 0.8"
|
"node": ">= 0.8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/c8": {
|
|
||||||
"version": "10.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz",
|
|
||||||
"integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"@bcoe/v8-coverage": "^1.0.1",
|
|
||||||
"@istanbuljs/schema": "^0.1.3",
|
|
||||||
"find-up": "^5.0.0",
|
|
||||||
"foreground-child": "^3.1.1",
|
|
||||||
"istanbul-lib-coverage": "^3.2.0",
|
|
||||||
"istanbul-lib-report": "^3.0.1",
|
|
||||||
"istanbul-reports": "^3.1.6",
|
|
||||||
"test-exclude": "^7.0.1",
|
|
||||||
"v8-to-istanbul": "^9.0.0",
|
|
||||||
"yargs": "^17.7.2",
|
|
||||||
"yargs-parser": "^21.1.1"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"c8": "bin/c8.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
},
|
|
||||||
"peerDependencies": {
|
|
||||||
"monocart-coverage-reports": "^2"
|
|
||||||
},
|
|
||||||
"peerDependenciesMeta": {
|
|
||||||
"monocart-coverage-reports": {
|
|
||||||
"optional": true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/cacheable-lookup": {
|
"node_modules/cacheable-lookup": {
|
||||||
"version": "7.0.0",
|
"version": "7.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
|
||||||
@@ -6867,6 +6824,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.1.tgz",
|
||||||
"integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==",
|
"integrity": "sha512-fm4D8ti0dQmFPeF8DXSAA//btEmqCOgAc/9Oa3C1LW94h5usNrJEfrON7b4FkPZgnDEn6OUs5NdxiJZmAtGOpQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"cssnano-preset-default": "^7.0.9",
|
"cssnano-preset-default": "^7.0.9",
|
||||||
"lilconfig": "^3.1.3"
|
"lilconfig": "^3.1.3"
|
||||||
@@ -7634,6 +7592,7 @@
|
|||||||
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
"integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -7995,6 +7954,7 @@
|
|||||||
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
"resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz",
|
||||||
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
"integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"accepts": "~1.3.8",
|
"accepts": "~1.3.8",
|
||||||
"array-flatten": "1.1.1",
|
"array-flatten": "1.1.1",
|
||||||
@@ -8127,9 +8087,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/fast-xml-builder": {
|
"node_modules/fast-xml-builder": {
|
||||||
"version": "1.0.0",
|
"version": "1.1.4",
|
||||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
|
||||||
"integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==",
|
"integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -8137,7 +8097,31 @@
|
|||||||
"url": "https://github.com/sponsors/NaturalIntelligence"
|
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT"
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"path-expression-matcher": "^1.1.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fast-xml-parser": {
|
||||||
|
"version": "5.5.8",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz",
|
||||||
|
"integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"fast-xml-builder": "^1.1.4",
|
||||||
|
"path-expression-matcher": "^1.2.0",
|
||||||
|
"strnum": "^2.2.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"fxparser": "src/cli/cli.js"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/fastq": {
|
"node_modules/fastq": {
|
||||||
"version": "1.20.1",
|
"version": "1.20.1",
|
||||||
@@ -8278,9 +8262,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/flatted": {
|
"node_modules/flatted": {
|
||||||
"version": "3.3.3",
|
"version": "3.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
@@ -8727,9 +8711,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/glob/node_modules/brace-expansion": {
|
"node_modules/glob/node_modules/brace-expansion": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
@@ -8829,9 +8813,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/handlebars": {
|
"node_modules/handlebars": {
|
||||||
"version": "4.7.8",
|
"version": "4.7.9",
|
||||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
|
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz",
|
||||||
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
|
"integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"minimist": "^1.2.5",
|
"minimist": "^1.2.5",
|
||||||
@@ -9639,6 +9623,7 @@
|
|||||||
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
|
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jest/core": "30.2.0",
|
"@jest/core": "30.2.0",
|
||||||
"@jest/types": "30.2.0",
|
"@jest/types": "30.2.0",
|
||||||
@@ -10130,9 +10115,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/jest-util/node_modules/picomatch": {
|
"node_modules/jest-util/node_modules/picomatch": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@@ -10667,9 +10652,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lodash": {
|
"node_modules/lodash": {
|
||||||
"version": "4.17.23",
|
"version": "4.18.1",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lodash.camelcase": {
|
"node_modules/lodash.camelcase": {
|
||||||
@@ -11211,9 +11196,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/mjml-cli/node_modules/brace-expansion": {
|
"node_modules/mjml-cli/node_modules/brace-expansion": {
|
||||||
"version": "2.0.2",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"balanced-match": "^1.0.0"
|
"balanced-match": "^1.0.0"
|
||||||
@@ -12307,6 +12292,22 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-expression-matcher": {
|
||||||
|
"version": "1.2.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.2.1.tgz",
|
||||||
|
"integrity": "sha512-d7gQQmLvAKXKXE2GeP9apIGbMYKz88zWdsn/BN2HRWVQsDFdUY36WSLTY0Jvd4HWi7Fb30gQ62oAOzdgJA6fZw==",
|
||||||
|
"dev": true,
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"type": "github",
|
||||||
|
"url": "https://github.com/sponsors/NaturalIntelligence"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"license": "MIT",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/path-is-absolute": {
|
"node_modules/path-is-absolute": {
|
||||||
"version": "1.0.1",
|
"version": "1.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||||
@@ -12343,9 +12344,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/path-to-regexp": {
|
"node_modules/path-to-regexp": {
|
||||||
"version": "0.1.12",
|
"version": "0.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
||||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/path-type": {
|
"node_modules/path-type": {
|
||||||
@@ -12358,6 +12359,96 @@
|
|||||||
"node": ">=8"
|
"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": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.1.1",
|
"version": "1.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||||
@@ -12365,9 +12456,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/picomatch": {
|
"node_modules/picomatch": {
|
||||||
"version": "2.3.1",
|
"version": "2.3.2",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=8.6"
|
"node": ">=8.6"
|
||||||
@@ -12509,6 +12600,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -12982,6 +13074,45 @@
|
|||||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/posthtml": {
|
||||||
"version": "0.16.7",
|
"version": "0.16.7",
|
||||||
"resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.7.tgz",
|
"resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.7.tgz",
|
||||||
@@ -14027,6 +14158,15 @@
|
|||||||
"integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==",
|
"integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==",
|
||||||
"license": "ISC"
|
"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": {
|
"node_modules/sprintf-js": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||||
@@ -14344,9 +14484,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/strnum": {
|
"node_modules/strnum": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.2",
|
||||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz",
|
||||||
"integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==",
|
"integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"funding": [
|
"funding": [
|
||||||
{
|
{
|
||||||
@@ -14373,25 +14513,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/super-simple-scheduler": {
|
"node_modules/super-simple-scheduler": {
|
||||||
"version": "1.4.5",
|
"version": "1.9.1",
|
||||||
"resolved": "https://registry.npmjs.org/super-simple-scheduler/-/super-simple-scheduler-1.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/super-simple-scheduler/-/super-simple-scheduler-1.9.1.tgz",
|
||||||
"integrity": "sha512-xHR3a+pLywY7HtwjWj/+OaANdEwAieQhckC/i0s6OoHtVJQ4+VLO1VfkKUbxp+oSROGXcaZet8PTGONVdKNUUw==",
|
"integrity": "sha512-JSiEQLk7MsnlSkJ87uoyFKIseG2N27ikB0FSxlh25nnQHWC7R5Lrd0VcoLpwSBanyLzlAslhK4W2rQ0mCU8jCQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"uuid": "11.1.0",
|
"uuid": "11.1.0"
|
||||||
"winston": "3.17.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/super-simple-scheduler/node_modules/is-stream": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=8"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/super-simple-scheduler/node_modules/uuid": {
|
"node_modules/super-simple-scheduler/node_modules/uuid": {
|
||||||
@@ -14407,28 +14534,6 @@
|
|||||||
"uuid": "dist/esm/bin/uuid"
|
"uuid": "dist/esm/bin/uuid"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/super-simple-scheduler/node_modules/winston": {
|
|
||||||
"version": "3.17.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/winston/-/winston-3.17.0.tgz",
|
|
||||||
"integrity": "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"@colors/colors": "^1.6.0",
|
|
||||||
"@dabh/diagnostics": "^2.0.2",
|
|
||||||
"async": "^3.2.3",
|
|
||||||
"is-stream": "^2.0.0",
|
|
||||||
"logform": "^2.7.0",
|
|
||||||
"one-time": "^1.0.0",
|
|
||||||
"readable-stream": "^3.4.0",
|
|
||||||
"safe-stable-stringify": "^2.3.1",
|
|
||||||
"stack-trace": "0.0.x",
|
|
||||||
"triple-beam": "^1.3.0",
|
|
||||||
"winston-transport": "^4.9.0"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 12.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/supports-color": {
|
"node_modules/supports-color": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
@@ -14516,47 +14621,6 @@
|
|||||||
"node": ">=6"
|
"node": ">=6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/test-exclude": {
|
|
||||||
"version": "7.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz",
|
|
||||||
"integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"@istanbuljs/schema": "^0.1.2",
|
|
||||||
"glob": "^10.4.1",
|
|
||||||
"minimatch": "^9.0.4"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/test-exclude/node_modules/brace-expansion": {
|
|
||||||
"version": "2.0.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
|
|
||||||
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"balanced-match": "^1.0.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/test-exclude/node_modules/minimatch": {
|
|
||||||
"version": "9.0.9",
|
|
||||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
|
|
||||||
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
|
|
||||||
"dev": true,
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"brace-expansion": "^2.0.2"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=16 || 14 >=14.17"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://github.com/sponsors/isaacs"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/text-hex": {
|
"node_modules/text-hex": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||||
@@ -14599,11 +14663,12 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.4",
|
||||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -14772,6 +14837,7 @@
|
|||||||
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
"integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"peer": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@cspotcode/source-map-support": "^0.8.0",
|
"@cspotcode/source-map-support": "^0.8.0",
|
||||||
"@tsconfig/node10": "^1.0.7",
|
"@tsconfig/node10": "^1.0.7",
|
||||||
@@ -14934,6 +15000,7 @@
|
|||||||
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
"integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==",
|
||||||
"devOptional": true,
|
"devOptional": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
|
"peer": true,
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -14987,9 +15054,9 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/undici": {
|
"node_modules/undici": {
|
||||||
"version": "6.23.0",
|
"version": "6.24.1",
|
||||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
|
||||||
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=18.17"
|
"node": ">=18.17"
|
||||||
@@ -15657,9 +15724,9 @@
|
|||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/yaml": {
|
"node_modules/yaml": {
|
||||||
"version": "2.8.2",
|
"version": "2.8.3",
|
||||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
|
||||||
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "ISC",
|
"license": "ISC",
|
||||||
"bin": {
|
"bin": {
|
||||||
|
|||||||
+8
-4
@@ -5,10 +5,13 @@
|
|||||||
"main": "index.js",
|
"main": "index.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "NODE_OPTIONS=--experimental-vm-modules c8 jest --runInBand",
|
"test": "NODE_OPTIONS=--experimental-vm-modules jest --coverage",
|
||||||
|
"test:unit": "NODE_OPTIONS=--experimental-vm-modules jest --coverage --collectCoverageFrom='src/**/*.ts' test/unit",
|
||||||
|
"test:unit:services": "NODE_OPTIONS=--experimental-vm-modules jest --coverage --collectCoverageFrom='src/service/**/*.ts' test/unit/services test/unit/providers/network test/unit/providers/notifications",
|
||||||
|
"test:integration": "NODE_OPTIONS=--experimental-vm-modules jest --no-coverage test/integration",
|
||||||
"dev": "nodemon --exec tsx src/index.js",
|
"dev": "nodemon --exec tsx src/index.js",
|
||||||
"start": "node --watch ./dist/index.js",
|
"start": "node --watch ./dist/index.js",
|
||||||
"build": "tsc && tsc-alias",
|
"build": "tsc && tsc-alias && cp -r src/templates dist/templates",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
"lint-fix": "eslint --fix .",
|
"lint-fix": "eslint --fix .",
|
||||||
"format": "prettier --write .",
|
"format": "prettier --write .",
|
||||||
@@ -52,10 +55,11 @@
|
|||||||
"mongoose": "^8.3.3",
|
"mongoose": "^8.3.3",
|
||||||
"multer": "^1.4.5-lts.1",
|
"multer": "^1.4.5-lts.1",
|
||||||
"nodemailer": "8.0.1",
|
"nodemailer": "8.0.1",
|
||||||
|
"pg": "8.20.0",
|
||||||
"ping": "0.4.4",
|
"ping": "0.4.4",
|
||||||
"sharp": "0.33.5",
|
"sharp": "0.33.5",
|
||||||
"ssl-checker": "2.0.10",
|
"ssl-checker": "2.0.10",
|
||||||
"super-simple-scheduler": "1.4.5",
|
"super-simple-scheduler": "1.9.1",
|
||||||
"swagger-ui-express": "5.0.1",
|
"swagger-ui-express": "5.0.1",
|
||||||
"winston": "^3.13.0",
|
"winston": "^3.13.0",
|
||||||
"ws": "^8.19.0",
|
"ws": "^8.19.0",
|
||||||
@@ -77,12 +81,12 @@
|
|||||||
"@types/multer": "^2.0.0",
|
"@types/multer": "^2.0.0",
|
||||||
"@types/nodemailer": "7.0.1",
|
"@types/nodemailer": "7.0.1",
|
||||||
"@types/papaparse": "^5.5.2",
|
"@types/papaparse": "^5.5.2",
|
||||||
|
"@types/pg": "8.20.0",
|
||||||
"@types/ping": "0.4.4",
|
"@types/ping": "0.4.4",
|
||||||
"@types/swagger-ui-express": "4.1.8",
|
"@types/swagger-ui-express": "4.1.8",
|
||||||
"@types/ws": "^8.18.1",
|
"@types/ws": "^8.18.1",
|
||||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||||
"@typescript-eslint/parser": "^8.56.1",
|
"@typescript-eslint/parser": "^8.56.1",
|
||||||
"c8": "10.1.3",
|
|
||||||
"eslint": "^9.17.0",
|
"eslint": "^9.17.0",
|
||||||
"eslint-plugin-mocha": "^10.5.0",
|
"eslint-plugin-mocha": "^10.5.0",
|
||||||
"esm": "3.2.25",
|
"esm": "3.2.25",
|
||||||
|
|||||||
+2
-2
@@ -4,7 +4,7 @@ import cors from "cors";
|
|||||||
import helmet from "helmet";
|
import helmet from "helmet";
|
||||||
import compression from "compression";
|
import compression from "compression";
|
||||||
import cookieParser from "cookie-parser";
|
import cookieParser from "cookie-parser";
|
||||||
import swaggerUi from "swagger-ui-express";
|
import swaggerUi, { type JsonObject } from "swagger-ui-express";
|
||||||
import { handleErrors } from "@/middleware/handleErrors.js";
|
import { handleErrors } from "@/middleware/handleErrors.js";
|
||||||
import { generalApiLimiter } from "@/middleware/rateLimiter.js";
|
import { generalApiLimiter } from "@/middleware/rateLimiter.js";
|
||||||
import { sanitizeBody, sanitizeQuery } from "@/middleware/sanitization.js";
|
import { sanitizeBody, sanitizeQuery } from "@/middleware/sanitization.js";
|
||||||
@@ -24,7 +24,7 @@ export const createApp = ({
|
|||||||
controllers: InitializedControllers;
|
controllers: InitializedControllers;
|
||||||
envSettings: EnvConfig;
|
envSettings: EnvConfig;
|
||||||
frontendPath: string;
|
frontendPath: string;
|
||||||
openApiSpec: any;
|
openApiSpec: JsonObject;
|
||||||
}) => {
|
}) => {
|
||||||
const allowedOrigin = envSettings.clientHost;
|
const allowedOrigin = envSettings.clientHost;
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export interface InitializedControllers {
|
|||||||
export const initializeControllers = (services: InitializedServices): InitializedControllers => {
|
export const initializeControllers = (services: InitializedServices): InitializedControllers => {
|
||||||
return {
|
return {
|
||||||
authController: new AuthController(services.userService),
|
authController: new AuthController(services.userService),
|
||||||
monitorController: new MonitorController(services.monitorService),
|
monitorController: new MonitorController(services.monitorService, services.notificationsService),
|
||||||
settingsController: new SettingsController(services.settingsService, services.emailService),
|
settingsController: new SettingsController(services.settingsService, services.emailService),
|
||||||
checkController: new CheckController(services.checkService),
|
checkController: new CheckController(services.checkService),
|
||||||
geoCheckController: new GeoCheckController(services.geoChecksService),
|
geoCheckController: new GeoCheckController(services.geoChecksService),
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
|
import type { Application } from "express";
|
||||||
import { createVerifyJWT } from "../middleware/verifyJWT.js";
|
import { createVerifyJWT } from "../middleware/verifyJWT.js";
|
||||||
import { createVerifyStatusPageAccess } from "../middleware/verifyStatusPageAccess.js";
|
import { createVerifyStatusPageAccess } from "../middleware/verifyStatusPageAccess.js";
|
||||||
import { authApiLimiter } from "../middleware/rateLimiter.js";
|
import { authApiLimiter } from "../middleware/rateLimiter.js";
|
||||||
|
import type { InitializedControllers } from "./controllers.js";
|
||||||
|
import type { InitializedServices } from "./services.js";
|
||||||
|
|
||||||
import AuthRoutes from "../routes/authRoute.js";
|
import AuthRoutes from "../routes/authRoute.js";
|
||||||
import InviteRoutes from "../routes/inviteRoute.js";
|
import InviteRoutes from "../routes/inviteRoute.js";
|
||||||
@@ -17,7 +20,7 @@ import NotificationRoutes from "../routes/notificationRoute.js";
|
|||||||
|
|
||||||
import IncidentRoutes from "../routes/incidentRoute.js";
|
import IncidentRoutes from "../routes/incidentRoute.js";
|
||||||
|
|
||||||
export const setupRoutes = (app: any, controllers: Record<string, any>, services: Record<string, any>) => {
|
export const setupRoutes = (app: Application, controllers: InitializedControllers, services: InitializedServices) => {
|
||||||
const verifyJWT = createVerifyJWT(services.settingsService);
|
const verifyJWT = createVerifyJWT(services.settingsService);
|
||||||
const authRoutes = new AuthRoutes(controllers.authController, verifyJWT);
|
const authRoutes = new AuthRoutes(controllers.authController, verifyJWT);
|
||||||
const monitorRoutes = new MonitorRoutes(controllers.monitorController);
|
const monitorRoutes = new MonitorRoutes(controllers.monitorController);
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
PagerDutyProvider,
|
PagerDutyProvider,
|
||||||
MatrixProvider,
|
MatrixProvider,
|
||||||
TeamsProvider,
|
TeamsProvider,
|
||||||
|
TelegramProvider,
|
||||||
// Interfaces
|
// Interfaces
|
||||||
INetworkService,
|
INetworkService,
|
||||||
IEmailService,
|
IEmailService,
|
||||||
@@ -46,6 +47,7 @@ import {
|
|||||||
IIncidentService,
|
IIncidentService,
|
||||||
INotificationMessageBuilder,
|
INotificationMessageBuilder,
|
||||||
ISettingsService,
|
ISettingsService,
|
||||||
|
SettingsService,
|
||||||
EnvConfig,
|
EnvConfig,
|
||||||
} from "@/service/index.js";
|
} from "@/service/index.js";
|
||||||
|
|
||||||
@@ -92,9 +94,23 @@ import {
|
|||||||
MongoInvitesRepository,
|
MongoInvitesRepository,
|
||||||
MongoRecoveryTokensRepository,
|
MongoRecoveryTokensRepository,
|
||||||
MongoNotificationsRepository,
|
MongoNotificationsRepository,
|
||||||
MongoIncidentRepository,
|
MongoIncidentsRepository,
|
||||||
MongoTeamsRepository,
|
MongoTeamsRepository,
|
||||||
MongoMaintenanceWindowsRepository,
|
MongoMaintenanceWindowsRepository,
|
||||||
|
MongoSettingsRepository,
|
||||||
|
TimescaleMonitorsRepository,
|
||||||
|
TimescaleChecksRepository,
|
||||||
|
TimescaleGeoChecksRepository,
|
||||||
|
TimescaleMonitorStatsRepository,
|
||||||
|
TimescaleStatusPagesRepository,
|
||||||
|
TimescaleUsersRepository,
|
||||||
|
TimescaleInvitesRepository,
|
||||||
|
TimescaleRecoveryTokensRepository,
|
||||||
|
TimescaleNotificationsRepository,
|
||||||
|
TimescaleIncidentsRepository,
|
||||||
|
TimescaleTeamsRepository,
|
||||||
|
TimescaleMaintenanceWindowsRepository,
|
||||||
|
TimescaleSettingsRepository,
|
||||||
IMonitorsRepository,
|
IMonitorsRepository,
|
||||||
IChecksRepository,
|
IChecksRepository,
|
||||||
IGeoChecksRepository,
|
IGeoChecksRepository,
|
||||||
@@ -110,6 +126,8 @@ import {
|
|||||||
IMaintenanceWindowsRepository,
|
IMaintenanceWindowsRepository,
|
||||||
} from "@/repositories/index.js";
|
} from "@/repositories/index.js";
|
||||||
import { ILogger } from "@/utils/logger.js";
|
import { ILogger } from "@/utils/logger.js";
|
||||||
|
import TimescaleDB from "@/db/TimescaleDB.js";
|
||||||
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
|
||||||
export type InitializedServices = {
|
export type InitializedServices = {
|
||||||
settingsService: ISettingsService;
|
settingsService: ISettingsService;
|
||||||
@@ -152,32 +170,81 @@ export const initializeServices = async ({
|
|||||||
logger,
|
logger,
|
||||||
envSettings,
|
envSettings,
|
||||||
settingsService,
|
settingsService,
|
||||||
settingsRepository,
|
|
||||||
}: {
|
}: {
|
||||||
logger: ILogger;
|
logger: ILogger;
|
||||||
envSettings: EnvConfig;
|
envSettings: EnvConfig;
|
||||||
settingsService: ISettingsService;
|
settingsService: ISettingsService;
|
||||||
settingsRepository: ISettingsRepository;
|
|
||||||
}): Promise<InitializedServices> => {
|
}): Promise<InitializedServices> => {
|
||||||
// Create DB
|
// Create DB
|
||||||
|
|
||||||
const db = new MongoDB(logger, envSettings);
|
const dbType = envSettings.dbType;
|
||||||
|
|
||||||
|
let db: IDb | null = null;
|
||||||
|
|
||||||
|
if (dbType === "mongodb") {
|
||||||
|
db = new MongoDB(logger, envSettings);
|
||||||
|
} else if (dbType === "timescaledb") {
|
||||||
|
db = new TimescaleDB(logger, envSettings);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!db) {
|
||||||
|
throw new AppError({ message: "Unsupported database type", status: 500 });
|
||||||
|
}
|
||||||
|
|
||||||
await db.connect();
|
await db.connect();
|
||||||
|
|
||||||
|
let monitorsRepository: IMonitorsRepository;
|
||||||
|
let checksRepository: IChecksRepository;
|
||||||
|
let geoChecksRepository: IGeoChecksRepository;
|
||||||
|
let monitorStatsRepository: IMonitorStatsRepository;
|
||||||
|
let statusPagesRepository: IStatusPagesRepository;
|
||||||
|
let usersRepository: IUsersRepository;
|
||||||
|
let invitesRepository: IInvitesRepository;
|
||||||
|
let recoveryTokensRepository: IRecoveryTokensRepository;
|
||||||
|
let settingsRepository: ISettingsRepository;
|
||||||
|
let notificationsRepository: INotificationsRepository;
|
||||||
|
let incidentsRepository: IIncidentsRepository;
|
||||||
|
let teamsRepository: ITeamsRepository;
|
||||||
|
let maintenanceWindowsRepository: IMaintenanceWindowsRepository;
|
||||||
|
|
||||||
// Repositories
|
// Repositories
|
||||||
const monitorsRepository = new MongoMonitorsRepository();
|
|
||||||
const checksRepository = new MongoChecksRepository(logger);
|
if (dbType === "mongodb") {
|
||||||
const geoChecksRepository = new MongoGeoChecksRepository(logger);
|
monitorsRepository = new MongoMonitorsRepository();
|
||||||
const monitorStatsRepository = new MongoMonitorStatsRepository();
|
checksRepository = new MongoChecksRepository(logger);
|
||||||
const statusPagesRepository = new MongoStatusPagesRepository();
|
geoChecksRepository = new MongoGeoChecksRepository(logger);
|
||||||
const usersRepository = new MongoUsersRepository();
|
monitorStatsRepository = new MongoMonitorStatsRepository();
|
||||||
const invitesRepository = new MongoInvitesRepository();
|
statusPagesRepository = new MongoStatusPagesRepository();
|
||||||
const recoveryTokensRepository = new MongoRecoveryTokensRepository();
|
usersRepository = new MongoUsersRepository();
|
||||||
const notificationsRepository = new MongoNotificationsRepository();
|
invitesRepository = new MongoInvitesRepository();
|
||||||
const incidentsRepository = new MongoIncidentRepository();
|
recoveryTokensRepository = new MongoRecoveryTokensRepository();
|
||||||
const teamsRepository = new MongoTeamsRepository();
|
settingsRepository = new MongoSettingsRepository();
|
||||||
const maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
|
notificationsRepository = new MongoNotificationsRepository();
|
||||||
|
incidentsRepository = new MongoIncidentsRepository();
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inject settings repository into settings service (now that DB is connected)
|
||||||
|
(settingsService as SettingsService).setRepository(settingsRepository);
|
||||||
|
|
||||||
// Network providers
|
// Network providers
|
||||||
const pingProvider = new PingProvider(ping);
|
const pingProvider = new PingProvider(ping);
|
||||||
@@ -230,6 +297,7 @@ export const initializeServices = async ({
|
|||||||
const pagerDutyProvider = new PagerDutyProvider(logger);
|
const pagerDutyProvider = new PagerDutyProvider(logger);
|
||||||
const matrixProvider = new MatrixProvider(logger);
|
const matrixProvider = new MatrixProvider(logger);
|
||||||
const teamsProvider = new TeamsProvider(logger);
|
const teamsProvider = new TeamsProvider(logger);
|
||||||
|
const telegramProvider = new TelegramProvider(logger);
|
||||||
|
|
||||||
const notificationsService = new NotificationsService(
|
const notificationsService = new NotificationsService(
|
||||||
notificationsRepository,
|
notificationsRepository,
|
||||||
@@ -241,6 +309,7 @@ export const initializeServices = async ({
|
|||||||
pagerDutyProvider,
|
pagerDutyProvider,
|
||||||
matrixProvider,
|
matrixProvider,
|
||||||
teamsProvider,
|
teamsProvider,
|
||||||
|
telegramProvider,
|
||||||
settingsService,
|
settingsService,
|
||||||
logger,
|
logger,
|
||||||
notificationMessageBuilder
|
notificationMessageBuilder
|
||||||
@@ -295,7 +364,6 @@ export const initializeServices = async ({
|
|||||||
});
|
});
|
||||||
const monitorService = new MonitorService({
|
const monitorService = new MonitorService({
|
||||||
jobQueue: superSimpleQueue,
|
jobQueue: superSimpleQueue,
|
||||||
emailService,
|
|
||||||
logger,
|
logger,
|
||||||
games,
|
games,
|
||||||
monitorsRepository,
|
monitorsRepository,
|
||||||
|
|||||||
@@ -23,7 +23,24 @@ import { IUserService } from "@/service/index.js";
|
|||||||
|
|
||||||
const SERVICE_NAME = "authController";
|
const SERVICE_NAME = "authController";
|
||||||
|
|
||||||
class AuthController {
|
export interface IAuthController {
|
||||||
|
registerUser(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
createUser(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
loginUser(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
editUser(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
checkSuperadminExists(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
requestRecovery(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
validateRecovery(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
resetPassword(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
deleteUser(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
deleteUserById(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
getAllUsers(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
getUserById(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
editUserById(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
editUserPasswordById(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class AuthController implements IAuthController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private userService: IUserService;
|
private userService: IUserService;
|
||||||
|
|||||||
@@ -12,7 +12,16 @@ import { requireTeamId } from "@/controllers/controllerUtils.js";
|
|||||||
|
|
||||||
const SERVICE_NAME = "checkController";
|
const SERVICE_NAME = "checkController";
|
||||||
|
|
||||||
class CheckController {
|
export interface ICheckController {
|
||||||
|
serviceName: string;
|
||||||
|
getChecksByMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getChecksByTeam: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getChecksSummaryByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
deleteChecks: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
deleteChecksByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class CheckController implements ICheckController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private checkService: ICheckService;
|
private checkService: ICheckService;
|
||||||
|
|||||||
@@ -2,7 +2,12 @@ import { IDiagnosticService } from "@/service/index.js";
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
|
||||||
const SERVICE_NAME = "diagnosticController";
|
const SERVICE_NAME = "diagnosticController";
|
||||||
class DiagnosticController {
|
|
||||||
|
export interface IDiagnosticController {
|
||||||
|
getSystemStats: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class DiagnosticController implements IDiagnosticController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private diagnosticService: IDiagnosticService;
|
private diagnosticService: IDiagnosticService;
|
||||||
|
|||||||
@@ -1,10 +1,15 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { getChecksParamValidation, getChecksQueryValidation } from "@/validation/checkValidation.js";
|
import { getChecksParamValidation, getChecksQueryValidation } from "@/validation/checkValidation.js";
|
||||||
import type { IGeoChecksService } from "@/service/business/geoChecksService.js";
|
import type { IGeoChecksService } from "@/service/business/geoChecksService.js";
|
||||||
|
import { requireTeamId } from "./controllerUtils.js";
|
||||||
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "geoCheckController";
|
const SERVICE_NAME = "geoCheckController";
|
||||||
|
|
||||||
class GeoCheckController {
|
export interface IGeoCheckController {
|
||||||
|
getGeoChecksByMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
}
|
||||||
|
class GeoCheckController implements IGeoCheckController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private geoChecksService: IGeoChecksService;
|
private geoChecksService: IGeoChecksService;
|
||||||
@@ -18,13 +23,19 @@ class GeoCheckController {
|
|||||||
|
|
||||||
getGeoChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
getGeoChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
getChecksParamValidation.parse(req.params);
|
const validatedParams = getChecksParamValidation.parse(req.params);
|
||||||
getChecksQueryValidation.parse(req.query);
|
const validatedQuery = getChecksQueryValidation.parse(req.query);
|
||||||
|
|
||||||
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
|
||||||
const result = await this.geoChecksService.getGeoChecksByMonitor({
|
const result = await this.geoChecksService.getGeoChecksByMonitor({
|
||||||
monitorId: req?.params?.monitorId as string,
|
monitorId: validatedParams.monitorId,
|
||||||
query: req?.query,
|
teamId: teamId,
|
||||||
teamId: req?.user?.teamId as string,
|
sortOrder: validatedQuery.sortOrder,
|
||||||
|
dateRange: validatedQuery.dateRange,
|
||||||
|
page: validatedQuery.page,
|
||||||
|
rowsPerPage: validatedQuery.rowsPerPage,
|
||||||
|
continent: validatedQuery.continent,
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
|
|||||||
@@ -5,7 +5,14 @@ import { IIncidentService } from "@/service/index.js";
|
|||||||
import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } from "@/validation/incidentValidation.js";
|
import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } from "@/validation/incidentValidation.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "IncidentController";
|
const SERVICE_NAME = "IncidentController";
|
||||||
class IncidentController {
|
|
||||||
|
export interface IIncidentController {
|
||||||
|
getIncidentsByTeam: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getIncidentSummary: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getIncidentById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
resolveIncidentManually: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
}
|
||||||
|
class IncidentController implements IIncidentController {
|
||||||
private incidentService: IIncidentService;
|
private incidentService: IIncidentService;
|
||||||
constructor(incidentService: IIncidentService) {
|
constructor(incidentService: IIncidentService) {
|
||||||
this.incidentService = incidentService;
|
this.incidentService = incidentService;
|
||||||
|
|||||||
@@ -4,7 +4,13 @@ import { requireFirstName, requireTeamId, requireUserRoles } from "@/controllers
|
|||||||
import { IInviteService } from "@/service/index.js";
|
import { IInviteService } from "@/service/index.js";
|
||||||
const SERVICE_NAME = "inviteController";
|
const SERVICE_NAME = "inviteController";
|
||||||
|
|
||||||
class InviteController {
|
export interface IInviteController {
|
||||||
|
getInviteToken: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
sendInviteEmail: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
verifyInviteToken: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class InviteController implements IInviteController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private inviteService: IInviteService;
|
private inviteService: IInviteService;
|
||||||
constructor(inviteService: IInviteService) {
|
constructor(inviteService: IInviteService) {
|
||||||
|
|||||||
@@ -3,6 +3,10 @@ import { Request, Response, NextFunction } from "express";
|
|||||||
|
|
||||||
const SERVICE_NAME = "LogController";
|
const SERVICE_NAME = "LogController";
|
||||||
|
|
||||||
|
export interface ILogController {
|
||||||
|
getLogs: (req: Request, res: Response, next: NextFunction) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
class LogController {
|
class LogController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private logger: ILogger;
|
private logger: ILogger;
|
||||||
|
|||||||
@@ -11,7 +11,15 @@ import {
|
|||||||
import { requireTeamId } from "@/controllers/controllerUtils.js";
|
import { requireTeamId } from "@/controllers/controllerUtils.js";
|
||||||
import { IMaintenanceWindowService } from "@/service/index.js";
|
import { IMaintenanceWindowService } from "@/service/index.js";
|
||||||
|
|
||||||
class MaintenanceWindowController {
|
export interface IMaintenanceWindowController {
|
||||||
|
createMaintenanceWindows: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getMaintenanceWindowById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getMaintenanceWindowsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getMaintenanceWindowsByMonitorId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
deleteMaintenanceWindow: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
editMaintenanceWindow: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
}
|
||||||
|
class MaintenanceWindowController implements IMaintenanceWindowController {
|
||||||
private maintenanceWindowService: IMaintenanceWindowService;
|
private maintenanceWindowService: IMaintenanceWindowService;
|
||||||
constructor(maintenanceWindowService: IMaintenanceWindowService) {
|
constructor(maintenanceWindowService: IMaintenanceWindowService) {
|
||||||
this.maintenanceWindowService = maintenanceWindowService;
|
this.maintenanceWindowService = maintenanceWindowService;
|
||||||
@@ -21,7 +29,17 @@ class MaintenanceWindowController {
|
|||||||
try {
|
try {
|
||||||
const validatedBody = createMaintenanceWindowBodyValidation.parse(req.body);
|
const validatedBody = createMaintenanceWindowBodyValidation.parse(req.body);
|
||||||
const teamId = requireTeamId(req?.user?.teamId);
|
const teamId = requireTeamId(req?.user?.teamId);
|
||||||
await this.maintenanceWindowService.createMaintenanceWindow({ teamId, body: validatedBody });
|
|
||||||
|
const monitorIDs = validatedBody.monitors;
|
||||||
|
const name = validatedBody.name;
|
||||||
|
const active = validatedBody.active ?? true;
|
||||||
|
const duration = validatedBody.duration;
|
||||||
|
const durationUnit = validatedBody.durationUnit;
|
||||||
|
const repeat = validatedBody.repeat;
|
||||||
|
const start = validatedBody.start;
|
||||||
|
const end = validatedBody.end;
|
||||||
|
|
||||||
|
await this.maintenanceWindowService.createMaintenanceWindow({ teamId, monitorIDs, name, active, duration, durationUnit, repeat, start, end });
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
import { updateNotificationsValidation } from "@/validation/notificationValidation.js";
|
||||||
import {
|
import {
|
||||||
getMonitorByIdParamValidation,
|
getMonitorByIdParamValidation,
|
||||||
getMonitorByIdQueryValidation,
|
getMonitorByIdQueryValidation,
|
||||||
@@ -16,18 +16,43 @@ import {
|
|||||||
importMonitorsBodyValidation,
|
importMonitorsBodyValidation,
|
||||||
} from "@/validation/monitorValidation.js";
|
} from "@/validation/monitorValidation.js";
|
||||||
import sslChecker from "ssl-checker";
|
import sslChecker from "ssl-checker";
|
||||||
import { fetchMonitorCertificate, requireTeamId, requireUserId } from "./controllerUtils.js";
|
import { fetchMonitorCertificate, requireTeamId, requireUserId } from "@/controllers/controllerUtils.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { IMonitorService } from "@/service/index.js";
|
import { IMonitorService, INotificationsService } from "@/service/index.js";
|
||||||
|
import { GeoContinent } from "@/types/geoCheck.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "monitorController";
|
const SERVICE_NAME = "monitorController";
|
||||||
class MonitorController {
|
|
||||||
|
export interface IMonitorController {
|
||||||
|
getMonitorCertificate: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getUptimeDetailsById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getHardwareDetailsById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getPageSpeedDetailsById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getGeoChecksByMonitorId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getMonitorById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
createMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
importMonitorsFromJSON: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
deleteMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
deleteAllMonitors: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
editMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
pauseMonitor: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
addDemoMonitors: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getMonitorsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getMonitorsWithChecksByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
exportMonitorsToJSON: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getAllGames: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getGroupsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
updateNotifications: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
}
|
||||||
|
class MonitorController implements IMonitorController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
|
|
||||||
private monitorService: IMonitorService;
|
private monitorService: IMonitorService;
|
||||||
|
private notificationsService: INotificationsService;
|
||||||
|
|
||||||
constructor(monitorService: IMonitorService) {
|
constructor(monitorService: IMonitorService, notificationsService: INotificationsService) {
|
||||||
this.monitorService = monitorService;
|
this.monitorService = monitorService;
|
||||||
|
this.notificationsService = notificationsService;
|
||||||
}
|
}
|
||||||
|
|
||||||
get serviceName() {
|
get serviceName() {
|
||||||
@@ -60,7 +85,6 @@ class MonitorController {
|
|||||||
|
|
||||||
const monitorId = validatedParams.monitorId;
|
const monitorId = validatedParams.monitorId;
|
||||||
const dateRange = validatedQuery.dateRange;
|
const dateRange = validatedQuery.dateRange;
|
||||||
const normalize = validatedQuery.normalize;
|
|
||||||
|
|
||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
|
||||||
@@ -68,7 +92,6 @@ class MonitorController {
|
|||||||
teamId,
|
teamId,
|
||||||
monitorId,
|
monitorId,
|
||||||
dateRange,
|
dateRange,
|
||||||
normalize,
|
|
||||||
});
|
});
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -324,7 +347,6 @@ class MonitorController {
|
|||||||
try {
|
try {
|
||||||
const validatedQuery = getMonitorsWithChecksQueryValidation.parse(req.query);
|
const validatedQuery = getMonitorsWithChecksQueryValidation.parse(req.query);
|
||||||
|
|
||||||
const explain = validatedQuery.explain;
|
|
||||||
const limit = validatedQuery.limit;
|
const limit = validatedQuery.limit;
|
||||||
const page = validatedQuery.page;
|
const page = validatedQuery.page;
|
||||||
const rowsPerPage = validatedQuery.rowsPerPage;
|
const rowsPerPage = validatedQuery.rowsPerPage;
|
||||||
@@ -343,7 +365,6 @@ class MonitorController {
|
|||||||
filter,
|
filter,
|
||||||
field,
|
field,
|
||||||
order,
|
order,
|
||||||
explain,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
@@ -396,6 +417,42 @@ class MonitorController {
|
|||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
updateNotifications = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
|
try {
|
||||||
|
updateNotificationsValidation.parse(req.body);
|
||||||
|
|
||||||
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
const { monitorIds, notificationIds, action } = req.body;
|
||||||
|
|
||||||
|
// Verify all requested notification IDs actually belong to this team
|
||||||
|
const teamNotifications = await this.notificationsService.findNotificationsByTeamId(teamId);
|
||||||
|
const validNotificationIds = teamNotifications.map((n) => n.id);
|
||||||
|
|
||||||
|
const invalidIds = notificationIds.filter((id: string) => !validNotificationIds.includes(id));
|
||||||
|
if (invalidIds.length > 0) {
|
||||||
|
throw new AppError({
|
||||||
|
message: `The following notification IDs are invalid or do not belong to your team: ${invalidIds.join(", ")}`,
|
||||||
|
status: 403,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const modifiedCount = await this.monitorService.updateNotifications({
|
||||||
|
teamId,
|
||||||
|
monitorIds,
|
||||||
|
notificationIds,
|
||||||
|
action,
|
||||||
|
});
|
||||||
|
|
||||||
|
return res.status(200).json({
|
||||||
|
success: true,
|
||||||
|
msg: `Notifications updated successfully on ${modifiedCount} monitor(s)`,
|
||||||
|
data: { modifiedCount },
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
next(error);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MonitorController;
|
export default MonitorController;
|
||||||
@@ -9,13 +9,22 @@ import {
|
|||||||
testAllNotificationsBodyValidation,
|
testAllNotificationsBodyValidation,
|
||||||
} from "@/validation/notificationValidation.js";
|
} from "@/validation/notificationValidation.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
import { IMonitorsRepository } from "@/repositories/index.js";
|
|
||||||
import { INotificationsService } from "@/service/index.js";
|
import { INotificationsService } from "@/service/index.js";
|
||||||
import { requireTeamId, requireUserId } from "./controllerUtils.js";
|
import { requireTeamId, requireUserId } from "./controllerUtils.js";
|
||||||
|
import { IMonitorsRepository } from "@/repositories/index.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "NotificationController";
|
const SERVICE_NAME = "NotificationController";
|
||||||
|
|
||||||
class NotificationController {
|
export interface INotificationController {
|
||||||
|
testNotification: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
createNotification: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getNotificationsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
deleteNotification: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
getNotificationById: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
editNotification: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
testAllNotifications: (req: Request, res: Response, next: NextFunction) => Promise<Response | void>;
|
||||||
|
}
|
||||||
|
class NotificationController implements INotificationController {
|
||||||
private notificationsService: INotificationsService;
|
private notificationsService: INotificationsService;
|
||||||
private monitorsRepository: IMonitorsRepository;
|
private monitorsRepository: IMonitorsRepository;
|
||||||
constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) {
|
constructor(notificationsService: INotificationsService, monitorsRepository: IMonitorsRepository) {
|
||||||
@@ -133,8 +142,8 @@ class NotificationController {
|
|||||||
const teamId = requireTeamId(req.user?.teamId);
|
const teamId = requireTeamId(req.user?.teamId);
|
||||||
|
|
||||||
const monitor = await this.monitorsRepository.findById(validatedBody.monitorId, teamId);
|
const monitor = await this.monitorsRepository.findById(validatedBody.monitorId, teamId);
|
||||||
|
const notifications = monitor.notifications || [];
|
||||||
|
|
||||||
const notifications = monitor.notifications;
|
|
||||||
if (notifications.length === 0) {
|
if (notifications.length === 0) {
|
||||||
throw new AppError({ message: "No notifications", status: 400 });
|
throw new AppError({ message: "No notifications", status: 400 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
|
import { ISuperSimpleQueue } from "@/service/index.js";
|
||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
|
|
||||||
const SERVICE_NAME = "JobQueueController";
|
const SERVICE_NAME = "JobQueueController";
|
||||||
|
|
||||||
class JobQueueController {
|
export interface IJobQueueController {
|
||||||
|
getMetrics(req: Request, res: Response, next: NextFunction): Promise<void>;
|
||||||
|
getJobs(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
getAllMetrics(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
flushQueue(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class JobQueueController implements IJobQueueController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private jobQueue: any;
|
private jobQueue: ISuperSimpleQueue;
|
||||||
constructor(jobQueue: any) {
|
constructor(jobQueue: ISuperSimpleQueue) {
|
||||||
this.jobQueue = jobQueue;
|
this.jobQueue = jobQueue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,18 +61,6 @@ class JobQueueController {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
addJob = async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
await this.jobQueue.addJob(Math.random().toString(36).substring(7));
|
|
||||||
return res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
msg: "Job added to queue successfully",
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
flushQueue = async (req: Request, res: Response, next: NextFunction) => {
|
flushQueue = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
const result = await this.jobQueue.flushQueues();
|
const result = await this.jobQueue.flushQueues();
|
||||||
@@ -77,18 +73,5 @@ class JobQueueController {
|
|||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
checkQueueHealth = async (req: Request, res: Response, next: NextFunction) => {
|
|
||||||
try {
|
|
||||||
const stuckQueues = await this.jobQueue.checkQueueHealth();
|
|
||||||
return res.status(200).json({
|
|
||||||
success: true,
|
|
||||||
msg: "Queue health checked successfully",
|
|
||||||
data: stuckQueues,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
next(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
export default JobQueueController;
|
export default JobQueueController;
|
||||||
@@ -2,14 +2,23 @@ import { Request, Response, NextFunction } from "express";
|
|||||||
import { updateAppSettingsBodyValidation } from "@/validation/settingsValidation.js";
|
import { updateAppSettingsBodyValidation } from "@/validation/settingsValidation.js";
|
||||||
import { sendTestEmailBodyValidation } from "@/validation/notificationValidation.js";
|
import { sendTestEmailBodyValidation } from "@/validation/notificationValidation.js";
|
||||||
import { AppError } from "@/utils/AppError.js";
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
import { IEmailService, ISettingsService } from "@/service/index.js";
|
||||||
|
import { Settings } from "@/types/settings.js";
|
||||||
|
|
||||||
const SERVICE_NAME = "SettingsController";
|
const SERVICE_NAME = "SettingsController";
|
||||||
|
|
||||||
class SettingsController {
|
export interface ISettingsController {
|
||||||
|
serviceName: string;
|
||||||
|
getAppSettings(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
updateAppSettings(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
sendTestEmail(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class SettingsController implements ISettingsController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private settingsService: any;
|
private settingsService: ISettingsService;
|
||||||
private emailService: any;
|
private emailService: IEmailService;
|
||||||
constructor(settingsService: any, emailService: any) {
|
constructor(settingsService: ISettingsService, emailService: IEmailService) {
|
||||||
this.settingsService = settingsService;
|
this.settingsService = settingsService;
|
||||||
this.emailService = emailService;
|
this.emailService = emailService;
|
||||||
}
|
}
|
||||||
@@ -18,11 +27,11 @@ class SettingsController {
|
|||||||
return SettingsController.SERVICE_NAME;
|
return SettingsController.SERVICE_NAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
buildAppSettings = (dbSettings: any) => {
|
buildAppSettings = (dbSettings: Settings) => {
|
||||||
const sanitizedSettings = { ...dbSettings };
|
const sanitizedSettings: Record<string, unknown> = { ...dbSettings };
|
||||||
delete sanitizedSettings.version;
|
delete sanitizedSettings.version;
|
||||||
delete sanitizedSettings.jwtSecret;
|
delete sanitizedSettings.jwtSecret;
|
||||||
const returnSettings = {
|
const returnSettings: Record<string, unknown | null> = {
|
||||||
pagespeedKeySet: false,
|
pagespeedKeySet: false,
|
||||||
emailPasswordSet: false,
|
emailPasswordSet: false,
|
||||||
settings: null,
|
settings: null,
|
||||||
@@ -57,9 +66,9 @@ class SettingsController {
|
|||||||
|
|
||||||
updateAppSettings = async (req: Request, res: Response, next: NextFunction) => {
|
updateAppSettings = async (req: Request, res: Response, next: NextFunction) => {
|
||||||
try {
|
try {
|
||||||
updateAppSettingsBodyValidation.parse(req.body);
|
const validatedBody = updateAppSettingsBodyValidation.parse(req.body);
|
||||||
|
|
||||||
const updatedSettings = await this.settingsService.updateDbSettings(req.body);
|
const updatedSettings = await this.settingsService.updateDbSettings(validatedBody);
|
||||||
const returnSettings = this.buildAppSettings(updatedSettings);
|
const returnSettings = this.buildAppSettings(updatedSettings);
|
||||||
return res.status(200).json({
|
return res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -95,6 +104,9 @@ class SettingsController {
|
|||||||
const context = { testName: "Monitoring System" };
|
const context = { testName: "Monitoring System" };
|
||||||
|
|
||||||
const html = await this.emailService.buildEmail("testEmailTemplate", context);
|
const html = await this.emailService.buildEmail("testEmailTemplate", context);
|
||||||
|
if (!html) {
|
||||||
|
throw new AppError({ message: "Failed to build email template.", status: 500 });
|
||||||
|
}
|
||||||
const messageId = await this.emailService.sendEmail(to, subject, html, {
|
const messageId = await this.emailService.sendEmail(to, subject, html, {
|
||||||
systemEmailHost,
|
systemEmailHost,
|
||||||
systemEmailPort,
|
systemEmailPort,
|
||||||
|
|||||||
@@ -15,7 +15,16 @@ import { NormalizeData } from "@/utils/dataUtils.js";
|
|||||||
|
|
||||||
const SERVICE_NAME = "statusPageController";
|
const SERVICE_NAME = "statusPageController";
|
||||||
|
|
||||||
class StatusPageController {
|
export interface IStatusPageController {
|
||||||
|
readonly serviceName: string;
|
||||||
|
createStatusPage(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
updateStatusPage(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
getStatusPageByUrl(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
getStatusPagesByTeamId(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
deleteStatusPage(req: Request, res: Response, next: NextFunction): Promise<Response | void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
class StatusPageController implements IStatusPageController {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
private statusPageService: IStatusPageService;
|
private statusPageService: IStatusPageService;
|
||||||
private monitorsRepository: IMonitorsRepository;
|
private monitorsRepository: IMonitorsRepository;
|
||||||
@@ -66,7 +75,7 @@ class StatusPageController {
|
|||||||
if (statusPage === null) {
|
if (statusPage === null) {
|
||||||
throw new AppError({ message: "Status page not found", status: 404 });
|
throw new AppError({ message: "Status page not found", status: 404 });
|
||||||
}
|
}
|
||||||
return res.status(200).json({
|
res.status(200).json({
|
||||||
success: true,
|
success: true,
|
||||||
msg: "Status page updated successfully",
|
msg: "Status page updated successfully",
|
||||||
data: statusPage,
|
data: statusPage,
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
|
import { Pool } from "pg";
|
||||||
|
|
||||||
export interface IDb {
|
export interface IDb {
|
||||||
connect(): Promise<void>;
|
connect(): Promise<void>;
|
||||||
disconnect(): Promise<void>;
|
disconnect(): Promise<void>;
|
||||||
|
getPool(): Pool | null;
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import { ILogger } from "@/utils/logger.js";
|
|||||||
import { EnvConfig } from "@/service/system/settingsService.js";
|
import { EnvConfig } from "@/service/system/settingsService.js";
|
||||||
const SERVICE_NAME = "MongoDB";
|
const SERVICE_NAME = "MongoDB";
|
||||||
import { IDb } from "@/db/IDb.js";
|
import { IDb } from "@/db/IDb.js";
|
||||||
|
import { Pool } from "pg";
|
||||||
|
|
||||||
class MongoDB implements IDb {
|
class MongoDB implements IDb {
|
||||||
static SERVICE_NAME = SERVICE_NAME;
|
static SERVICE_NAME = SERVICE_NAME;
|
||||||
@@ -74,6 +75,10 @@ class MongoDB implements IDb {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
getPool = () => {
|
||||||
|
return null;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MongoDB;
|
export default MongoDB;
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
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;
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import mongoose from "mongoose";
|
import mongoose from "mongoose";
|
||||||
|
import type { Document, AnyBulkWriteOperation } from "mongodb";
|
||||||
|
|
||||||
const CHECKS_COLLECTION = "checks";
|
const CHECKS_COLLECTION = "checks";
|
||||||
const BACKUP_COLLECTION = "checks_backup";
|
const BACKUP_COLLECTION = "checks_backup";
|
||||||
@@ -85,7 +86,7 @@ const createTimeSeriesCollection = async (backedUp: boolean) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const validateDocument = (doc: any): boolean => {
|
const validateDocument = (doc: Document): boolean => {
|
||||||
if (!doc.createdAt) {
|
if (!doc.createdAt) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -95,7 +96,7 @@ const validateDocument = (doc: any): boolean => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const storeFailedDocument = async (doc: any, reason: string) => {
|
const storeFailedDocument = async (doc: Document, reason: string) => {
|
||||||
const db = getDb();
|
const db = getDb();
|
||||||
await db.collection(FAILED_DOCS_COLLECTION).insertOne({
|
await db.collection(FAILED_DOCS_COLLECTION).insertOne({
|
||||||
originalDoc: doc,
|
originalDoc: doc,
|
||||||
@@ -124,8 +125,8 @@ const migrateBackupData = async (backedUp: boolean): Promise<MigrationStats> =>
|
|||||||
stats.totalSource = await source.countDocuments();
|
stats.totalSource = await source.countDocuments();
|
||||||
|
|
||||||
const cursor = source.find().addCursorFlag("noCursorTimeout", true);
|
const cursor = source.find().addCursorFlag("noCursorTimeout", true);
|
||||||
const operations: any[] = [];
|
const operations: AnyBulkWriteOperation<Document>[] = [];
|
||||||
const invalidDocs: any[] = [];
|
const invalidDocs: Document[] = [];
|
||||||
|
|
||||||
while (await cursor.hasNext()) {
|
while (await cursor.hasNext()) {
|
||||||
const doc = await cursor.next();
|
const doc = await cursor.next();
|
||||||
@@ -139,6 +140,7 @@ const migrateBackupData = async (backedUp: boolean): Promise<MigrationStats> =>
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { _id, monitorId, teamId, type, ...rest } = doc;
|
const { _id, monitorId, teamId, type, ...rest } = doc;
|
||||||
|
void _id;
|
||||||
const metadata = { monitorId, teamId, type };
|
const metadata = { monitorId, teamId, type };
|
||||||
operations.push({ insertOne: { document: { ...rest, metadata } } });
|
operations.push({ insertOne: { document: { ...rest, metadata } } });
|
||||||
|
|
||||||
|
|||||||
@@ -42,10 +42,10 @@ export async function cleanupDuplicateMonitorStats(): Promise<void> {
|
|||||||
const monitorId = duplicate._id;
|
const monitorId = duplicate._id;
|
||||||
const { ids, updatedAts } = duplicate;
|
const { ids, updatedAts } = duplicate;
|
||||||
|
|
||||||
type DocPair = { id: any; updatedAt: Date };
|
type DocPair = { id: string; updatedAt: Date };
|
||||||
|
|
||||||
// Create array of {id, updatedAt} pairs and sort by updatedAt descending
|
// Create array of {id, updatedAt} pairs and sort by updatedAt descending
|
||||||
const docs: DocPair[] = ids.map((id: any, index: number) => ({
|
const docs: DocPair[] = ids.map((id: string, index: number) => ({
|
||||||
id,
|
id,
|
||||||
updatedAt: updatedAts[index],
|
updatedAt: updatedAts[index],
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -37,7 +37,8 @@ export async function fixInfrastructureThresholds(): Promise<void> {
|
|||||||
const update: Record<string, number> = {};
|
const update: Record<string, number> = {};
|
||||||
|
|
||||||
for (const field of thresholdFields) {
|
for (const field of thresholdFields) {
|
||||||
if ((monitor as any)[field] === 0 || (monitor as any)[field] === 5) {
|
const value = monitor.get(field);
|
||||||
|
if (value === 0 || value === 5) {
|
||||||
update[field] = 100;
|
update[field] = 100;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
import mongoose from "mongoose";
|
||||||
|
import { logger } from "@/utils/logger.js";
|
||||||
|
import StatusPageModel from "../models/StatusPage.js";
|
||||||
|
|
||||||
|
export async function migrateStatusPageTypeToArray(): Promise<void> {
|
||||||
|
const SERVICE_NAME = "Migration:MigrateStatusPageTypeToArray";
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info({ service: SERVICE_NAME, message: "Starting migration of StatusPage type field from string to array" });
|
||||||
|
|
||||||
|
const db = mongoose.connection.db;
|
||||||
|
if (!db) {
|
||||||
|
throw new Error("Database connection is not initialized");
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await StatusPageModel.updateMany(
|
||||||
|
{
|
||||||
|
$expr: {
|
||||||
|
$eq: [{ $type: "$type" }, "string"], // only scalar string, not existing array
|
||||||
|
},
|
||||||
|
},
|
||||||
|
[
|
||||||
|
{
|
||||||
|
$set: {
|
||||||
|
type: ["$type"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result.modifiedCount === 0) {
|
||||||
|
logger.info({ service: SERVICE_NAME, message: "No StatusPage documents needed migration" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info({
|
||||||
|
service: SERVICE_NAME,
|
||||||
|
message: `Migration complete. Converted ${result.modifiedCount} StatusPage document(s) from string type to array type`,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
logger.error({ service: SERVICE_NAME, message: `Error during StatusPage type migration: ${errorMessage}` });
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,8 @@ import { convertChecksToTimeSeries } from "./0002_convertChecksToTimeSeries.js";
|
|||||||
import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js";
|
import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStats.js";
|
||||||
import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js";
|
import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js";
|
||||||
import MigrationModel from "../models/Migration.js";
|
import MigrationModel from "../models/Migration.js";
|
||||||
|
import { migrateStatusPageTypeToArray } from "./0005_migrateStatusPageTypeToArray.js";
|
||||||
|
import type { ILogger } from "@/utils/logger.js";
|
||||||
|
|
||||||
type MigrationEntry = {
|
type MigrationEntry = {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -14,9 +16,10 @@ const migrations: MigrationEntry[] = [
|
|||||||
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
|
{ name: "0002_convertChecksToTimeSeries", execute: convertChecksToTimeSeries },
|
||||||
{ name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats },
|
{ name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats },
|
||||||
{ name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds },
|
{ name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds },
|
||||||
|
{ name: "0005_migrateStatusPageTypeToArray", execute: migrateStatusPageTypeToArray },
|
||||||
];
|
];
|
||||||
|
|
||||||
const runMigrations = async (logger?: { info: Function; error: Function }) => {
|
const runMigrations = async (logger?: ILogger) => {
|
||||||
try {
|
try {
|
||||||
logger?.info({ message: "Running migrations", service: "Migrations" });
|
logger?.info({ message: "Running migrations", service: "Migrations" });
|
||||||
for (const migration of migrations) {
|
for (const migration of migrations) {
|
||||||
@@ -43,7 +46,7 @@ const runMigrations = async (logger?: { info: Function; error: Function }) => {
|
|||||||
logger?.info({ message: "Migrations completed", service: "Migrations" });
|
logger?.info({ message: "Migrations completed", service: "Migrations" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const err = error as Error;
|
const err = error as Error;
|
||||||
logger?.error({ message: "Migration failed", service: "Migrations", details: err?.message, stack: err?.stack });
|
logger?.error({ message: "Migration failed", service: "Migrations", details: { error: err?.message }, stack: err?.stack });
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
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;
|
||||||
|
`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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;`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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;`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
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;`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
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;`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
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;`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
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;`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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;`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
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;`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
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;`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
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;`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
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;`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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;`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
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;`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
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;`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
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;`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
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;`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
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;
|
||||||
|
`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
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();
|
||||||
|
`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
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;
|
||||||
|
`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
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);`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
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<void>;
|
||||||
|
down: (pool: Pool) => Promise<void>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 };
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
import { Schema, model, type Types } from "mongoose";
|
||||||
|
import { DLQItemTypes, DLQItemStatuses, type DLQItem } from "@/types/dlqItem.js";
|
||||||
|
|
||||||
|
export interface DLQItemDocument extends Omit<DLQItem, "id" | "monitorId" | "teamId" | "nextRetryAt" | "createdAt" | "updatedAt"> {
|
||||||
|
_id: Types.ObjectId;
|
||||||
|
monitorId: Types.ObjectId;
|
||||||
|
teamId: Types.ObjectId;
|
||||||
|
nextRetryAt: Date;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DLQItemSchema = new Schema<DLQItemDocument>(
|
||||||
|
{
|
||||||
|
type: {
|
||||||
|
type: String,
|
||||||
|
enum: DLQItemTypes,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
status: {
|
||||||
|
type: String,
|
||||||
|
enum: DLQItemStatuses,
|
||||||
|
default: "pending",
|
||||||
|
},
|
||||||
|
payload: {
|
||||||
|
type: Schema.Types.Mixed,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
monitorId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: "Monitor",
|
||||||
|
required: true,
|
||||||
|
immutable: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
teamId: {
|
||||||
|
type: Schema.Types.ObjectId,
|
||||||
|
ref: "Team",
|
||||||
|
required: true,
|
||||||
|
immutable: true,
|
||||||
|
index: true,
|
||||||
|
},
|
||||||
|
retryCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
maxRetries: {
|
||||||
|
type: Number,
|
||||||
|
default: 5,
|
||||||
|
},
|
||||||
|
lastError: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
nextRetryAt: {
|
||||||
|
type: Date,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ timestamps: true }
|
||||||
|
);
|
||||||
|
|
||||||
|
DLQItemSchema.index({ status: 1, nextRetryAt: 1 });
|
||||||
|
DLQItemSchema.index({ teamId: 1, status: 1 });
|
||||||
|
DLQItemSchema.index({ createdAt: 1 });
|
||||||
|
|
||||||
|
const DLQItemModel = model<DLQItemDocument>("DLQItem", DLQItemSchema);
|
||||||
|
|
||||||
|
export { DLQItemModel };
|
||||||
|
export default DLQItemModel;
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Schema, model, type Types } from "mongoose";
|
import { Schema, model, type Types } from "mongoose";
|
||||||
import type { MaintenanceWindow, DurationUnit } from "@/types/maintenanceWindow.js";
|
import type { MaintenanceWindow } from "@/types/maintenanceWindow.js";
|
||||||
|
|
||||||
type MaintenanceWindowDocumentBase = Omit<MaintenanceWindow, "id" | "monitorId" | "teamId" | "start" | "end" | "createdAt" | "updatedAt"> & {
|
type MaintenanceWindowDocumentBase = Omit<MaintenanceWindow, "id" | "monitorId" | "teamId" | "start" | "end" | "createdAt" | "updatedAt"> & {
|
||||||
monitorId: Types.ObjectId;
|
monitorId: Types.ObjectId;
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
import { Schema, model, Types, type UpdateQuery } from "mongoose";
|
import { Schema, model, Types } from "mongoose";
|
||||||
import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js";
|
import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/types/monitor.js";
|
||||||
import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js";
|
import { MonitorTypes, MonitorStatuses } from "@/types/monitor.js";
|
||||||
|
import type {
|
||||||
|
CheckAudits,
|
||||||
|
CheckCaptureInfo,
|
||||||
|
CheckCpuInfo,
|
||||||
|
CheckDiskInfo,
|
||||||
|
CheckErrorInfo,
|
||||||
|
CheckHostInfo,
|
||||||
|
CheckMemoryInfo,
|
||||||
|
CheckNetworkInterfaceInfo,
|
||||||
|
GotTimings,
|
||||||
|
ILighthouseAudit,
|
||||||
|
} from "@/types/check.js";
|
||||||
|
|
||||||
type CheckSnapshotDocument = Omit<CheckSnapshot, "createdAt"> & { createdAt: Date };
|
type CheckSnapshotDocument = Omit<CheckSnapshot, "createdAt"> & { createdAt: Date };
|
||||||
|
|
||||||
@@ -23,13 +35,167 @@ interface MonitorDocument extends MonitorDocumentBase {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const snapshotTimingPhasesSchema = new Schema<GotTimings["phases"]>(
|
||||||
|
{
|
||||||
|
wait: { type: Number },
|
||||||
|
dns: { type: Number },
|
||||||
|
tcp: { type: Number },
|
||||||
|
tls: { type: Number },
|
||||||
|
request: { type: Number },
|
||||||
|
firstByte: { type: Number },
|
||||||
|
download: { type: Number },
|
||||||
|
total: { type: Number },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotTimingsSchema = new Schema<GotTimings>(
|
||||||
|
{
|
||||||
|
start: { type: Number },
|
||||||
|
socket: { type: Number },
|
||||||
|
lookup: { type: Number },
|
||||||
|
connect: { type: Number },
|
||||||
|
secureConnect: { type: Number },
|
||||||
|
upload: { type: Number },
|
||||||
|
response: { type: Number },
|
||||||
|
end: { type: Number },
|
||||||
|
phases: { type: snapshotTimingPhasesSchema },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotCpuSchema = new Schema<CheckCpuInfo>(
|
||||||
|
{
|
||||||
|
physical_core: { type: Number },
|
||||||
|
logical_core: { type: Number },
|
||||||
|
frequency: { type: Number },
|
||||||
|
current_frequency: { type: Number },
|
||||||
|
temperature: { type: [Number] },
|
||||||
|
free_percent: { type: Number },
|
||||||
|
usage_percent: { type: Number },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotMemorySchema = new Schema<CheckMemoryInfo>(
|
||||||
|
{
|
||||||
|
total_bytes: { type: Number },
|
||||||
|
available_bytes: { type: Number },
|
||||||
|
used_bytes: { type: Number },
|
||||||
|
usage_percent: { type: Number },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotDiskSchema = new Schema<CheckDiskInfo>(
|
||||||
|
{
|
||||||
|
device: { type: String },
|
||||||
|
mountpoint: { type: String },
|
||||||
|
total_bytes: { type: Number },
|
||||||
|
free_bytes: { type: Number },
|
||||||
|
used_bytes: { type: Number },
|
||||||
|
usage_percent: { type: Number },
|
||||||
|
total_inodes: { type: Number },
|
||||||
|
free_inodes: { type: Number },
|
||||||
|
used_inodes: { type: Number },
|
||||||
|
inodes_usage_percent: { type: Number },
|
||||||
|
read_bytes: { type: Number },
|
||||||
|
write_bytes: { type: Number },
|
||||||
|
read_time: { type: Number },
|
||||||
|
write_time: { type: Number },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotHostSchema = new Schema<CheckHostInfo>(
|
||||||
|
{
|
||||||
|
os: { type: String },
|
||||||
|
platform: { type: String },
|
||||||
|
kernel_version: { type: String },
|
||||||
|
pretty_name: { type: String },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotErrorSchema = new Schema<CheckErrorInfo>(
|
||||||
|
{
|
||||||
|
metric: { type: [String] },
|
||||||
|
err: { type: String },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotCaptureSchema = new Schema<CheckCaptureInfo>(
|
||||||
|
{
|
||||||
|
version: { type: String },
|
||||||
|
mode: { type: String },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotNetworkInterfaceSchema = new Schema<CheckNetworkInterfaceInfo>(
|
||||||
|
{
|
||||||
|
name: { type: String },
|
||||||
|
bytes_sent: { type: Number },
|
||||||
|
bytes_recv: { type: Number },
|
||||||
|
packets_sent: { type: Number },
|
||||||
|
packets_recv: { type: Number },
|
||||||
|
err_in: { type: Number },
|
||||||
|
err_out: { type: Number },
|
||||||
|
drop_in: { type: Number },
|
||||||
|
drop_out: { type: Number },
|
||||||
|
fifo_in: { type: Number },
|
||||||
|
fifo_out: { type: Number },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotLighthouseAuditSchema = new Schema<ILighthouseAudit>(
|
||||||
|
{
|
||||||
|
id: { type: String },
|
||||||
|
title: { type: String },
|
||||||
|
score: { type: Number },
|
||||||
|
displayValue: { type: String },
|
||||||
|
numericValue: { type: Number },
|
||||||
|
numericUnit: { type: String },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
|
const snapshotAuditsSchema = new Schema<CheckAudits>(
|
||||||
|
{
|
||||||
|
cls: { type: snapshotLighthouseAuditSchema },
|
||||||
|
si: { type: snapshotLighthouseAuditSchema },
|
||||||
|
fcp: { type: snapshotLighthouseAuditSchema },
|
||||||
|
lcp: { type: snapshotLighthouseAuditSchema },
|
||||||
|
tbt: { type: snapshotLighthouseAuditSchema },
|
||||||
|
},
|
||||||
|
{ _id: false }
|
||||||
|
);
|
||||||
|
|
||||||
const checkSnapshotSchema = new Schema<CheckSnapshotDocument>(
|
const checkSnapshotSchema = new Schema<CheckSnapshotDocument>(
|
||||||
{
|
{
|
||||||
id: { type: String, required: true },
|
id: { type: String, required: true },
|
||||||
status: { type: Boolean, required: true },
|
status: { type: Boolean, required: true },
|
||||||
|
responseTime: { type: Number },
|
||||||
|
timings: { type: snapshotTimingsSchema },
|
||||||
|
statusCode: { type: Number },
|
||||||
|
message: { type: String },
|
||||||
|
cpu: { type: snapshotCpuSchema },
|
||||||
|
memory: { type: snapshotMemorySchema },
|
||||||
|
disk: { type: [snapshotDiskSchema] },
|
||||||
|
host: { type: snapshotHostSchema },
|
||||||
|
errors: { type: [snapshotErrorSchema] },
|
||||||
|
capture: { type: snapshotCaptureSchema },
|
||||||
|
net: { type: [snapshotNetworkInterfaceSchema] },
|
||||||
|
accessibility: { type: Number },
|
||||||
|
bestPractices: { type: Number },
|
||||||
|
seo: { type: Number },
|
||||||
|
performance: { type: Number },
|
||||||
|
audits: { type: snapshotAuditsSchema },
|
||||||
createdAt: { type: Date, required: true },
|
createdAt: { type: Date, required: true },
|
||||||
},
|
},
|
||||||
{ _id: false, strict: false }
|
{ _id: false }
|
||||||
);
|
);
|
||||||
|
|
||||||
const MonitorSchema = new Schema<MonitorDocument>(
|
const MonitorSchema = new Schema<MonitorDocument>(
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const NotificationSchema = new Schema<NotificationDocument>(
|
|||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: String,
|
||||||
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams"] as NotificationChannel[],
|
enum: ["email", "slack", "discord", "webhook", "pager_duty", "matrix", "teams", "telegram"] as NotificationChannel[],
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
notificationName: {
|
notificationName: {
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ const StatusPageSchema = new Schema<StatusPageDocument>(
|
|||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
type: {
|
type: {
|
||||||
type: String,
|
type: [String],
|
||||||
required: true,
|
required: true,
|
||||||
default: "uptime",
|
default: ["uptime"],
|
||||||
enum: StatusPageTypes,
|
enum: StatusPageTypes,
|
||||||
},
|
},
|
||||||
companyName: {
|
companyName: {
|
||||||
@@ -100,6 +100,10 @@ const StatusPageSchema = new Schema<StatusPageDocument>(
|
|||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
showInfrastructure: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
customCSS: {
|
customCSS: {
|
||||||
type: String,
|
type: String,
|
||||||
default: "",
|
default: "",
|
||||||
|
|||||||
@@ -36,3 +36,6 @@ export { default as MaintenanceWindowModel } from "@/db/models/MaintenanceWindow
|
|||||||
|
|
||||||
export * from "@/db/models/GeoCheck.js";
|
export * from "@/db/models/GeoCheck.js";
|
||||||
export { default as GeoCheckModel } from "@/db/models/GeoCheck.js";
|
export { default as GeoCheckModel } from "@/db/models/GeoCheck.js";
|
||||||
|
|
||||||
|
export * from "@/db/models/DLQItem.js";
|
||||||
|
export { default as DLQItemModel } from "@/db/models/DLQItem.js";
|
||||||
+6
-12
@@ -1,16 +1,14 @@
|
|||||||
import { initializeServices } from "./config/services.js";
|
import { initializeServices } from "./config/services.js";
|
||||||
import { initializeControllers } from "./config/controllers.js";
|
import { initializeControllers } from "./config/controllers.js";
|
||||||
import { createApp } from "./app.js";
|
import { createApp } from "./app.js";
|
||||||
import { initShutdownListener } from "./shutdown.js";
|
import { initShutdownListener } from "@/shutdown.js";
|
||||||
import { validateEnv } from "./validation/envValidation.js";
|
import { validateEnv } from "@/validation/envValidation.js";
|
||||||
import { fileURLToPath } from "url";
|
import { fileURLToPath } from "url";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
import { runMigrations } from "./db/migration/index.js";
|
|
||||||
|
|
||||||
import Logger, { ILogger } from "@/utils/logger.js";
|
import Logger, { ILogger } from "@/utils/logger.js";
|
||||||
import { SettingsService } from "@/service/index.js";
|
import { SettingsService } from "@/service/index.js";
|
||||||
import { MongoSettingsRepository } from "./repositories/index.js";
|
|
||||||
|
|
||||||
const SERVICE_NAME = "Server";
|
const SERVICE_NAME = "Server";
|
||||||
let logger: ILogger;
|
let logger: ILogger;
|
||||||
@@ -25,19 +23,15 @@ const startApp = async () => {
|
|||||||
const openApiSpec = JSON.parse(fs.readFileSync(path.join(__dirname, "../openapi.json"), "utf8"));
|
const openApiSpec = JSON.parse(fs.readFileSync(path.join(__dirname, "../openapi.json"), "utf8"));
|
||||||
const frontendPath = path.join(__dirname, "..", "public");
|
const frontendPath = path.join(__dirname, "..", "public");
|
||||||
|
|
||||||
// Create services
|
// Create settings service (env only — DB repository injected after connect)
|
||||||
const settingsRepository = new MongoSettingsRepository();
|
const settingsService = new SettingsService(env);
|
||||||
const settingsService = new SettingsService(settingsRepository, env);
|
|
||||||
|
|
||||||
const envSettings = settingsService.loadSettings();
|
const envSettings = settingsService.loadSettings();
|
||||||
|
|
||||||
// Create logger
|
// Create logger
|
||||||
logger = new Logger({ envSettings });
|
logger = new Logger({ envSettings });
|
||||||
|
|
||||||
// Initialize services
|
// Initialize services (connects DB, creates repositories, injects settingsRepository)
|
||||||
const services = await initializeServices({ logger, envSettings, settingsService, settingsRepository });
|
const services = await initializeServices({ logger, envSettings, settingsService });
|
||||||
|
|
||||||
await runMigrations(logger);
|
|
||||||
|
|
||||||
// Initialize controllers
|
// Initialize controllers
|
||||||
const controllers = initializeControllers(services);
|
const controllers = initializeControllers(services);
|
||||||
|
|||||||
@@ -1,15 +1,18 @@
|
|||||||
import type { NextFunction, Request, Response } from "express";
|
import type { NextFunction, Request, Response } from "express";
|
||||||
import { logger } from "@/utils/logger.js";
|
import { logger } from "@/utils/logger.js";
|
||||||
|
import { AppError } from "@/utils/AppError.js";
|
||||||
|
|
||||||
const handleErrors = (error: any, req: Request, res: Response, next: NextFunction) => {
|
const handleErrors = (error: unknown, req: Request, res: Response, _next: NextFunction) => {
|
||||||
const status = error.status || 500;
|
const status = error instanceof AppError ? error.status || 500 : 500;
|
||||||
const message = error.message || "Server error";
|
const message = error instanceof AppError ? error.message : error instanceof Error ? error.message : "Server error";
|
||||||
const service = error.service || "unknownService";
|
const service = error instanceof AppError ? error.service : "unknownService";
|
||||||
|
const method = error instanceof AppError ? error.method : "unknownMethod";
|
||||||
logger.error({
|
logger.error({
|
||||||
message: message,
|
message: message,
|
||||||
service: service,
|
service: service,
|
||||||
method: error.method,
|
method: method,
|
||||||
stack: error.stack,
|
stack: error instanceof Error ? error.stack : undefined,
|
||||||
|
details: error instanceof AppError ? error.details : undefined,
|
||||||
});
|
});
|
||||||
res.status(status).json({
|
res.status(status).json({
|
||||||
status,
|
status,
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { Request, Response, NextFunction } from "express";
|
import type { Request, Response, NextFunction } from "express";
|
||||||
import DOMPurify from "isomorphic-dompurify";
|
import DOMPurify from "isomorphic-dompurify";
|
||||||
|
|
||||||
export const sanitizeInput = (input: any, options = {}) => {
|
export function sanitizeInput(input: string, options?: object): string;
|
||||||
|
export function sanitizeInput(input: unknown, options?: object): unknown;
|
||||||
|
export function sanitizeInput(input: unknown, options = {}): unknown {
|
||||||
if (typeof input !== "string") {
|
if (typeof input !== "string") {
|
||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
@@ -15,9 +17,9 @@ export const sanitizeInput = (input: any, options = {}) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return DOMPurify.sanitize(input, defaultConfig);
|
return DOMPurify.sanitize(input, defaultConfig);
|
||||||
};
|
}
|
||||||
|
|
||||||
export const sanitizeObject = (obj: Record<string, any>, options = {}): Record<string, any> => {
|
export const sanitizeObject = (obj: unknown, options = {}): unknown => {
|
||||||
if (typeof obj !== "object" || obj === null) {
|
if (typeof obj !== "object" || obj === null) {
|
||||||
return obj;
|
return obj;
|
||||||
}
|
}
|
||||||
@@ -26,7 +28,7 @@ export const sanitizeObject = (obj: Record<string, any>, options = {}): Record<s
|
|||||||
return obj.map((item) => sanitizeObject(item, options));
|
return obj.map((item) => sanitizeObject(item, options));
|
||||||
}
|
}
|
||||||
|
|
||||||
const sanitized: Record<string, any> = {};
|
const sanitized: Record<string, unknown> = {};
|
||||||
for (const [key, value] of Object.entries(obj)) {
|
for (const [key, value] of Object.entries(obj)) {
|
||||||
if (typeof value === "string") {
|
if (typeof value === "string") {
|
||||||
sanitized[key] = sanitizeInput(value, options);
|
sanitized[key] = sanitizeInput(value, options);
|
||||||
@@ -52,7 +54,12 @@ export const sanitizeBody = (options = {}): ((req: Request, res: Response, next:
|
|||||||
export const sanitizeQuery = (options = {}): ((req: Request, res: Response, next: NextFunction) => void) => {
|
export const sanitizeQuery = (options = {}): ((req: Request, res: Response, next: NextFunction) => void) => {
|
||||||
return (req: Request, res: Response, next: NextFunction) => {
|
return (req: Request, res: Response, next: NextFunction) => {
|
||||||
if (req.query && typeof req.query === "object") {
|
if (req.query && typeof req.query === "object") {
|
||||||
req.query = sanitizeObject(req.query, options);
|
for (const key of Object.keys(req.query)) {
|
||||||
|
const value = req.query[key];
|
||||||
|
if (typeof value === "string") {
|
||||||
|
req.query[key] = sanitizeInput(value, options);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
|
|||||||
Loaded 100 of 232 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user