Compare commits
182
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dbccce445b | ||
|
|
6ed0818bad | ||
|
|
f2e3640e28 | ||
|
|
8bfffd540d | ||
|
|
559288fceb | ||
|
|
8886566a8f | ||
|
|
28cb2aeacd | ||
|
|
cbd82e29af | ||
|
|
50876e57b9 | ||
|
|
b70916de20 | ||
|
|
3202a4276d | ||
|
|
7a8caec159 | ||
|
|
7e7f1bb6ca | ||
|
|
5f12459bfd | ||
|
|
27e866ba08 | ||
|
|
f7a2b2af54 | ||
|
|
3f35176651 | ||
|
|
62c7e20726 | ||
|
|
ff08645087 | ||
|
|
3e947ca2df | ||
|
|
cc32206547 | ||
|
|
984a611d19 | ||
|
|
39011e2e33 | ||
|
|
4998fe4c94 | ||
|
|
247308b636 | ||
|
|
33110cf6c8 | ||
|
|
15c3fcb9b9 | ||
|
|
d5295f4f6c | ||
|
|
886e04710f | ||
|
|
d0e79ea99c | ||
|
|
a0568a44a6 | ||
|
|
4435a66255 | ||
|
|
dbc592b86b | ||
|
|
47a78edba8 | ||
|
|
f4fd836137 | ||
|
|
6c6823cf76 | ||
|
|
59187cc114 | ||
|
|
0ad9ef15af | ||
|
|
e23a185eaf | ||
|
|
36cbe4096d | ||
|
|
ab692f8033 | ||
|
|
15542154f4 | ||
|
|
7eb7fa1629 | ||
|
|
7906fce77f | ||
|
|
c57614e8df | ||
|
|
d5d6797f91 | ||
|
|
77545e9cf7 | ||
|
|
29ea54419c | ||
|
|
9042cb917a | ||
|
|
a65e1071a5 | ||
|
|
4493cca871 | ||
|
|
999dfb13b9 | ||
|
|
97f4008f4c | ||
|
|
6152144807 | ||
|
|
b376fe9ab0 | ||
|
|
aa3e097e16 | ||
|
|
d77aeeb46c | ||
|
|
82b7ab83dd | ||
|
|
88bc4e080d | ||
|
|
fa6aef1e86 | ||
|
|
57ed0e1e7d | ||
|
|
26077151ef | ||
|
|
55b05a2100 | ||
|
|
9210259511 | ||
|
|
e9a0914fad | ||
|
|
f392bee287 | ||
|
|
4281e87642 | ||
|
|
d40b766c83 | ||
|
|
d139db7697 | ||
|
|
7547d1db2e | ||
|
|
24d7ead1d1 | ||
|
|
e60dbb9f74 | ||
|
|
f12a96ce58 | ||
|
|
5920b92869 | ||
|
|
8f55d966f9 | ||
|
|
c7ef412d7a | ||
|
|
14f76ec10b | ||
|
|
d532d13371 | ||
|
|
58532d1ff6 | ||
|
|
b5fe314dde | ||
|
|
0d163e7100 | ||
|
|
5dda0b51c3 | ||
|
|
273aa7d01a | ||
|
|
cc1b6df8f2 | ||
|
|
97353bc7d2 | ||
|
|
10a703d657 | ||
|
|
b3077f6239 | ||
|
|
80ae5a1977 | ||
|
|
8579708f05 | ||
|
|
675ec40836 | ||
|
|
7b164e4df5 | ||
|
|
2be1b48995 | ||
|
|
be3b7c2e86 | ||
|
|
69bec42a66 | ||
|
|
12b879ced5 | ||
|
|
9e24cc655c | ||
|
|
95db80db5d | ||
|
|
ab7f2c42f5 | ||
|
|
be462811ac | ||
|
|
564b356115 | ||
|
|
bcacd42162 | ||
|
|
6f7fba5ae5 | ||
|
|
61d753b119 | ||
|
|
b482e871a3 | ||
|
|
bca602f4b0 | ||
|
|
f6222014b7 | ||
|
|
360e8ad2a0 | ||
|
|
e8bcfdd21d | ||
|
|
fd8dce8abe | ||
|
|
b9c14c76c9 | ||
|
|
bf60c52692 | ||
|
|
a8339c0169 | ||
|
|
d74f1bb6a8 | ||
|
|
9bb7224fc4 | ||
|
|
403290db32 | ||
|
|
98ae6b1105 | ||
|
|
2014825618 | ||
|
|
4a61bc88d3 | ||
|
|
fa4c0632ae | ||
|
|
ece16158ef | ||
|
|
bd27721685 | ||
|
|
7c8549d092 | ||
|
|
1e8c9d0187 | ||
|
|
faa59ab694 | ||
|
|
de4de0f5db | ||
|
|
48c1e326c8 | ||
|
|
22f54391d0 | ||
|
|
0bb3320010 | ||
|
|
7998bae87d | ||
|
|
ec929aef45 | ||
|
|
77e72bc3bf | ||
|
|
e1b00ce735 | ||
|
|
0fcb38f7e6 | ||
|
|
b3930b31f2 | ||
|
|
6222635eef | ||
|
|
dc7688bc13 | ||
|
|
c287cf16d1 | ||
|
|
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 |
No files matched your search
@@ -0,0 +1,2 @@
|
||||
* text=auto
|
||||
* text eol=lf
|
||||
@@ -1,27 +1,28 @@
|
||||
**(Please remove this line only before submitting your PR. Ensure that all relevant items are checked before submission.)**
|
||||
**(Please remove this line only before submitting your PR. Ensure that all relevant items are checked before submission.)**
|
||||
|
||||
## Describe your changes
|
||||
|
||||
Briefly describe the changes you made and their purpose.
|
||||
Briefly describe the changes you made and their purpose.
|
||||
|
||||
## Write your issue number after "Fixes "
|
||||
|
||||
Fixes #123
|
||||
Fixes #123
|
||||
|
||||
## Please ensure all items are checked off before requesting a review. "Checked off" means you need to add an "x" character between brackets so they turn into checkmarks.
|
||||
|
||||
- [ ] (Do not skip this or your PR will be closed) I deployed the application locally.
|
||||
- [ ] (Do not skip this or your PR will be closed) I have performed a self-review and testing of my code.
|
||||
- [ ] (Do not skip this or your PR will be closed) I have performed a self-reviewing and testing of my code.
|
||||
- [ ] I have included the issue # in the PR.
|
||||
- [ ] I have added i18n support to visible strings (instead of `<div>Add</div>`, use):
|
||||
- [ ] I have added i18n support to visible strings (instead of `<div>Add</div>`, use):
|
||||
|
||||
```Javascript
|
||||
const { t } = useTranslation();
|
||||
<div>{t('add')}</div>
|
||||
```
|
||||
|
||||
- [ ] I have **not** included any files that are not related to my pull request, including package-lock and package-json if dependencies have not changed
|
||||
- [ ] I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application).
|
||||
- [ ] I made sure font sizes, color choices etc are all referenced from the theme. I don't have any hardcoded dimensions.
|
||||
- [ ] My PR is granular and targeted to one specific feature.
|
||||
- [ ] I ran `npm run format` in server and client directories, which automatically formats your code.
|
||||
- [ ] I took a screenshot or a video and attached to this PR if there is a UI change.
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
name: Test Check (Server)
|
||||
on:
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test-server:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
|
||||
- name: Install server dependencies
|
||||
working-directory: server
|
||||
run: npm install
|
||||
|
||||
- name: Run server tests
|
||||
working-directory: server
|
||||
run: npm test
|
||||
+122
-104
@@ -1,120 +1,138 @@
|
||||
name: POEditor Translation Synchronization
|
||||
name: POEditor Translation Synchronization (disabled)
|
||||
|
||||
# This workflow is intentionally disabled — kept for reference only.
|
||||
# The full definition is preserved commented-out below. To re-enable,
|
||||
# delete the stub job and uncomment the section starting at `on:`.
|
||||
|
||||
on:
|
||||
# For manual triggering
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
languages:
|
||||
description: "Languages to synchronize (comma separated, e.g.: tr,en,es)"
|
||||
required: false
|
||||
default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn,th"
|
||||
format:
|
||||
description: "Export format (key_value_json or json)"
|
||||
required: false
|
||||
default: "key_value_json"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
pull-requests: write
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
sync-translations:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
issues: write
|
||||
pull-requests: write
|
||||
disabled:
|
||||
if: false
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- run: echo "This workflow is disabled. See comments below for the original definition."
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fetch-depth: 0
|
||||
# ---------------------------------------------------------------------------
|
||||
# Original (disabled) workflow below. Do not uncomment if you don't know what you're doing.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "22"
|
||||
# on:
|
||||
# # For manual triggering
|
||||
# workflow_dispatch:
|
||||
# inputs:
|
||||
# languages:
|
||||
# description: "Languages to synchronize (comma separated, e.g.: tr,en,es)"
|
||||
# required: false
|
||||
# default: "ar,zh-tw,cs,en,fi,fr,de,pt-br,ru,es,tr,ja,zh-cn,th"
|
||||
# format:
|
||||
# description: "Export format (key_value_json or json)"
|
||||
# required: false
|
||||
# default: "key_value_json"
|
||||
|
||||
- name: Create package.json for scripts
|
||||
run: |
|
||||
mkdir -p .github/scripts
|
||||
cat > .github/scripts/package.json << EOF
|
||||
{
|
||||
"name": "poeditor-scripts",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"fs-extra": "^11.1.1"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
# permissions:
|
||||
# contents: write
|
||||
# pull-requests: write
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
cd .github/scripts
|
||||
npm install
|
||||
# jobs:
|
||||
# sync-translations:
|
||||
# runs-on: ubuntu-latest
|
||||
# permissions:
|
||||
# contents: write
|
||||
# issues: write
|
||||
# pull-requests: write
|
||||
|
||||
- name: Download translations from POEditor
|
||||
env:
|
||||
POEDITOR_API: ${{ secrets.POEDITOR_API }}
|
||||
POEDITOR_PROJECT_ID: ${{ secrets.POEDITOR_PROJECT_ID }}
|
||||
LANGUAGES: ${{ github.event.inputs.languages || 'tr,en' }}
|
||||
EXPORT_FORMAT: ${{ github.event.inputs.format || 'key_value_json' }}
|
||||
run: |
|
||||
mkdir -p temp
|
||||
node .github/scripts/download-translations.js
|
||||
# steps:
|
||||
# - name: Checkout code
|
||||
# uses: actions/checkout@v3
|
||||
# with:
|
||||
# token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# fetch-depth: 0
|
||||
|
||||
- name: Verify translation files
|
||||
run: |
|
||||
echo "Verifying translation files..."
|
||||
for file in temp/*.json; do
|
||||
echo "Checking $file"
|
||||
if [ ! -s "$file" ]; then
|
||||
echo "Error: $file is empty or does not exist"
|
||||
exit 1
|
||||
fi
|
||||
# Validate JSON format
|
||||
cat "$file" | jq . > /dev/null || { echo "Error: $file is not valid JSON"; exit 1; }
|
||||
done
|
||||
echo "All translation files are valid"
|
||||
# - name: Setup Node.js
|
||||
# uses: actions/setup-node@v3
|
||||
# with:
|
||||
# node-version: "22"
|
||||
|
||||
- name: Copy translations to project
|
||||
run: |
|
||||
mkdir -p client/src/locales
|
||||
cp -r temp/* client/src/locales/
|
||||
echo "Translation files copied to client/src/locales/"
|
||||
# - name: Create package.json for scripts
|
||||
# run: |
|
||||
# mkdir -p .github/scripts
|
||||
# cat > .github/scripts/package.json << EOF
|
||||
# {
|
||||
# "name": "poeditor-scripts",
|
||||
# "version": "1.0.0",
|
||||
# "private": true,
|
||||
# "type": "module",
|
||||
# "dependencies": {
|
||||
# "axios": "^1.6.0",
|
||||
# "fs-extra": "^11.1.1"
|
||||
# }
|
||||
# }
|
||||
# EOF
|
||||
|
||||
- name: Get current date
|
||||
id: date
|
||||
run: echo "date=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT
|
||||
# - name: Install dependencies
|
||||
# run: |
|
||||
# cd .github/scripts
|
||||
# npm install
|
||||
|
||||
- name: Format client code
|
||||
run: |
|
||||
cd client
|
||||
npm ci
|
||||
npm run format
|
||||
# - name: Download translations from POEditor
|
||||
# env:
|
||||
# POEDITOR_API: ${{ secrets.POEDITOR_API }}
|
||||
# POEDITOR_PROJECT_ID: ${{ secrets.POEDITOR_PROJECT_ID }}
|
||||
# LANGUAGES: ${{ github.event.inputs.languages || 'tr,en' }}
|
||||
# EXPORT_FORMAT: ${{ github.event.inputs.format || 'key_value_json' }}
|
||||
# run: |
|
||||
# mkdir -p temp
|
||||
# node .github/scripts/download-translations.js
|
||||
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "feat: translations updated from POEditor"
|
||||
title: "🌐 Updated Translations from POEditor"
|
||||
body: |
|
||||
This PR contains the latest translations from POEditor.
|
||||
# - name: Verify translation files
|
||||
# run: |
|
||||
# echo "Verifying translation files..."
|
||||
# for file in temp/*.json; do
|
||||
# echo "Checking $file"
|
||||
# if [ ! -s "$file" ]; then
|
||||
# echo "Error: $file is empty or does not exist"
|
||||
# exit 1
|
||||
# fi
|
||||
# # Validate JSON format
|
||||
# cat "$file" | jq . > /dev/null || { echo "Error: $file is not valid JSON"; exit 1; }
|
||||
# done
|
||||
# echo "All translation files are valid"
|
||||
|
||||
📅 Update Date: ${{ steps.date.outputs.date }}
|
||||
🔄 Updated Languages: ${{ github.event.inputs.languages || 'tr,en' }}
|
||||
# - name: Copy translations to project
|
||||
# run: |
|
||||
# mkdir -p client/src/locales
|
||||
# cp -r temp/* client/src/locales/
|
||||
# echo "Translation files copied to client/src/locales/"
|
||||
|
||||
⚠️ Please review the translations and approve the PR if everything looks correct.
|
||||
branch: translation-update-${{ github.run_number }}
|
||||
delete-branch: true
|
||||
base: develop
|
||||
add-paths: |
|
||||
client/src/locales/*.json
|
||||
committer: GitHub Action <github-actions[bot]@users.noreply.github.com>
|
||||
author: GitHub Action <github-actions[bot]@users.noreply.github.com>
|
||||
# - name: Get current date
|
||||
# id: date
|
||||
# run: echo "date=$(date +'%Y-%m-%d %H:%M:%S')" >> $GITHUB_OUTPUT
|
||||
|
||||
# - name: Format client code
|
||||
# run: |
|
||||
# cd client
|
||||
# npm ci
|
||||
# npm run format
|
||||
|
||||
# - name: Create Pull Request
|
||||
# uses: peter-evans/create-pull-request@v7
|
||||
# with:
|
||||
# token: ${{ secrets.GITHUB_TOKEN }}
|
||||
# commit-message: "feat: translations updated from POEditor"
|
||||
# title: "🌐 Updated Translations from POEditor"
|
||||
# body: |
|
||||
# This PR contains the latest translations from POEditor.
|
||||
|
||||
# 📅 Update Date: ${{ steps.date.outputs.date }}
|
||||
# 🔄 Updated Languages: ${{ github.event.inputs.languages || 'tr,en' }}
|
||||
|
||||
# ⚠️ Please review the translations and approve the PR if everything looks correct.
|
||||
# branch: translation-update-${{ github.run_number }}
|
||||
# delete-branch: true
|
||||
# base: develop
|
||||
# add-paths: |
|
||||
# client/src/locales/*.json
|
||||
# committer: GitHub Action <github-actions[bot]@users.noreply.github.com>
|
||||
# author: GitHub Action <github-actions[bot]@users.noreply.github.com>
|
||||
@@ -2,7 +2,7 @@ name: Demo deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["demo"]
|
||||
branches: [ "demo" ]
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
docker-build-and-push-client:
|
||||
@@ -84,7 +84,8 @@ jobs:
|
||||
script: |
|
||||
cd checkmate
|
||||
git pull
|
||||
cd docker/prod
|
||||
cd docker/unified
|
||||
docker compose down
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
docker system prune -af
|
||||
@@ -2,7 +2,7 @@ name: Staging deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["develop"]
|
||||
branches: [ "develop" ]
|
||||
|
||||
jobs:
|
||||
docker-build-and-push-client:
|
||||
@@ -78,13 +78,13 @@ jobs:
|
||||
- name: SSH into server and restart container using Docker Compose
|
||||
uses: appleboy/ssh-action@v1.2.2
|
||||
with:
|
||||
host: ${{ secrets.STAGING_SERVER_HOST }}
|
||||
username: ${{ secrets.STAGING_SERVER_USER }}
|
||||
key: ${{ secrets.STAGING_SERVER_SSH_KEY }}
|
||||
host: ${{ secrets.DEMO_SERVER_HOST }}
|
||||
username: ${{ secrets.DEMO_SERVER_USER }}
|
||||
key: ${{ secrets.DEMO_SERVER_SSH_KEY }}
|
||||
script: |
|
||||
cd checkmate
|
||||
git pull
|
||||
cd docker/staging
|
||||
cd docker/unified
|
||||
docker compose down
|
||||
docker compose pull
|
||||
docker compose up -d
|
||||
|
||||
+1
-3
@@ -3,10 +3,8 @@
|
||||
.VSCodeCounter
|
||||
*.sh
|
||||
mongo
|
||||
timescaledb
|
||||
node_modules/
|
||||
docs/architecture
|
||||
docs/reviews
|
||||
docs/todo
|
||||
docs/frontend
|
||||
docs/timescale
|
||||
docs/frontend
|
||||
@@ -105,6 +105,56 @@ client/src/
|
||||
- **Queue/Cache**: Redis + BullMQ + Pulse (cron scheduling)
|
||||
- **i18n**: i18next + react-i18next (translations via PoEditor)
|
||||
|
||||
---
|
||||
|
||||
## Backend Architecture Patterns
|
||||
|
||||
### Repository Pattern & Separation of Concerns
|
||||
|
||||
The backend enforces a strict three-layer separation between HTTP handling, business logic, and data access:
|
||||
|
||||
```
|
||||
Request → Controller → Service → Repository → MongoDB (Mongoose)
|
||||
```
|
||||
|
||||
- **Controllers** (`/controllers`) handle HTTP concerns only: parsing request params, calling the appropriate service, and returning a response via the `responseHandler` middleware. They contain no business logic.
|
||||
- **Services** (`/service/business`) contain all business logic: deciding whether an incident should be created, whether a notification should fire, what state a monitor is in, etc.
|
||||
- **Repositories** (`/repositories`) are the sole layer that talks to MongoDB through Mongoose. They expose clean, reusable query methods (e.g. `findByMonitorId`, `createCheck`) so that services never construct raw DB queries directly.
|
||||
|
||||
This separation makes each layer independently testable and keeps Mongoose-specific code out of business logic. When adding a new feature, the pattern to follow is: add a repository method for any new DB query, call it from a service, and expose it via a controller route.
|
||||
|
||||
### Monitoring Flow: From Check to Notification
|
||||
|
||||
Background monitoring runs on a scheduled queue, not on the HTTP request cycle. The high-level flow for uptime monitoring is:
|
||||
|
||||
```
|
||||
Pulse (cron) → BullMQ Job → StatusService
|
||||
├── performs HTTP/port/ping check
|
||||
├── saves Check via CheckRepository
|
||||
├── evaluates monitor state change
|
||||
│ └── calls IncidentService (create / resolve incident)
|
||||
└── calls NotificationService (email, Slack, Discord, webhook)
|
||||
```
|
||||
|
||||
1. **Pulse** (cron scheduler) enqueues a job into a **BullMQ** queue for each active monitor at its configured interval.
|
||||
2. A **BullMQ worker** picks up the job and calls `StatusService`, which performs the actual check (HTTP request, TCP port probe, ping, etc.).
|
||||
3. The result is persisted as a `Check` document via the repository layer.
|
||||
4. `StatusService` compares the new result against the monitor's previous state. If the monitor transitions from up → down (or down → up), it delegates to `IncidentService` to open or resolve an `Incident` document.
|
||||
5. On a state change, `NotificationService` reads the monitor's configured `Notification` documents and dispatches alerts to all enabled channels (email, Discord, Slack, webhooks).
|
||||
|
||||
### Queue System (BullMQ + Redis)
|
||||
|
||||
Redis serves two roles: job queue storage for BullMQ and ephemeral caching. BullMQ manages concurrency, retries, and backpressure for monitoring jobs, ensuring checks are processed reliably even under load.
|
||||
|
||||
- Each monitor type (HTTP, port, ping, infrastructure) maps to its own queue worker so failures in one type don't block others.
|
||||
- Job scheduling interval is driven by the `interval` field on the `Monitor` model.
|
||||
- Failed jobs are retried with configurable backoff before being moved to a dead-letter state.
|
||||
- Redis is also used to cache frequently read data (e.g. aggregated stats) to reduce MongoDB query pressure.
|
||||
|
||||
When working on anything related to check scheduling, incident lifecycle, or notifications, trace the flow starting from the relevant BullMQ worker rather than from the controller layer.
|
||||
|
||||
---
|
||||
|
||||
## Code Conventions
|
||||
|
||||
### Internationalization
|
||||
@@ -142,4 +192,4 @@ Key Mongoose models in `/server/src/db/models/`:
|
||||
- **StatusPage** - Public status pages
|
||||
- **Notification** - Alert configuration (email, Discord, Slack, webhooks)
|
||||
- **MaintenanceWindow** - Scheduled maintenance periods
|
||||
- **AppSettings** - Global application settings
|
||||
- **AppSettings** - Global application settings
|
||||
+69
-69
@@ -1,69 +1,69 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in the **bluewave-uptime** project and community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community, especially during Hacktoberfest, where we encourage new contributors to join and feel supported.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email address, without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, especially during Hacktoberfest to ensure a positive experience for all contributors.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all project spaces, and it also applies when an individual is officially representing the project or its community in public spaces. Examples of representing our project include using an official project email address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@bluewavelabs.ca. All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All project team members are obligated to respect the privacy and security of the reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Project maintainers will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
||||
**Consequence**: A private, written warning from project maintainers, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
**Community Impact**: A violation through a single incident or series of actions.
|
||||
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
||||
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
---
|
||||
|
||||
By following this Code of Conduct, we can ensure a welcoming and inclusive environment, encouraging new contributors to engage with the project and the community. Thank you for helping make **bluewave-uptime** a positive space for all!
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
We as members, contributors, and leaders pledge to make participation in the **checkmate** project and community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community, especially during Hacktoberfest, where we encourage new contributors to join and feel supported.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to a positive environment for our community include:
|
||||
|
||||
- Demonstrating empathy and kindness toward other people
|
||||
- Being respectful of differing opinions, viewpoints, and experiences
|
||||
- Giving and gracefully accepting constructive feedback
|
||||
- Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
|
||||
- Focusing on what is best not just for us as individuals, but for the overall community
|
||||
|
||||
Examples of unacceptable behavior include:
|
||||
|
||||
- The use of sexualized language or imagery, and sexual attention or advances of any kind
|
||||
- Trolling, insulting or derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or email address, without their explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, especially during Hacktoberfest to ensure a positive experience for all contributors.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies within all project spaces, and it also applies when an individual is officially representing the project or its community in public spaces. Examples of representing our project include using an official project email address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at hello@bluewavelabs.ca. All complaints will be reviewed and investigated promptly and fairly.
|
||||
|
||||
All project team members are obligated to respect the privacy and security of the reporter of any incident.
|
||||
|
||||
## Enforcement Guidelines
|
||||
|
||||
Project maintainers will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
|
||||
|
||||
### 1. Correction
|
||||
**Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
|
||||
**Consequence**: A private, written warning from project maintainers, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
|
||||
|
||||
### 2. Warning
|
||||
**Community Impact**: A violation through a single incident or series of actions.
|
||||
**Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
|
||||
|
||||
### 3. Temporary Ban
|
||||
**Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
|
||||
**Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
|
||||
|
||||
### 4. Permanent Ban
|
||||
**Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
|
||||
**Consequence**: A permanent ban from any sort of public interaction within the community.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 2.0, available at https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
||||
|
||||
---
|
||||
|
||||
By following this Code of Conduct, we can ensure a welcoming and inclusive environment, encouraging new contributors to engage with the project and the community. Thank you for helping make **checkmate** a positive space for all!
|
||||
+7
-1
@@ -16,7 +16,7 @@ PS: **We work closely with contributors on our [Discord channel](https://discord
|
||||
Before you dive in, make sure you have these installed:
|
||||
|
||||
```bash
|
||||
# Check Node.js (v16+ required)
|
||||
# Check Node.js (v20+ required)
|
||||
node --version
|
||||
|
||||
# Check npm
|
||||
@@ -96,10 +96,16 @@ cd docker/dev
|
||||
|
||||
Build the Docker images:
|
||||
|
||||
Linux:
|
||||
```bash
|
||||
./build_images.sh
|
||||
```
|
||||
|
||||
Windows:
|
||||
```bash
|
||||
./build_images.bat
|
||||
```
|
||||
|
||||
Run MongoDB container:
|
||||
|
||||
```bash
|
||||
|
||||
+1
-1
@@ -33,4 +33,4 @@ Ensure you execute `npm run format` before submitting your pull requests in both
|
||||
|
||||
### Keep PRs focused
|
||||
|
||||
It may be tempting to address a bug you suddenly remembered or make some tiny adjustments in some component that bothers you, but don’t! Keep all commits in your pull request fully focused on the specific feature you are working on. Open up another PR if you want to fix a big or work on another feature.
|
||||
It may be tempting to address a bug you suddenly remembered or make some tiny adjustments in some component that bothers you, but don’t! Keep all commits in your pull request fully focused on the specific feature you are working on. Open up another PR if you want to fix a bug or work on another feature.
|
||||
+1
-1
@@ -76,7 +76,7 @@ También puedes ver el consumo de memoria de MongoDB y Redis en el mismo servido
|
||||
Si tienes alguna pregunta, sugerencia o comentario, tienes varias opciones:
|
||||
|
||||
- [Canal de Discord](https://discord.gg/NAb6H3UTjK)
|
||||
- [Discusiones en GitHub](https://github.com/bluewave-labs/bluewave-uptime/discussions)
|
||||
- [Discusiones en GitHub](https://github.com/bluewave-labs/checkmate/discussions)
|
||||
- [Grupo en Reddit](https://www.reddit.com/r/CheckmateMonitoring/)
|
||||
|
||||
¡No dudes en hacer preguntas o compartir tus ideas, nos encantaría saber de ti!
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
<p align="center"><strong>An open source uptime and infrastructure monitoring application</strong></p>
|
||||
|
||||
[](https://www.pikapods.com/pods?run=checkmate)
|
||||
|
||||
<img width="1660" alt="image" src="https://github.com/user-attachments/assets/b748f36d-a271-4965-ad0a-18bf153bbee7" />
|
||||
|
||||
This repository contains both the frontend and the backend of Checkmate, an open-source, self-hosted monitoring tool for tracking server hardware, uptime, response times, and incidents in real-time with beautiful visualizations. Checkmate regularly checks whether a server/website is accessible and performs optimally, providing real-time alerts and reports on the monitored services' availability, downtime, and response time.
|
||||
@@ -29,7 +31,6 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul
|
||||
- [📦 Demo](#demo)
|
||||
- [🔗 User's guide](#users-guide)
|
||||
- [🛠️ Installation](#installation)
|
||||
- [🏁 Translations](#translations)
|
||||
- [🚀 Performance](#performance)
|
||||
- [💚 Questions & Ideas](#questions--ideas)
|
||||
- [🧩 Features](#features)
|
||||
@@ -42,7 +43,9 @@ Checkmate has been stress-tested with 1000+ active monitors without any particul
|
||||
|
||||
## Demo
|
||||
|
||||
You can see the latest build of [Checkmate](https://checkmate-demo.bluewavelabs.ca/) in action. The username is demouser@demo.com and the password is Demouser1! (just a note that we update the demo server from time to time, so if it doesn't work for you, please ping us on the Discussions channel).
|
||||
You can see the latest build of [Checkmate](https://demo.checkmate.so/) in action.
|
||||
|
||||
The username is demouser@demo.com and the password is Demouser1! (just a note that we update the demo server from time to time, so if it doesn't work for you, please ping us on the Discussions channel).
|
||||
|
||||
## User's guide
|
||||
|
||||
@@ -56,7 +59,7 @@ Usage instructions can be found [here](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
|
||||
|
||||
@@ -64,11 +67,6 @@ If you need to monitor internal HTTPS endpoints with certificates from private C
|
||||
|
||||
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
|
||||
|
||||
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:
|
||||
@@ -116,6 +114,15 @@ Feel free to ask questions or share your ideas - we'd love to hear from you!
|
||||
|
||||
If you would like to sponsor an additional feature, [see this page](https://checkmate.so/sponsored-features).
|
||||
|
||||
## Monitor Lifecycle
|
||||
|
||||
1. A monitor executes a check (HTTP / ping / port / hardware via Capture agent)
|
||||
2. The result is stored (success/failure + response time)
|
||||
3. Recent check results are evaluated against the monitor's configured status change threshold
|
||||
4. If the monitor's status change threshold is met and the current status is not equal to the previous status, the monitor's state changes (e.g. `initializing`, `up`, `down`, `breached`)
|
||||
5. Upon a state change: an incident is either created or resolved, depending on the monitor's current status
|
||||
6. Notifications are triggered based on configuration
|
||||
|
||||
## Screenshots
|
||||
|
||||
<p>
|
||||
|
||||
@@ -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();
|
||||
@@ -1,13 +1,12 @@
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { PulseDot, Dot, StrategyBadge } from "@/Components/design-elements";
|
||||
import { PulseDot, Dot } from "@/Components/design-elements";
|
||||
import { getStatusColor, formatUrl } from "@/Utils/MonitorUtils";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import prettyMilliseconds from "pretty-ms";
|
||||
import { typographyLevels } from "@/Utils/Theme/Palette";
|
||||
import useMediaQuery from "@mui/material/useMediaQuery";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
|
||||
const theme = useTheme();
|
||||
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
|
||||
@@ -29,7 +28,7 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems={"center"}
|
||||
gap={theme.spacing(LAYOUT.XS)}
|
||||
gap={theme.spacing(4)}
|
||||
>
|
||||
<PulseDot color={getStatusColor(monitor.status, theme)} />
|
||||
<Typography
|
||||
@@ -42,12 +41,6 @@ export const MonitorStatus = ({ monitor }: { monitor: Monitor }) => {
|
||||
>
|
||||
{formatUrl(monitor?.url)}
|
||||
</Typography>
|
||||
{monitor.type === "pagespeed" && monitor.strategy && (
|
||||
<>
|
||||
<Dot />
|
||||
<StrategyBadge strategy={monitor.strategy} />
|
||||
</>
|
||||
)}
|
||||
{!isSmall && (
|
||||
<>
|
||||
<Dot />
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
import Typography from "@mui/material/Typography";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { PageSpeedStrategy } from "@/Types/Monitor";
|
||||
import { SPACING } from "@/Utils/Theme/constants";
|
||||
|
||||
interface StrategyBadgeProps {
|
||||
strategy: PageSpeedStrategy | undefined;
|
||||
variant?: "caption" | "body2";
|
||||
}
|
||||
|
||||
export const StrategyBadge = ({ strategy, variant = "body2" }: StrategyBadgeProps) => {
|
||||
const theme = useTheme();
|
||||
const { t } = useTranslation();
|
||||
|
||||
if (!strategy) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Typography
|
||||
variant={variant}
|
||||
px={SPACING.XXL}
|
||||
py={SPACING.SM}
|
||||
borderRadius={theme.shape.borderRadius}
|
||||
bgcolor={theme.palette.secondary.light}
|
||||
color={theme.palette.text.primary}
|
||||
>
|
||||
{t(`pages.pageSpeed.strategy.${strategy}`)}
|
||||
</Typography>
|
||||
);
|
||||
};
|
||||
@@ -21,4 +21,3 @@ export * from "./SplitBox";
|
||||
export * from "./TextLink";
|
||||
export * from "./OfflineBanner";
|
||||
export * from "./Avatar";
|
||||
export * from "./StrategyBadge";
|
||||
@@ -25,10 +25,7 @@ export const MonitorStatBoxes = ({
|
||||
}
|
||||
|
||||
const timeOfLastFailure = monitorStats?.timeOfLastFailure || 0;
|
||||
const timeSinceLastFailure =
|
||||
timeOfLastFailure > 0
|
||||
? Date.now() - timeOfLastFailure
|
||||
: Date.now() - new Date(monitorStats?.createdAt || 0).getTime();
|
||||
const timeSinceLastFailure = timeOfLastFailure > 0 ? Date.now() - timeOfLastFailure : 0;
|
||||
|
||||
// Determine time since last check
|
||||
const timeOfLastCheck = monitorStats?.lastCheckTimestamp || 0;
|
||||
@@ -42,6 +39,12 @@ export const MonitorStatBoxes = ({
|
||||
const streakTime = prettyMilliseconds(timeSinceLastFailure, options);
|
||||
|
||||
const lastCheckTime = prettyMilliseconds(timeSinceLastCheck, options);
|
||||
const isActive =
|
||||
monitor?.status === "up" ||
|
||||
monitor?.status === "paused" ||
|
||||
monitor?.status === "maintenance" ||
|
||||
monitor?.status === "initializing" ||
|
||||
monitor?.status === "breached";
|
||||
const palette = getStatusPalette(monitor?.status);
|
||||
|
||||
return (
|
||||
@@ -51,8 +54,12 @@ export const MonitorStatBoxes = ({
|
||||
>
|
||||
<StatBox
|
||||
palette={palette}
|
||||
title={t("pages.common.monitors.statBoxes.activeFor")}
|
||||
subtitle={streakTime}
|
||||
title={
|
||||
isActive
|
||||
? t("pages.common.monitors.statBoxes.activeFor")
|
||||
: t("pages.common.monitors.statBoxes.serviceIsDown")
|
||||
}
|
||||
subtitle={isActive ? streakTime : ""}
|
||||
/>
|
||||
<StatBox
|
||||
title={t("pages.common.monitors.statBoxes.lastCheck")}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useMemo } from "react";
|
||||
import { monitorSchema, type MonitorFormData } from "@/Validation/monitor";
|
||||
import { DefaultPageSpeedStrategy } from "@/Types/Monitor";
|
||||
import type { Monitor, MonitorType } from "@/Types/Monitor";
|
||||
|
||||
interface UseMonitorFormOptions {
|
||||
@@ -89,7 +88,6 @@ export const useMonitorForm = ({
|
||||
...base,
|
||||
type: "pagespeed",
|
||||
url: data?.url || "",
|
||||
strategy: data?.strategy ?? DefaultPageSpeedStrategy,
|
||||
};
|
||||
break;
|
||||
case "hardware":
|
||||
|
||||
@@ -60,6 +60,24 @@ function buildDefaults(data: Notification | null): NotificationFormData {
|
||||
address: data.address || "",
|
||||
};
|
||||
}
|
||||
if (data?.type === "twilio") {
|
||||
return {
|
||||
type: "twilio",
|
||||
notificationName: data.notificationName || "",
|
||||
accountSid: data.accountSid || "",
|
||||
accessToken: data.accessToken || "",
|
||||
phone: data.phone || "",
|
||||
twilioPhoneNumber: data.twilioPhoneNumber || "",
|
||||
};
|
||||
}
|
||||
if (data?.type === "pushover") {
|
||||
return {
|
||||
type: "pushover",
|
||||
notificationName: data.notificationName || "",
|
||||
address: data.address || "",
|
||||
accessToken: data.accessToken || "",
|
||||
};
|
||||
}
|
||||
// Default: email (covers both data === null and data.type === "email")
|
||||
return {
|
||||
type: "email",
|
||||
|
||||
@@ -43,6 +43,7 @@ export const AddTeamMemberDialog = ({
|
||||
const roleOptions: { value: UserRole; label: string }[] = [
|
||||
{ value: "admin", label: t("common.auth.roles.admin") },
|
||||
{ value: "user", label: t("common.auth.roles.user") },
|
||||
{ value: "demo", label: t("common.auth.roles.demo") },
|
||||
];
|
||||
|
||||
const handleClose = () => {
|
||||
|
||||
@@ -48,6 +48,7 @@ export const InviteTeamMemberDialog = ({
|
||||
const roleOptions: { value: UserRole; label: string }[] = [
|
||||
{ value: "admin", label: t("common.auth.roles.admin") },
|
||||
{ value: "user", label: t("common.auth.roles.user") },
|
||||
{ value: "demo", label: t("common.auth.roles.demo") },
|
||||
];
|
||||
|
||||
const handleGenerateToken = async (data: InviteFormData) => {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { BaseAuthPage, TextLink } from "@/Components/design-elements";
|
||||
import { Button, TextField } from "@/Components/inputs";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js";
|
||||
@@ -9,7 +10,7 @@ import type { LoginFormData } from "@/Validation/login";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { setAuthState } from "@/Features/Auth/authSlice";
|
||||
import { usePost } from "@/Hooks/UseApi";
|
||||
import { useLazyGet, usePost } from "@/Hooks/UseApi";
|
||||
|
||||
const LoginPage = () => {
|
||||
const { t } = useTranslation();
|
||||
@@ -17,6 +18,17 @@ const LoginPage = () => {
|
||||
const navigate = useNavigate();
|
||||
const { post, loading } = usePost();
|
||||
|
||||
const [isCheckingAdmin, setIsCheckingAdmin] = useState(true);
|
||||
const { get } = useLazyGet<boolean>();
|
||||
|
||||
useEffect(() => {
|
||||
get("/auth/users/superadmin").then((res) => {
|
||||
if (res?.data === false) navigate("/register", { replace: true });
|
||||
else setIsCheckingAdmin(false);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const { schema, defaults } = useLoginForm();
|
||||
|
||||
const { control, handleSubmit } = useForm<LoginFormData>({
|
||||
@@ -24,6 +36,8 @@ const LoginPage = () => {
|
||||
defaultValues: defaults,
|
||||
});
|
||||
|
||||
if (isCheckingAdmin) return null;
|
||||
|
||||
const onSubmit = async (data: LoginFormData) => {
|
||||
if (loading) return;
|
||||
|
||||
@@ -82,12 +96,6 @@ const LoginPage = () => {
|
||||
linkText={t("pages.auth.login.links.forgotPassword.linkText")}
|
||||
href="/forgot-password"
|
||||
/>
|
||||
<TextLink
|
||||
alignSelf={"center"}
|
||||
text={t("pages.auth.login.links.register.text")}
|
||||
linkText={t("pages.auth.login.links.register.linkText")}
|
||||
href="/register"
|
||||
/>
|
||||
</BaseAuthPage>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
import { BaseAuthPage } from "@/Components/design-elements";
|
||||
import { Button, TextField } from "@/Components/inputs";
|
||||
import Alert from "@mui/material/Alert";
|
||||
import { useForm, Controller } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod/dist/zod.js";
|
||||
import { useRegisterForm } from "@/Hooks/useRegisterForm";
|
||||
import type { RegisterFormData } from "@/Validation/register";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { usePost, useGet } from "@/Hooks/UseApi";
|
||||
import { useLazyGet, usePost } from "@/Hooks/UseApi";
|
||||
import { setAuthState } from "@/Features/Auth/authSlice";
|
||||
import { useDispatch } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
import type { AuthResponse } from "@/Types/User";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
||||
interface RegisterPayload {
|
||||
user: Omit<RegisterFormData, "confirm">;
|
||||
@@ -32,21 +33,23 @@ const RegisterPage = () => {
|
||||
const { post: verifyToken } = usePost<{ token: string }, InviteVerifyResponse>();
|
||||
const hasVerified = useRef(false);
|
||||
|
||||
const { data: superAdminExists, isLoading: isCheckingAdmin } = useGet<boolean>(
|
||||
token ? null : "/auth/users/superadmin"
|
||||
);
|
||||
const [isCheckingAdmin, setIsCheckingAdmin] = useState(!token);
|
||||
const { get } = useLazyGet<boolean>();
|
||||
|
||||
useEffect(() => {
|
||||
if (token) return;
|
||||
get("/auth/users/superadmin").then((res) => {
|
||||
if (res?.data === true) navigate("/login", { replace: true });
|
||||
else setIsCheckingAdmin(false);
|
||||
});
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
const { control, handleSubmit, setError, reset } = useForm<RegisterFormData>({
|
||||
resolver: zodResolver(schema),
|
||||
defaultValues: defaults,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (superAdminExists === true) {
|
||||
navigate("/login", { replace: true });
|
||||
}
|
||||
}, [superAdminExists, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!token || hasVerified.current) return;
|
||||
hasVerified.current = true;
|
||||
@@ -92,6 +95,7 @@ const RegisterPage = () => {
|
||||
title={t("pages.auth.register.title")}
|
||||
subtitle={t("pages.auth.register.subtitle")}
|
||||
>
|
||||
{!token && <Alert severity="info">{t("pages.auth.register.setupNotice")}</Alert>}
|
||||
<Controller
|
||||
name="firstName"
|
||||
control={control}
|
||||
|
||||
@@ -37,7 +37,6 @@ import {
|
||||
type MonitorType,
|
||||
type GamesMap,
|
||||
supportsGeoCheck,
|
||||
DefaultPageSpeedStrategy,
|
||||
} from "@/Types/Monitor";
|
||||
import type { Notification } from "@/Types/Notification";
|
||||
import type { MonitorFormData } from "@/Validation/monitor";
|
||||
@@ -52,7 +51,6 @@ interface GeneralSettingsConfig {
|
||||
showSecret: boolean;
|
||||
showGrpcServiceName: boolean;
|
||||
showIgnoreTls: boolean;
|
||||
showStrategy: boolean;
|
||||
}
|
||||
|
||||
const getGeneralSettingsConfig = (
|
||||
@@ -70,7 +68,6 @@ const getGeneralSettingsConfig = (
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
showStrategy: false,
|
||||
},
|
||||
ping: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.host.label"),
|
||||
@@ -82,7 +79,6 @@ const getGeneralSettingsConfig = (
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
showStrategy: false,
|
||||
},
|
||||
docker: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.container.label"),
|
||||
@@ -94,7 +90,6 @@ const getGeneralSettingsConfig = (
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
showStrategy: false,
|
||||
},
|
||||
port: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||
@@ -106,7 +101,6 @@ const getGeneralSettingsConfig = (
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
showStrategy: false,
|
||||
},
|
||||
game: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||
@@ -118,7 +112,6 @@ const getGeneralSettingsConfig = (
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
showStrategy: false,
|
||||
},
|
||||
grpc: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.host.label"),
|
||||
@@ -130,7 +123,6 @@ const getGeneralSettingsConfig = (
|
||||
showSecret: false,
|
||||
showGrpcServiceName: true,
|
||||
showIgnoreTls: true,
|
||||
showStrategy: false,
|
||||
},
|
||||
pagespeed: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||
@@ -142,7 +134,6 @@ const getGeneralSettingsConfig = (
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
showStrategy: true,
|
||||
},
|
||||
hardware: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.url.label"),
|
||||
@@ -154,7 +145,6 @@ const getGeneralSettingsConfig = (
|
||||
showSecret: true,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: false,
|
||||
showStrategy: false,
|
||||
},
|
||||
websocket: {
|
||||
urlLabel: t("pages.createMonitor.form.general.option.wsUrl.label"),
|
||||
@@ -166,7 +156,6 @@ const getGeneralSettingsConfig = (
|
||||
showSecret: false,
|
||||
showGrpcServiceName: false,
|
||||
showIgnoreTls: true,
|
||||
showStrategy: false,
|
||||
},
|
||||
};
|
||||
return configs[type] || configs.http;
|
||||
@@ -393,31 +382,6 @@ const CreateMonitorPage = () => {
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Strategy field - only for pagespeed type */}
|
||||
{generalSettingsConfig.showStrategy && (
|
||||
<Controller
|
||||
name="strategy"
|
||||
control={control}
|
||||
render={({ field, fieldState }) => (
|
||||
<Select
|
||||
{...field}
|
||||
value={field.value ?? DefaultPageSpeedStrategy}
|
||||
fieldLabel={t(
|
||||
"pages.createMonitor.form.general.option.strategy.label"
|
||||
)}
|
||||
error={!!fieldState.error}
|
||||
>
|
||||
<MenuItem value="desktop">
|
||||
{t("pages.createMonitor.form.general.option.strategy.desktop")}
|
||||
</MenuItem>
|
||||
<MenuItem value="mobile">
|
||||
{t("pages.createMonitor.form.general.option.strategy.mobile")}
|
||||
</MenuItem>
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Port field - only for port and game types */}
|
||||
{generalSettingsConfig.showPort && (
|
||||
<Controller
|
||||
|
||||
@@ -147,30 +147,33 @@ const NotificationsCreatePage = () => {
|
||||
/>
|
||||
}
|
||||
/>
|
||||
{watchedType !== "matrix" && watchedType !== "telegram" && (
|
||||
<ConfigBox
|
||||
title={addressConfig.title}
|
||||
subtitle={addressConfig.description}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="address"
|
||||
control={control}
|
||||
defaultValue={"address" in defaults ? defaults.address : ""}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="text"
|
||||
fieldLabel={addressConfig.fieldLabel}
|
||||
placeholder={addressConfig.placeholder}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{watchedType !== "matrix" &&
|
||||
watchedType !== "telegram" &&
|
||||
watchedType !== "pushover" &&
|
||||
watchedType !== "twilio" && (
|
||||
<ConfigBox
|
||||
title={addressConfig.title}
|
||||
subtitle={addressConfig.description}
|
||||
rightContent={
|
||||
<Controller
|
||||
name="address"
|
||||
control={control}
|
||||
defaultValue={"address" in defaults ? defaults.address : ""}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="text"
|
||||
fieldLabel={addressConfig.fieldLabel}
|
||||
placeholder={addressConfig.placeholder}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{watchedType === "telegram" && (
|
||||
<ConfigBox
|
||||
title={t("pages.notifications.form.telegram.title")}
|
||||
@@ -214,6 +217,134 @@ const NotificationsCreatePage = () => {
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{watchedType === "pushover" && (
|
||||
<ConfigBox
|
||||
title={t("pages.notifications.form.pushover.title")}
|
||||
subtitle={t("pages.notifications.form.pushover.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.pushover.optionAppToken")}
|
||||
placeholder={t(
|
||||
"pages.notifications.form.pushover.placeholderAppToken"
|
||||
)}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="address"
|
||||
control={control}
|
||||
defaultValue={"address" in defaults ? defaults.address : ""}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="text"
|
||||
fieldLabel={t("pages.notifications.form.pushover.optionUserKey")}
|
||||
placeholder={t(
|
||||
"pages.notifications.form.pushover.placeholderUserKey"
|
||||
)}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{watchedType === "twilio" && (
|
||||
<ConfigBox
|
||||
title={t("pages.notifications.form.twilio.title")}
|
||||
subtitle={t("pages.notifications.form.twilio.description")}
|
||||
rightContent={
|
||||
<Stack spacing={theme.spacing(8)}>
|
||||
<Controller
|
||||
name="accountSid"
|
||||
control={control}
|
||||
defaultValue={"accountSid" in defaults ? defaults.accountSid : ""}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="text"
|
||||
fieldLabel={t("pages.notifications.form.twilio.optionAccountSid")}
|
||||
placeholder={t(
|
||||
"pages.notifications.form.twilio.placeholderAccountSid"
|
||||
)}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="accessToken"
|
||||
control={control}
|
||||
defaultValue={"accessToken" in defaults ? defaults.accessToken : ""}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="text"
|
||||
fieldLabel={t("pages.notifications.form.twilio.optionAuthToken")}
|
||||
placeholder={t(
|
||||
"pages.notifications.form.twilio.placeholderAuthToken"
|
||||
)}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="twilioPhoneNumber"
|
||||
control={control}
|
||||
defaultValue={
|
||||
"twilioPhoneNumber" in defaults ? defaults.twilioPhoneNumber : ""
|
||||
}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="text"
|
||||
fieldLabel={t("pages.notifications.form.twilio.optionFromNumber")}
|
||||
placeholder={t(
|
||||
"pages.notifications.form.twilio.placeholderFromNumber"
|
||||
)}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Controller
|
||||
name="phone"
|
||||
control={control}
|
||||
defaultValue={"phone" in defaults ? defaults.phone : ""}
|
||||
render={({ field, fieldState }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
type="text"
|
||||
fieldLabel={t("pages.notifications.form.twilio.optionToNumber")}
|
||||
placeholder={t("pages.notifications.form.twilio.placeholderToNumber")}
|
||||
fullWidth
|
||||
error={!!fieldState.error}
|
||||
helperText={fieldState.error?.message ?? ""}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Stack>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{watchedType === "matrix" && (
|
||||
<ConfigBox
|
||||
title={t("pages.notifications.form.matrix.title")}
|
||||
|
||||
@@ -1,13 +1,7 @@
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Box from "@mui/material/Box";
|
||||
import Typography from "@mui/material/Typography";
|
||||
import {
|
||||
Table,
|
||||
Pagination,
|
||||
StatusLabel,
|
||||
StrategyBadge,
|
||||
} from "@/Components/design-elements";
|
||||
import { LAYOUT } from "@/Utils/Theme/constants";
|
||||
import { Table, Pagination, StatusLabel } from "@/Components/design-elements";
|
||||
import { HistogramPageSpeed } from "@/Components/monitors";
|
||||
import { ArrowDown, ArrowUp } from "lucide-react";
|
||||
import type { Header } from "@/Components/design-elements/Table";
|
||||
@@ -185,19 +179,7 @@ export const PageSpeedMonitorsTable = ({
|
||||
</Stack>
|
||||
),
|
||||
render: (row) => {
|
||||
return (
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
gap={LAYOUT.XS}
|
||||
>
|
||||
<Typography>{row.name}</Typography>
|
||||
<StrategyBadge
|
||||
strategy={row.strategy}
|
||||
variant="caption"
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
return row.name;
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,35 +1,115 @@
|
||||
import { AlertTriangle, CircleCheck } from "lucide-react";
|
||||
import {
|
||||
AlertTriangle,
|
||||
CircleCheck,
|
||||
CircleX,
|
||||
Loader,
|
||||
PauseCircle,
|
||||
ShieldAlert,
|
||||
Wrench,
|
||||
} from "lucide-react";
|
||||
import Stack from "@mui/material/Stack";
|
||||
import Typography from "@mui/material/Typography";
|
||||
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { Theme } from "@mui/material";
|
||||
import { useTheme } from "@mui/material";
|
||||
import type { Monitor } from "@/Types/Monitor";
|
||||
import type { Theme } from "@mui/material";
|
||||
import type { Monitor, MonitorStatus } from "@/Types/Monitor";
|
||||
|
||||
interface StatusDisplay {
|
||||
icon: JSX.Element;
|
||||
msg?: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const getMonitorStatus = (monitors: Monitor[], theme: Theme, t: Function) => {
|
||||
const monitorsStatus: Record<string, any> = {
|
||||
const monitorsStatus: StatusDisplay = {
|
||||
icon: <AlertTriangle size={24} />,
|
||||
};
|
||||
|
||||
if (monitors.every((monitor) => monitor.status === "up")) {
|
||||
// Handle empty monitors array
|
||||
if (monitors.length === 0) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.noMonitors");
|
||||
monitorsStatus.color = theme.palette.warning.main;
|
||||
monitorsStatus.icon = <CircleX size={24} />;
|
||||
return monitorsStatus;
|
||||
}
|
||||
|
||||
const allOf = (...statuses: MonitorStatus[]) =>
|
||||
monitors.every((m) => statuses.includes(m.status));
|
||||
const someOf = (...statuses: MonitorStatus[]) =>
|
||||
monitors.some((m) => statuses.includes(m.status));
|
||||
const noneOf = (...statuses: MonitorStatus[]) =>
|
||||
monitors.every((m) => !statuses.includes(m.status));
|
||||
|
||||
// All monitors in a single state
|
||||
if (allOf("up")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.allUp");
|
||||
monitorsStatus.color = theme.palette.success.main;
|
||||
monitorsStatus.icon = <CircleCheck size={24} />;
|
||||
return monitorsStatus;
|
||||
} else if (monitors.every((monitor) => monitor.status === "down")) {
|
||||
} else if (allOf("breached")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.allBreached");
|
||||
monitorsStatus.color = theme.palette.error.main;
|
||||
monitorsStatus.icon = <ShieldAlert size={24} />;
|
||||
} else if (allOf("maintenance")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.allMaintenance");
|
||||
monitorsStatus.color = theme.palette.warning.main;
|
||||
monitorsStatus.icon = <Wrench size={24} />;
|
||||
} else if (allOf("down")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.allDown");
|
||||
monitorsStatus.color = theme.palette.error.main;
|
||||
return monitorsStatus;
|
||||
} else if (monitors.some((monitor) => monitor.status === "down")) {
|
||||
monitorsStatus.icon = <CircleX size={24} />;
|
||||
} else if (allOf("paused")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.allPaused");
|
||||
monitorsStatus.color = theme.palette.warning.main;
|
||||
monitorsStatus.icon = <PauseCircle size={24} />;
|
||||
} else if (allOf("initializing")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.allInitializing");
|
||||
monitorsStatus.color = theme.palette.warning.main;
|
||||
monitorsStatus.icon = <Loader size={24} />;
|
||||
|
||||
// Breached takes highest priority in mixed states
|
||||
} else if (someOf("breached") && someOf("down")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.breachedAndDown");
|
||||
monitorsStatus.color = theme.palette.error.main;
|
||||
monitorsStatus.icon = <ShieldAlert size={24} />;
|
||||
} else if (someOf("breached")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.breached");
|
||||
monitorsStatus.color = theme.palette.error.main;
|
||||
monitorsStatus.icon = <ShieldAlert size={24} />;
|
||||
|
||||
// Maintenance combinations
|
||||
} else if (someOf("maintenance") && someOf("down")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.maintenanceAndDown");
|
||||
monitorsStatus.color = theme.palette.error.main;
|
||||
monitorsStatus.icon = <Wrench size={24} />;
|
||||
} else if (someOf("maintenance") && noneOf("down")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.maintenance");
|
||||
monitorsStatus.color = theme.palette.warning.main;
|
||||
monitorsStatus.icon = <Wrench size={24} />;
|
||||
|
||||
// Degraded (some down, no maintenance/breached)
|
||||
} else if (someOf("down")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.degraded");
|
||||
monitorsStatus.color = theme.palette.warning.main;
|
||||
return monitorsStatus;
|
||||
monitorsStatus.icon = <AlertTriangle size={24} />;
|
||||
|
||||
// Some Paused
|
||||
} else if (someOf("paused")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.partiallyPaused");
|
||||
monitorsStatus.color = theme.palette.warning.main;
|
||||
monitorsStatus.icon = <PauseCircle size={24} />;
|
||||
|
||||
// Initializing
|
||||
} else if (someOf("initializing")) {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.initializing");
|
||||
monitorsStatus.color = theme.palette.info.main;
|
||||
monitorsStatus.icon = <Loader size={24} />;
|
||||
} else {
|
||||
monitorsStatus.msg = t("pages.statusPages.statusBar.unknown");
|
||||
monitorsStatus.color = theme.palette.warning.main;
|
||||
return monitorsStatus;
|
||||
}
|
||||
|
||||
return monitorsStatus;
|
||||
};
|
||||
|
||||
interface StatusBarProps {
|
||||
|
||||
@@ -38,10 +38,6 @@ export type MonitorStatus = (typeof MonitorStatuses)[number];
|
||||
|
||||
export type MonitorMatchMethod = "equal" | "include" | "regex" | "";
|
||||
|
||||
export const PageSpeedStrategies = ["desktop", "mobile"] as const;
|
||||
export type PageSpeedStrategy = (typeof PageSpeedStrategies)[number];
|
||||
export const DefaultPageSpeedStrategy: PageSpeedStrategy = "desktop";
|
||||
|
||||
export interface Monitor {
|
||||
id: string;
|
||||
userId: string;
|
||||
@@ -76,7 +72,6 @@ export interface Monitor {
|
||||
selectedDisks: string[];
|
||||
gameId?: string;
|
||||
grpcServiceName?: string;
|
||||
strategy?: PageSpeedStrategy;
|
||||
group: string | null;
|
||||
geoCheckEnabled?: boolean;
|
||||
geoCheckLocations?: GeoContinent[];
|
||||
|
||||
@@ -7,6 +7,8 @@ export const NotificationChannels = [
|
||||
"matrix",
|
||||
"teams",
|
||||
"telegram",
|
||||
"pushover",
|
||||
"twilio",
|
||||
] as const;
|
||||
export type NotificationChannel = (typeof NotificationChannels)[number];
|
||||
|
||||
@@ -21,6 +23,8 @@ export interface Notification {
|
||||
homeserverUrl?: string;
|
||||
roomId?: string;
|
||||
accessToken?: string;
|
||||
accountSid?: string;
|
||||
twilioPhoneNumber?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { z } from "zod";
|
||||
import { GeoContinents } from "@/Types/GeoCheck";
|
||||
import { PageSpeedStrategies } from "@/Types/Monitor";
|
||||
|
||||
// URL schema with custom error message
|
||||
const urlSchema = z.url({ message: "Please enter a valid URL" });
|
||||
@@ -90,7 +89,6 @@ const grpcSchema = baseSchema.extend({
|
||||
const pagespeedSchema = baseSchema.extend({
|
||||
type: z.literal("pagespeed"),
|
||||
url: urlSchema,
|
||||
strategy: z.enum(PageSpeedStrategies),
|
||||
});
|
||||
|
||||
// Hardware/Infrastructure monitor schema
|
||||
|
||||
@@ -56,6 +56,20 @@ const telegramSchema = baseSchema.extend({
|
||||
accessToken: z.string().min(1, "Bot token is required"),
|
||||
});
|
||||
|
||||
const pushoverSchema = baseSchema.extend({
|
||||
type: z.literal("pushover"),
|
||||
address: z.string().min(1, "User key is required"),
|
||||
accessToken: z.string().min(1, "App token is required"),
|
||||
});
|
||||
|
||||
const twilioSchema = baseSchema.extend({
|
||||
type: z.literal("twilio"),
|
||||
accountSid: z.string().min(1, "Account SID is required"),
|
||||
accessToken: z.string().min(1, "Auth token is required"),
|
||||
phone: z.string().min(1, "Recipient phone number is required"),
|
||||
twilioPhoneNumber: z.string().min(1, "Twilio phone number is required"),
|
||||
});
|
||||
|
||||
export const notificationSchema = z.discriminatedUnion("type", [
|
||||
emailSchema,
|
||||
slackSchema,
|
||||
@@ -65,6 +79,8 @@ export const notificationSchema = z.discriminatedUnion("type", [
|
||||
matrixSchema,
|
||||
teamsSchema,
|
||||
telegramSchema,
|
||||
pushoverSchema,
|
||||
twilioSchema,
|
||||
]);
|
||||
|
||||
export type NotificationFormData = z.infer<typeof notificationSchema>;
|
||||
+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
+35
-11
@@ -343,6 +343,7 @@
|
||||
"register": {
|
||||
"title": "Welcome back to Checkmate!",
|
||||
"subtitle": "Sign up to get started",
|
||||
"setupNotice": "This page is only available during initial setup. Once a super admin is registered, additional users can be invited from team settings.",
|
||||
"submit": "Register"
|
||||
},
|
||||
"forgotPassword": {
|
||||
@@ -397,7 +398,8 @@
|
||||
"activeFor": "Active for",
|
||||
"certificateExpiry": "Certificate expiry",
|
||||
"lastCheck": "Last check",
|
||||
"lastResponseTime": "Last response time"
|
||||
"lastResponseTime": "Last response time",
|
||||
"serviceIsDown": "Service is down"
|
||||
},
|
||||
"status": {
|
||||
"down": "down",
|
||||
@@ -503,11 +505,6 @@
|
||||
"wsUrl": {
|
||||
"label": "WebSocket URL",
|
||||
"placeholder": "wss://example.com/socket"
|
||||
},
|
||||
"strategy": {
|
||||
"label": "Strategy",
|
||||
"desktop": "Desktop",
|
||||
"mobile": "Mobile"
|
||||
}
|
||||
},
|
||||
"title": "General settings",
|
||||
@@ -964,6 +961,26 @@
|
||||
"placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ",
|
||||
"optionChatId": "Chat ID",
|
||||
"placeholderChatId": "-1001234567890"
|
||||
},
|
||||
"pushover": {
|
||||
"title": "Pushover configuration",
|
||||
"description": "Configure Pushover to receive push notifications on your devices.",
|
||||
"optionAppToken": "Application API token",
|
||||
"placeholderAppToken": "azGDORePK8gMaC0QOYAMyEEuzJnyUi",
|
||||
"optionUserKey": "User key",
|
||||
"placeholderUserKey": "uQiRzpo4DXghDmr9QzzfQu27cmVRsG"
|
||||
},
|
||||
"twilio": {
|
||||
"title": "Twilio SMS configuration",
|
||||
"description": "Configure Twilio to send SMS notifications to a phone number.",
|
||||
"optionAccountSid": "Account SID",
|
||||
"placeholderAccountSid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||
"optionAuthToken": "Auth token",
|
||||
"placeholderAuthToken": "your_auth_token",
|
||||
"optionFromNumber": "From number (Twilio)",
|
||||
"placeholderFromNumber": "+15551234567",
|
||||
"optionToNumber": "To number (recipient)",
|
||||
"placeholderToNumber": "+15559876543"
|
||||
}
|
||||
},
|
||||
"table": {
|
||||
@@ -973,10 +990,6 @@
|
||||
}
|
||||
},
|
||||
"pageSpeed": {
|
||||
"strategy": {
|
||||
"desktop": "Desktop",
|
||||
"mobile": "Mobile"
|
||||
},
|
||||
"charts": {
|
||||
"common": {
|
||||
"cls": "Cumulative Layout Shift (CLS)",
|
||||
@@ -1217,9 +1230,20 @@
|
||||
}
|
||||
},
|
||||
"statusBar": {
|
||||
"allDown": "All systems are down",
|
||||
"allUp": "All systems operational",
|
||||
"allBreached": "All systems have exceeded thresholds",
|
||||
"allMaintenance": "All systems are under maintenance",
|
||||
"allDown": "All systems are down",
|
||||
"allPaused": "All systems are paused",
|
||||
"allInitializing": "All systems are initializing",
|
||||
"breachedAndDown": "Some systems have exceeded thresholds and are down",
|
||||
"breached": "Some systems have exceeded thresholds",
|
||||
"degraded": "Some systems are experiencing issues",
|
||||
"maintenanceAndDown": "Some systems are down during maintenance",
|
||||
"maintenance": "Some systems are under maintenance",
|
||||
"partiallyPaused": "Some systems are paused",
|
||||
"initializing": "Some systems are initializing",
|
||||
"noMonitors": "No monitors have been configured",
|
||||
"unknown": "Unable to determine system status"
|
||||
},
|
||||
"table": {
|
||||
|
||||
+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
+10
-1
@@ -7,13 +7,22 @@ const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
export default defineConfig(({}) => {
|
||||
let version = "3.5.1";
|
||||
let version = "3.6.1";
|
||||
|
||||
const allowedHosts = process.env.VITE_ALLOWED_HOSTS
|
||||
? process.env.VITE_ALLOWED_HOSTS.split(",").map((h) => h.trim())
|
||||
: true;
|
||||
|
||||
return {
|
||||
base: "/",
|
||||
plugins: [svgr(), react()],
|
||||
server: {
|
||||
host: true,
|
||||
allowedHosts,
|
||||
},
|
||||
preview: {
|
||||
host: true,
|
||||
allowedHosts,
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM Change directory to root Server directory for correct Docker Context
|
||||
cd /d "%~dp0"
|
||||
cd ../..
|
||||
|
||||
REM Define service names and their corresponding Dockerfiles
|
||||
set services[0]=bluewaveuptime/uptime_client
|
||||
set dockerfiles[0]=.\docker\coolify\client.Dockerfile
|
||||
|
||||
set services[1]=bluewaveuptime/uptime_database_mongo
|
||||
set dockerfiles[1]=.\docker\coolify\mongoDB.Dockerfile
|
||||
|
||||
set services[2]=bluewaveuptime/uptime_redis
|
||||
set dockerfiles[2]=.\docker\coolify\redis.Dockerfile
|
||||
|
||||
set services[3]=bluewaveuptime/uptime_server
|
||||
set dockerfiles[3]=.\docker\coolify\server.Dockerfile
|
||||
|
||||
REM Loop through each service and build the corresponding image
|
||||
for /L %%i in (0,1,2,3) do (
|
||||
set service=!services[%%i]!
|
||||
set dockerfile=!dockerfiles[%%i]!
|
||||
|
||||
docker build -f "!dockerfile!" -t "!service!" .
|
||||
|
||||
REM Check if the build succeeded
|
||||
if errorlevel 1 (
|
||||
echo Error building !service! image. Exiting...
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo All images built successfully
|
||||
exit /b 0
|
||||
@@ -1,14 +1,14 @@
|
||||
#!/bin/bash
|
||||
# Change directory to root Server directory for correct Docker Context
|
||||
cd "$(dirname "$0")"
|
||||
cd ../../../
|
||||
cd ../../
|
||||
|
||||
# Define an array of services and their Dockerfiles
|
||||
declare -A services=(
|
||||
["bluewaveuptime/uptime_client"]="./server/docker/dist/client.Dockerfile"
|
||||
["bluewaveuptime/uptime_database_mongo"]="./server/docker/dist/mongoDB.Dockerfile"
|
||||
["bluewaveuptime/uptime_redis"]="./server/docker/dist/redis.Dockerfile"
|
||||
["bluewaveuptime/uptime_server"]="./server/docker/dist/server.Dockerfile"
|
||||
["bluewaveuptime/uptime_client"]="./docker/coolify/client.Dockerfile"
|
||||
["bluewaveuptime/uptime_database_mongo"]="./docker/coolify/mongoDB.Dockerfile"
|
||||
["bluewaveuptime/uptime_redis"]="./docker/coolify/redis.Dockerfile"
|
||||
["bluewaveuptime/uptime_server"]="./docker/coolify/server.Dockerfile"
|
||||
)
|
||||
|
||||
# Loop through each service and build the corresponding image
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM Change directory to root Server directory for correct Docker Context
|
||||
cd /d "%~dp0"
|
||||
cd ../..
|
||||
|
||||
REM Define service names and their corresponding Dockerfiles
|
||||
set services[0]=uptime_client
|
||||
set dockerfiles[0]=.\docker\dev\client.Dockerfile
|
||||
|
||||
set services[1]=uptime_mongo
|
||||
set dockerfiles[1]=.\docker\dev\mongoDB.Dockerfile
|
||||
|
||||
set services[2]=uptime_server
|
||||
set dockerfiles[2]=.\docker\dev\server.Dockerfile
|
||||
|
||||
REM Loop through each service and build the corresponding image
|
||||
for /L %%i in (0,1,2) do (
|
||||
set service=!services[%%i]!
|
||||
set dockerfile=!dockerfiles[%%i]!
|
||||
|
||||
docker build -f "!dockerfile!" -t "!service!" .
|
||||
|
||||
REM Check if the build succeeded
|
||||
if errorlevel 1 (
|
||||
echo Error building !service! image. Exiting...
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo All images built successfully
|
||||
exit /b 0
|
||||
@@ -1,17 +1,16 @@
|
||||
# ---------------------
|
||||
# Frontend build stage
|
||||
# ---------------------
|
||||
FROM node:20-slim AS frontend-build
|
||||
|
||||
|
||||
# Build on the host arch (BUILDPLATFORM) rather than under QEMU — the output is
|
||||
# pure JS so it's arch-independent, and this avoids the esbuild host/native
|
||||
# binary version mismatch under emulation.
|
||||
FROM --platform=$BUILDPLATFORM node:20-slim AS frontend-build
|
||||
|
||||
WORKDIR /app/client
|
||||
|
||||
COPY client/package.json ./
|
||||
COPY client/package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
RUN npm install esbuild@0.25.5 --build-from-source
|
||||
RUN npm ci
|
||||
|
||||
COPY client ./
|
||||
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM Change directory to root Server directory for correct Docker Context
|
||||
cd /d "%~dp0"
|
||||
cd ../..
|
||||
|
||||
REM Define service names and their corresponding Dockerfiles
|
||||
set services[0]=uptime_database_mongo
|
||||
set dockerfiles[0]=.\docker\dist-mono\mongoDB.Dockerfile
|
||||
|
||||
set services[1]=uptime_server
|
||||
set dockerfiles[1]=.\docker\dist-mono\server.Dockerfile
|
||||
|
||||
REM Loop through each service and build the corresponding image
|
||||
for /L %%i in (0,1) do (
|
||||
set service=!services[%%i]!
|
||||
set dockerfile=!dockerfiles[%%i]!
|
||||
|
||||
docker build -f "!dockerfile!" -t "!service!" .
|
||||
|
||||
REM Check if the build succeeded
|
||||
if errorlevel 1 (
|
||||
echo Error building !service! image. Exiting...
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo All images built successfully
|
||||
exit /b 0
|
||||
@@ -19,10 +19,10 @@ WORKDIR /app/server
|
||||
|
||||
COPY server ./
|
||||
|
||||
|
||||
RUN npm install
|
||||
|
||||
RUN npm run build
|
||||
RUN cp -r src/templates dist/templates
|
||||
|
||||
COPY --from=frontend-build /app/client/dist ./public
|
||||
|
||||
@@ -30,4 +30,4 @@ RUN chmod +x ./scripts/inject-vars.sh
|
||||
|
||||
EXPOSE 52345
|
||||
|
||||
CMD ./scripts/inject-vars.sh && node ./dist/index.js
|
||||
CMD ./scripts/inject-vars.sh && node ./dist/index.js
|
||||
Vendored
+33
@@ -0,0 +1,33 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM Change directory to root Server directory for correct Docker Context
|
||||
cd /d "%~dp0"
|
||||
cd ../..
|
||||
|
||||
REM Define service names and their corresponding Dockerfiles
|
||||
set services[0]=bluewaveuptime/uptime_client
|
||||
set dockerfiles[0]=.\docker\dist\client.Dockerfile
|
||||
|
||||
set services[1]=bluewaveuptime/uptime_database_mongo
|
||||
set dockerfiles[1]=.\docker\dist\mongoDB.Dockerfile
|
||||
|
||||
set services[2]=bluewaveuptime/uptime_server
|
||||
set dockerfiles[2]=.\docker\dist\server.Dockerfile
|
||||
|
||||
REM Loop through each service and build the corresponding image
|
||||
for /L %%i in (0,1,2) do (
|
||||
set service=!services[%%i]!
|
||||
set dockerfile=!dockerfiles[%%i]!
|
||||
|
||||
docker build -f "!dockerfile!" -t "!service!" .
|
||||
|
||||
REM Check if the build succeeded
|
||||
if errorlevel 1 (
|
||||
echo Error building !service! image. Exiting...
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo All images built successfully
|
||||
exit /b 0
|
||||
@@ -0,0 +1,33 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM Change directory to root Server directory for correct Docker Context
|
||||
cd /d "%~dp0"
|
||||
cd ../..
|
||||
|
||||
REM Define service names and their corresponding Dockerfiles
|
||||
set services[0]=uptime_client
|
||||
set dockerfiles[0]=.\docker\prod\client.Dockerfile
|
||||
|
||||
set services[1]=uptime_database_mongo
|
||||
set dockerfiles[1]=.\docker\prod\mongoDB.Dockerfile
|
||||
|
||||
set services[2]=uptime_server
|
||||
set dockerfiles[2]=.\docker\prod\server.Dockerfile
|
||||
|
||||
REM Loop through each service and build the corresponding image
|
||||
for /L %%i in (0,1,2) do (
|
||||
set service=!services[%%i]!
|
||||
set dockerfile=!dockerfiles[%%i]!
|
||||
|
||||
docker build -f "!dockerfile!" -t "!service!" .
|
||||
|
||||
REM Check if the build succeeded
|
||||
if errorlevel 1 (
|
||||
echo Error building !service! image. Exiting...
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo All images built successfully
|
||||
exit /b 0
|
||||
@@ -3,8 +3,8 @@ services:
|
||||
image: ghcr.io/bluewave-labs/checkmate:frontend-demo
|
||||
restart: always
|
||||
environment:
|
||||
UPTIME_APP_API_BASE_URL: "https://checkmate-demo.bluewavelabs.ca/api/v1"
|
||||
UPTIME_APP_CLIENT_HOST: "https://checkmate-demo.bluewavelabs.ca"
|
||||
UPTIME_APP_API_BASE_URL: "https://demo.checkmate.so/api/v1"
|
||||
UPTIME_APP_CLIENT_HOST: "https://demo.checkmate.so"
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
@@ -12,16 +12,7 @@ services:
|
||||
- server
|
||||
volumes:
|
||||
- ./nginx/conf.d:/etc/nginx/conf.d/:ro
|
||||
- ./certbot/www:/var/www/certbot/:ro
|
||||
- ./certbot/conf/:/etc/nginx/ssl/:ro
|
||||
|
||||
certbot:
|
||||
image: certbot/certbot:latest
|
||||
restart: always
|
||||
volumes:
|
||||
- ./certbot/www/:/var/www/certbot/:rw
|
||||
- ./certbot/conf/:/etc/letsencrypt/:rw
|
||||
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew --webroot -w /var/www/certbot; sleep 12h & wait $${!}; done;'"
|
||||
- /etc/letsencrypt:/etc/nginx/ssl:ro
|
||||
|
||||
server:
|
||||
image: ghcr.io/bluewave-labs/checkmate:backend-demo
|
||||
|
||||
@@ -40,8 +40,8 @@ server {
|
||||
|
||||
server_name checkmate-demo.bluewavelabs.ca;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/live/checkmate-demo.bluewavelabs.ca/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/live/checkmate-demo.bluewavelabs.ca/privkey.pem;
|
||||
ssl_certificate /etc/nginx/ssl/live/demo.checkmate.so/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/live/demo.checkmate.so/privkey.pem;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
|
||||
@@ -17,6 +17,8 @@ COPY ./server ./
|
||||
|
||||
RUN npm run build
|
||||
|
||||
RUN cp -r src/templates dist/templates
|
||||
|
||||
EXPOSE 52345
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
@@ -0,0 +1,33 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM Change directory to root Server directory for correct Docker Context
|
||||
cd /d "%~dp0"
|
||||
cd ../..
|
||||
|
||||
REM Define service names and their corresponding Dockerfiles
|
||||
set services[0]=uptime_client
|
||||
set dockerfiles[0]=.\docker\staging\client.Dockerfile
|
||||
|
||||
set services[1]=uptime_database_mongo
|
||||
set dockerfiles[1]=.\docker\staging\mongoDB.Dockerfile
|
||||
|
||||
set services[2]=uptime_server
|
||||
set dockerfiles[2]=.\docker\staging\server.Dockerfile
|
||||
|
||||
REM Loop through each service and build the corresponding image
|
||||
for /L %%i in (0,1,2) do (
|
||||
set service=!services[%%i]!
|
||||
set dockerfile=!dockerfiles[%%i]!
|
||||
|
||||
docker build -f "!dockerfile!" -t "!service!" .
|
||||
|
||||
REM Check if the build succeeded
|
||||
if errorlevel 1 (
|
||||
echo Error building !service! image. Exiting...
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo All images built successfully
|
||||
exit /b 0
|
||||
@@ -17,6 +17,8 @@ COPY ./server ./
|
||||
|
||||
RUN npm run build
|
||||
|
||||
RUN cp -r src/templates dist/templates
|
||||
|
||||
EXPOSE 52345
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
@@ -0,0 +1,33 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
REM Change directory to root Server directory for correct Docker Context
|
||||
cd /d "%~dp0"
|
||||
cd ../..
|
||||
|
||||
REM Define service names and their corresponding Dockerfiles
|
||||
set services[0]=uptime_client
|
||||
set dockerfiles[0]=.\docker\unified\client.Dockerfile
|
||||
|
||||
set services[1]=uptime_database_mongo
|
||||
set dockerfiles[1]=.\docker\unified\mongoDB.Dockerfile
|
||||
|
||||
set services[2]=uptime_server
|
||||
set dockerfiles[2]=.\docker\unified\server.Dockerfile
|
||||
|
||||
REM Loop through each service and build the corresponding image
|
||||
for /L %%i in (0,1,2) do (
|
||||
set service=!services[%%i]!
|
||||
set dockerfile=!dockerfiles[%%i]!
|
||||
|
||||
docker build -f "!dockerfile!" -t "!service!" .
|
||||
|
||||
REM Check if the build succeeded
|
||||
if errorlevel 1 (
|
||||
echo Error building !service! image. Exiting...
|
||||
exit /b 1
|
||||
)
|
||||
)
|
||||
|
||||
echo All images built successfully
|
||||
exit /b 0
|
||||
Executable
+25
@@ -0,0 +1,25 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Change directory to root directory for correct Docker Context
|
||||
cd "$(dirname "$0")"
|
||||
cd ../../
|
||||
|
||||
# Define an array of services and their Dockerfiles
|
||||
declare -A services=(
|
||||
["uptime_client"]="./docker/unified/client.Dockerfile"
|
||||
["uptime_database_mongo"]="./docker/unified/mongoDB.Dockerfile"
|
||||
["uptime_server"]="./docker/unified/server.Dockerfile"
|
||||
)
|
||||
|
||||
# Loop through each service and build the corresponding image
|
||||
for service in "${!services[@]}"; do
|
||||
docker build -f "${services[$service]}" -t "$service" .
|
||||
|
||||
# Check if the build succeeded
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error building $service image. Exiting..."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
echo "All images built successfully"
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
version: "3"
|
||||
|
||||
services:
|
||||
webserver:
|
||||
image: nginx:latest
|
||||
ports:
|
||||
- 80:80
|
||||
- 443:443
|
||||
restart: always
|
||||
volumes:
|
||||
- ./nginx/conf.d/:/etc/nginx/conf.d/:ro
|
||||
- ./certbot/www/:/var/www/certbot/:ro
|
||||
certbot:
|
||||
image: certbot/certbot:latest
|
||||
volumes:
|
||||
- ./certbot/www/:/var/www/certbot/:rw
|
||||
- ./certbot/conf/:/etc/letsencrypt/:rw
|
||||
depends_on:
|
||||
- webserver
|
||||
Executable
+20
@@ -0,0 +1,20 @@
|
||||
FROM node:20-slim AS build
|
||||
|
||||
ENV NODE_OPTIONS="--max-old-space-size=4096"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./client/package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY ./client .
|
||||
|
||||
RUN npm run build
|
||||
|
||||
FROM nginx:1.27.1-alpine
|
||||
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
COPY --from=build /app/env.sh /docker-entrypoint.d/env.sh
|
||||
RUN chmod +x /docker-entrypoint.d/env.sh
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
Executable
+114
@@ -0,0 +1,114 @@
|
||||
# sudo certbot certonly --standalone --http-01-port 8080
|
||||
# cerbot needs to run on 8080 since port 80 is used by the gateway
|
||||
|
||||
services:
|
||||
# Gateway (Single Entry Point)
|
||||
gateway:
|
||||
image: nginx:1.27.1-alpine
|
||||
container_name: checkmate-gateway
|
||||
restart: always
|
||||
ports:
|
||||
- "80:80"
|
||||
- "443:443"
|
||||
volumes:
|
||||
- /etc/letsencrypt:/etc/nginx/ssl:ro
|
||||
- ./gateway/nginx.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
|
||||
# Production Stack -----
|
||||
client-prod:
|
||||
image: ghcr.io/bluewave-labs/checkmate:frontend-demo
|
||||
container_name: client-prod
|
||||
restart: always
|
||||
environment:
|
||||
UPTIME_APP_API_BASE_URL: "https://demo.checkmate.so/api/v1"
|
||||
UPTIME_APP_CLIENT_HOST: "https://demo.checkmate.so"
|
||||
expose:
|
||||
- "80"
|
||||
depends_on:
|
||||
- server-prod
|
||||
volumes:
|
||||
- ./nginx/conf.d/prod.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
|
||||
server-prod:
|
||||
image: ghcr.io/bluewave-labs/checkmate:backend-demo
|
||||
container_name: server-prod
|
||||
restart: always
|
||||
env_file:
|
||||
- server-prod.env
|
||||
depends_on:
|
||||
- mongodb-prod
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
|
||||
mongodb-prod:
|
||||
image: ghcr.io/bluewave-labs/checkmate:mongo-demo
|
||||
container_name: mongodb-prod
|
||||
restart: always
|
||||
command: [ "mongod", "--quiet", "--bind_ip_all" ]
|
||||
volumes:
|
||||
- ./mongo/data-prod:/data/db
|
||||
env_file:
|
||||
- mongo-prod.env
|
||||
healthcheck:
|
||||
test: [ "CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet" ]
|
||||
interval: 5s
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
start_interval: 1s
|
||||
retries: 30
|
||||
|
||||
# Staging Stack -----
|
||||
client-staging:
|
||||
image: ghcr.io/bluewave-labs/checkmate:frontend-staging
|
||||
container_name: client-staging
|
||||
restart: always
|
||||
environment:
|
||||
UPTIME_APP_API_BASE_URL: "https://staging.checkmate.so/api/v1"
|
||||
UPTIME_APP_CLIENT_HOST: "https://staging.checkmate.so"
|
||||
expose:
|
||||
- "80"
|
||||
depends_on:
|
||||
- server-staging
|
||||
volumes:
|
||||
- ./nginx/conf.d/staging.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
|
||||
server-staging:
|
||||
image: ghcr.io/bluewave-labs/checkmate:backend-staging
|
||||
container_name: server-staging
|
||||
restart: always
|
||||
env_file:
|
||||
- server-staging.env
|
||||
depends_on:
|
||||
- mongodb-staging
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock:ro
|
||||
|
||||
mongodb-staging:
|
||||
image: ghcr.io/bluewave-labs/checkmate:mongo-staging
|
||||
container_name: mongodb-staging
|
||||
restart: always
|
||||
command: [ "mongod", "--quiet", "--bind_ip_all" ]
|
||||
volumes:
|
||||
- ./mongo/data-staging:/data/db
|
||||
env_file:
|
||||
- mongo-staging.env
|
||||
healthcheck:
|
||||
test: [ "CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet" ]
|
||||
interval: 5s
|
||||
timeout: 30s
|
||||
start_period: 0s
|
||||
start_interval: 1s
|
||||
retries: 30
|
||||
|
||||
# Shared Utils
|
||||
capture:
|
||||
image: ghcr.io/bluewave-labs/capture:latest
|
||||
container_name: capture
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "59232:59232"
|
||||
environment:
|
||||
- API_SECRET=my_secret
|
||||
- GIN_MODE=release
|
||||
volumes:
|
||||
- /etc/os-release:/etc/os-release:ro
|
||||
@@ -0,0 +1,55 @@
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name demo.checkmate.so staging.checkmate.so;
|
||||
|
||||
location /.well-known/acme-challenge/ {
|
||||
# Forward the challenge to Certbot listening on 8080
|
||||
proxy_pass http://172.17.0.1:8080;
|
||||
}
|
||||
|
||||
location / {
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# --- PRODUCTION: demo.checkmate.so ---
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name demo.checkmate.so;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/live/demo.checkmate.so/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/live/demo.checkmate.so/privkey.pem;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
location / {
|
||||
# Passes traffic to the prod client on port 80
|
||||
proxy_pass http://client-prod:80;
|
||||
}
|
||||
}
|
||||
|
||||
# --- STAGING: staging.checkmate.so ---
|
||||
|
||||
server {
|
||||
listen 443 ssl;
|
||||
server_name staging.checkmate.so;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/live/staging.checkmate.so/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/live/staging.checkmate.so/privkey.pem;
|
||||
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
location / {
|
||||
# Passes traffic to the staging client on port 80
|
||||
proxy_pass http://client-staging:80;
|
||||
}
|
||||
}
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
FROM mongo
|
||||
EXPOSE 27017
|
||||
CMD ["mongod"]
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
server_name _;
|
||||
server_tokens off;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://server-prod:52345/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /api-docs/ {
|
||||
proxy_pass http://server-prod:52345/api-docs/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
Executable
+31
@@ -0,0 +1,31 @@
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
|
||||
server_name _;
|
||||
server_tokens off;
|
||||
|
||||
location / {
|
||||
root /usr/share/nginx/html;
|
||||
index index.html index.htm;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://server-staging:52345/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /api-docs/ {
|
||||
proxy_pass http://server-staging:52345/api-docs/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
Executable
+22
@@ -0,0 +1,22 @@
|
||||
FROM node:20-slim
|
||||
|
||||
# Install ping
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y iputils-ping \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
ENV NODE_OPTIONS="--max-old-space-size=2048"
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY ./server/package*.json ./
|
||||
|
||||
RUN npm install
|
||||
|
||||
COPY ./server ./
|
||||
|
||||
RUN npm run build
|
||||
|
||||
EXPOSE 52345
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
@@ -0,0 +1,31 @@
|
||||
# Contributing Translations
|
||||
|
||||
Checkmate supports multiple languages. This guide explains how to contribute translations.
|
||||
|
||||
## How to Add a New Language
|
||||
|
||||
1. Create a new JSON file in `client/src/locales/` named with the language code (e.g., `it.json` for Italian)
|
||||
|
||||
2. Copy the structure from `en.json` and translate all values
|
||||
|
||||
3. Add the language to the language selector in the UI
|
||||
|
||||
## Translation Guidelines
|
||||
|
||||
- en.json is the reference localization file, all keys should be declared here and mirrored in other localization files
|
||||
- Translate only the values
|
||||
- Use formal/informal tone consistently
|
||||
- Test your translations in the UI
|
||||
|
||||
## File Structure
|
||||
client/src/locales/
|
||||
├── en.json # Source language
|
||||
├── es.json # Spanish
|
||||
├── de.json # German
|
||||
└── [lang].json # Your language
|
||||
## Testing Translations
|
||||
|
||||
1. Start the development server: `npm run dev`
|
||||
2. Go to Settings → Language
|
||||
3. Select your language
|
||||
4. Verify all strings display correctly
|
||||
+1
-1
@@ -8,7 +8,7 @@ Checkmate's backend is designed to be lightweight and scalable, ensuring reliabl
|
||||
|
||||
## Installation & documentation
|
||||
|
||||
For setup instructions, configuration details, and deployment guidelines, please visit our official documentation at [docs.checkmate.so](https://docs.checkmate.so).
|
||||
For setup instructions, configuration details, and deployment guidelines, please visit our official documentation at [docs.checkmate.so](https://checkmate.so/docs).
|
||||
|
||||
## Related repositories
|
||||
|
||||
|
||||
@@ -9,8 +9,7 @@ const config: Config = {
|
||||
},
|
||||
moduleNameMapper: {
|
||||
"^@/validation/(.*)\\.js$": "<rootDir>/src/validation/$1.js",
|
||||
"^@/utils/(AppError)\\.js$": "<rootDir>/src/utils/$1.ts",
|
||||
"^@/utils/(.*)\\.js$": "<rootDir>/src/utils/$1.js",
|
||||
"^@/utils/(.*)\\.js$": "<rootDir>/src/utils/$1.ts",
|
||||
"^@/(.*)\\.ts$": "<rootDir>/src/$1.ts",
|
||||
"^@/(.*)\\.js$": "<rootDir>/src/$1.ts",
|
||||
"^@/(.*)$": "<rootDir>/src/$1",
|
||||
|
||||
@@ -3631,12 +3631,6 @@
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"strategy": {
|
||||
"type": "string",
|
||||
"enum": ["desktop", "mobile"],
|
||||
"default": "desktop",
|
||||
"description": "PageSpeed analysis strategy"
|
||||
},
|
||||
"selectedDisks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
@@ -3743,11 +3737,6 @@
|
||||
"grpcServiceName": {
|
||||
"type": "string"
|
||||
},
|
||||
"strategy": {
|
||||
"type": "string",
|
||||
"enum": ["desktop", "mobile"],
|
||||
"description": "PageSpeed analysis strategy"
|
||||
},
|
||||
"selectedDisks": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
|
||||
Generated
+132
-530
@@ -14,7 +14,6 @@
|
||||
"@pulsecron/pulse": "1.6.8",
|
||||
"axios": "^1.7.2",
|
||||
"bcryptjs": "3.0.2",
|
||||
"bullmq": "5.41.2",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"compression": "1.8.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
@@ -22,7 +21,7 @@
|
||||
"dockerode": "4.0.6",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"express-rate-limit": "8.0.1",
|
||||
"express-rate-limit": "8.3.2",
|
||||
"gamedig": "5.2.0",
|
||||
"got": "14.4.7",
|
||||
"handlebars": "^4.7.8",
|
||||
@@ -36,12 +35,11 @@
|
||||
"mjml": "^5.0.0-alpha.4",
|
||||
"mongoose": "^8.3.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "8.0.1",
|
||||
"pg": "8.20.0",
|
||||
"nodemailer": "8.0.5",
|
||||
"ping": "0.4.4",
|
||||
"sharp": "0.33.5",
|
||||
"ssl-checker": "2.0.10",
|
||||
"super-simple-scheduler": "1.4.5",
|
||||
"super-simple-scheduler": "1.9.1",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
"winston": "^3.13.0",
|
||||
"ws": "^8.19.0",
|
||||
@@ -63,13 +61,11 @@
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/nodemailer": "7.0.1",
|
||||
"@types/papaparse": "^5.5.2",
|
||||
"@types/pg": "8.20.0",
|
||||
"@types/ping": "0.4.4",
|
||||
"@types/swagger-ui-express": "4.1.8",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||
"@typescript-eslint/parser": "^8.56.1",
|
||||
"c8": "10.1.3",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-plugin-mocha": "^10.5.0",
|
||||
"esm": "3.2.25",
|
||||
@@ -801,40 +797,20 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@aws-sdk/xml-builder": {
|
||||
"version": "3.972.10",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.10.tgz",
|
||||
"integrity": "sha512-OnejAIVD+CxzyAUrVic7lG+3QRltyja9LoNqCE/1YVs8ichoTbJlVSaZ9iSMcnHLyzrSNtvaOGjSDRP+d/ouFA==",
|
||||
"version": "3.972.16",
|
||||
"resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.16.tgz",
|
||||
"integrity": "sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@smithy/types": "^4.13.0",
|
||||
"fast-xml-parser": "5.4.1",
|
||||
"@smithy/types": "^4.13.1",
|
||||
"fast-xml-parser": "5.5.8",
|
||||
"tslib": "^2.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"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": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.3.tgz",
|
||||
@@ -1378,16 +1354,6 @@
|
||||
"integrity": "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q==",
|
||||
"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": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.6.0.tgz",
|
||||
@@ -3200,84 +3166,6 @@
|
||||
"sparse-bitfield": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz",
|
||||
"integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz",
|
||||
"integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz",
|
||||
"integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
]
|
||||
},
|
||||
"node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz",
|
||||
"integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
]
|
||||
},
|
||||
"node_modules/@napi-rs/wasm-runtime": {
|
||||
"version": "0.2.12",
|
||||
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
|
||||
@@ -3855,9 +3743,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@smithy/types": {
|
||||
"version": "4.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.0.tgz",
|
||||
"integrity": "sha512-COuLsZILbbQsdrwKQpkkpyep7lCsByxwj7m0Mg5v66/ZTyenlfBc40/QFQ5chO0YN/PNEH1Bi3fGtfXPnYNeDw==",
|
||||
"version": "4.13.1",
|
||||
"resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.13.1.tgz",
|
||||
"integrity": "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
@@ -4529,18 +4417,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/pg": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz",
|
||||
"integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*",
|
||||
"pg-protocol": "*",
|
||||
"pg-types": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ping": {
|
||||
"version": "0.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/ping/-/ping-0.4.4.tgz",
|
||||
@@ -4962,9 +4838,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz",
|
||||
"integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==",
|
||||
"version": "5.0.5",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz",
|
||||
"integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -5565,14 +5441,14 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.13.6",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz",
|
||||
"integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==",
|
||||
"version": "1.15.1",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.1.tgz",
|
||||
"integrity": "sha512-WOG+Jj8ZOvR0a3rAn+Tuf1UQJRxw5venr6DgdbJzngJE3qG7X0kL83CZGpdHMxEm+ZK3seAbvFsw4FfOfP9vxg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.11",
|
||||
"form-data": "^4.0.5",
|
||||
"proxy-from-env": "^1.1.0"
|
||||
"proxy-from-env": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
@@ -5905,9 +5781,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
|
||||
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
|
||||
"version": "1.1.13",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.13.tgz",
|
||||
"integrity": "sha512-9ZLprWS6EENmhEOpjCYW2c8VkmOvckIJZfkr7rBW6dObmfgJ/L1GpSYW5Hpo9lDz4D1+n0Ckz8rU7FwHDQiG/w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -6038,21 +5914,6 @@
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bullmq": {
|
||||
"version": "5.41.2",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-5.41.2.tgz",
|
||||
"integrity": "sha512-wqsUIHW2Td86mTKTepqQpKLLUtP4gmX89bUO1YL2fAorxwj3da1GYtroGZMCg/zgB/+zMRsbylL6DHyMUWX7fA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cron-parser": "^4.9.0",
|
||||
"ioredis": "^5.4.1",
|
||||
"msgpackr": "^1.11.2",
|
||||
"node-abort-controller": "^3.1.1",
|
||||
"semver": "^7.5.4",
|
||||
"tslib": "^2.0.0",
|
||||
"uuid": "^9.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
@@ -6073,40 +5934,6 @@
|
||||
"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": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz",
|
||||
@@ -7335,13 +7162,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/dompurify": {
|
||||
"version": "3.3.2",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.3.2.tgz",
|
||||
"integrity": "sha512-6obghkliLdmKa56xdbLOpUZ43pAR6xFy1uOrxBaIDjT+yaRuuybLjGS9eVBoSR/UPU5fq3OXClEHLJNGvbxKpQ==",
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.0.tgz",
|
||||
"integrity": "sha512-nolgK9JcaUXMSmW+j1yaSvaEaoXYHwWyGJlkoCTghc97KgGDDSnpoU/PlEnw63Ah+TGKFOyY+X5LnxaWbCSfXg==",
|
||||
"license": "(MPL-2.0 OR Apache-2.0)",
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@types/trusted-types": "^2.0.7"
|
||||
}
|
||||
@@ -8062,12 +7886,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/express-rate-limit": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.0.1.tgz",
|
||||
"integrity": "sha512-aZVCnybn7TVmxO4BtlmnvX+nuz8qHW124KKJ8dumsBsmv5ZLxE0pYu7S2nwyRBGHHCAzdmnGyrc5U/rksSPO7Q==",
|
||||
"version": "8.3.2",
|
||||
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.3.2.tgz",
|
||||
"integrity": "sha512-77VmFeJkO0/rvimEDuUC5H30oqUC4EyOhyGccfqoLebB0oiEYfM7nwPrsDsBL1gsTpwfzX8SFy2MT3TDyRq+bg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "10.0.1"
|
||||
"ip-address": "10.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16"
|
||||
@@ -8152,9 +7976,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-xml-builder": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.0.0.tgz",
|
||||
"integrity": "sha512-fpZuDogrAgnyt9oDDz+5DBz0zgPdPZz6D4IR7iESxRXElrlGTRkHJ9eEt+SACRJwT0FNFrt71DFQIUFBJfX/uQ==",
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz",
|
||||
"integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -8162,7 +7986,31 @@
|
||||
"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": {
|
||||
"version": "1.20.1",
|
||||
@@ -8303,9 +8151,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/flatted": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
|
||||
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
|
||||
"version": "3.4.2",
|
||||
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz",
|
||||
"integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
@@ -8316,9 +8164,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/follow-redirects": {
|
||||
"version": "1.15.11",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
|
||||
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
|
||||
"version": "1.16.0",
|
||||
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz",
|
||||
"integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
@@ -8752,9 +8600,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/glob/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==",
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
||||
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -8854,9 +8702,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/handlebars": {
|
||||
"version": "4.7.8",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
|
||||
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
|
||||
"version": "4.7.9",
|
||||
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.9.tgz",
|
||||
"integrity": "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.5",
|
||||
@@ -9235,9 +9083,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/ip-address": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.0.1.tgz",
|
||||
"integrity": "sha512-NWv9YLW4PoW2B7xtzaS3NCot75m6nK7Icdv0o3lfMceJVRfSoQwqD4wEH5rLwoKJwUiZ/rfpiVBhnaF0FK4HoA==",
|
||||
"version": "10.1.0",
|
||||
"resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz",
|
||||
"integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 12"
|
||||
@@ -10156,9 +10004,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/jest-util/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -10693,9 +10541,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.17.23",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz",
|
||||
"integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==",
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.camelcase": {
|
||||
@@ -11237,9 +11085,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/mjml-cli/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==",
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.3.tgz",
|
||||
"integrity": "sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^1.0.0"
|
||||
@@ -11729,37 +11577,6 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/msgpackr": {
|
||||
"version": "1.11.5",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.5.tgz",
|
||||
"integrity": "sha512-UjkUHN0yqp9RWKy0Lplhh+wlpdt9oQBYgULZOiFhV3VclSF1JnSQWZ5r9gORQlNYaUKQoR8itv7g7z1xDDuACA==",
|
||||
"license": "MIT",
|
||||
"optionalDependencies": {
|
||||
"msgpackr-extract": "^3.0.2"
|
||||
}
|
||||
},
|
||||
"node_modules/msgpackr-extract": {
|
||||
"version": "3.0.3",
|
||||
"resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz",
|
||||
"integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"node-gyp-build-optional-packages": "5.2.2"
|
||||
},
|
||||
"bin": {
|
||||
"download-msgpackr-prebuilds": "bin/download-prebuilds.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3",
|
||||
"@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/multer": {
|
||||
"version": "1.4.5-lts.2",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.2.tgz",
|
||||
@@ -11869,12 +11686,6 @@
|
||||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-abort-controller": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-abort-controller/-/node-abort-controller-3.1.1.tgz",
|
||||
"integrity": "sha512-AGK2yQKIjRuqnc6VkX2Xj5d+QW8xZ87pa1UK6yA6ouUyuxfHuMP6umE5QK7UmTeOAymo+Zx1Fxiuw9rVx8taHQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/node-domexception": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
|
||||
@@ -11937,21 +11748,6 @@
|
||||
"webidl-conversions": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/node-gyp-build-optional-packages": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz",
|
||||
"integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"node-gyp-build-optional-packages": "bin.js",
|
||||
"node-gyp-build-optional-packages-optional": "optional.js",
|
||||
"node-gyp-build-optional-packages-test": "build-test.js"
|
||||
}
|
||||
},
|
||||
"node_modules/node-int64": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz",
|
||||
@@ -11966,9 +11762,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/nodemailer": {
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.1.tgz",
|
||||
"integrity": "sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==",
|
||||
"version": "8.0.5",
|
||||
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-8.0.5.tgz",
|
||||
"integrity": "sha512-0PF8Yb1yZuQfQbq+5/pZJrtF6WQcjTd5/S4JOHs9PGFxuTqoB/icwuB44pOdURHJbRKX1PPoJZtY7R4VUoCC8w==",
|
||||
"license": "MIT-0",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
@@ -12333,6 +12129,22 @@
|
||||
"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": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz",
|
||||
@@ -12369,9 +12181,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz",
|
||||
"integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==",
|
||||
"version": "0.1.13",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.13.tgz",
|
||||
"integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
@@ -12384,96 +12196,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/pg": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
|
||||
"integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"pg-connection-string": "^2.12.0",
|
||||
"pg-pool": "^3.13.0",
|
||||
"pg-protocol": "^1.13.0",
|
||||
"pg-types": "2.2.0",
|
||||
"pgpass": "1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"pg-cloudflare": "^1.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"pg-native": ">=3.0.1"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"pg-native": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/pg-cloudflare": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
|
||||
"integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/pg-connection-string": {
|
||||
"version": "2.12.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
|
||||
"integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-int8": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
|
||||
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-pool": {
|
||||
"version": "3.13.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
|
||||
"integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"pg": ">=8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/pg-protocol": {
|
||||
"version": "1.13.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz",
|
||||
"integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/pg-types": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
|
||||
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pg-int8": "1.0.1",
|
||||
"postgres-array": "~2.0.0",
|
||||
"postgres-bytea": "~1.0.0",
|
||||
"postgres-date": "~1.0.4",
|
||||
"postgres-interval": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/pgpass": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
|
||||
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"split2": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
@@ -12481,9 +12203,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz",
|
||||
"integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
@@ -13099,45 +12821,6 @@
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/postgres-array": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
|
||||
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-bytea": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
|
||||
"integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-date": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
|
||||
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/postgres-interval": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
|
||||
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"xtend": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/posthtml": {
|
||||
"version": "0.16.7",
|
||||
"resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.7.tgz",
|
||||
@@ -13318,9 +13001,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.5.4",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz",
|
||||
"integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==",
|
||||
"version": "7.5.5",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.5.tgz",
|
||||
"integrity": "sha512-3wY1AxV+VBNW8Yypfd1yQY9pXnqTAN+KwQxL8iYm3/BjKYMNg4i0owhEe26PWDOMaIrzeeF98Lqd5NGz4omiIg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
@@ -13355,10 +13038,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-from-env": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"license": "MIT"
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz",
|
||||
"integrity": "sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/pstree.remy": {
|
||||
"version": "1.1.8",
|
||||
@@ -14183,15 +13869,6 @@
|
||||
"integrity": "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/split2": {
|
||||
"version": "4.2.0",
|
||||
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
|
||||
"integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">= 10.x"
|
||||
}
|
||||
},
|
||||
"node_modules/sprintf-js": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
@@ -14509,9 +14186,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/strnum": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.0.tgz",
|
||||
"integrity": "sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==",
|
||||
"version": "2.2.2",
|
||||
"resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.2.tgz",
|
||||
"integrity": "sha512-DnR90I+jtXNSTXWdwrEy9FakW7UX+qUZg28gj5fk2vxxl7uS/3bpI4fjFYVmdK9etptYBPNkpahuQnEwhwECqA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
@@ -14538,25 +14215,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/super-simple-scheduler": {
|
||||
"version": "1.4.5",
|
||||
"resolved": "https://registry.npmjs.org/super-simple-scheduler/-/super-simple-scheduler-1.4.5.tgz",
|
||||
"integrity": "sha512-xHR3a+pLywY7HtwjWj/+OaANdEwAieQhckC/i0s6OoHtVJQ4+VLO1VfkKUbxp+oSROGXcaZet8PTGONVdKNUUw==",
|
||||
"version": "1.9.1",
|
||||
"resolved": "https://registry.npmjs.org/super-simple-scheduler/-/super-simple-scheduler-1.9.1.tgz",
|
||||
"integrity": "sha512-JSiEQLk7MsnlSkJ87uoyFKIseG2N27ikB0FSxlh25nnQHWC7R5Lrd0VcoLpwSBanyLzlAslhK4W2rQ0mCU8jCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"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"
|
||||
"uuid": "11.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/super-simple-scheduler/node_modules/uuid": {
|
||||
@@ -14572,28 +14236,6 @@
|
||||
"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": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@@ -14681,47 +14323,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": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/text-hex/-/text-hex-1.0.0.tgz",
|
||||
@@ -14764,9 +14365,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/tinyglobby/node_modules/picomatch": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
|
||||
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
@@ -15013,6 +14614,7 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"devOptional": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
@@ -15155,9 +14757,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.23.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz",
|
||||
"integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==",
|
||||
"version": "6.24.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.24.1.tgz",
|
||||
"integrity": "sha512-sC+b0tB1whOCzbtlx20fx3WgCXwkW627p4EA9uM+/tNNPkSS+eSEld6pAs9nDv7WbY1UUljBMYPtu9BCOrCWKA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
@@ -15825,9 +15427,9 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/yaml": {
|
||||
"version": "2.8.2",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz",
|
||||
"integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==",
|
||||
"version": "2.8.3",
|
||||
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
|
||||
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
|
||||
+7
-8
@@ -5,7 +5,10 @@
|
||||
"main": "index.js",
|
||||
"type": "module",
|
||||
"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",
|
||||
"start": "node --watch ./dist/index.js",
|
||||
"build": "tsc && tsc-alias && cp -r src/templates dist/templates",
|
||||
@@ -29,7 +32,6 @@
|
||||
"@pulsecron/pulse": "1.6.8",
|
||||
"axios": "^1.7.2",
|
||||
"bcryptjs": "3.0.2",
|
||||
"bullmq": "5.41.2",
|
||||
"cacheable-lookup": "7.0.0",
|
||||
"compression": "1.8.1",
|
||||
"cookie-parser": "^1.4.7",
|
||||
@@ -37,7 +39,7 @@
|
||||
"dockerode": "4.0.6",
|
||||
"dotenv": "^16.4.5",
|
||||
"express": "^4.19.2",
|
||||
"express-rate-limit": "8.0.1",
|
||||
"express-rate-limit": "8.3.2",
|
||||
"gamedig": "5.2.0",
|
||||
"got": "14.4.7",
|
||||
"handlebars": "^4.7.8",
|
||||
@@ -51,12 +53,11 @@
|
||||
"mjml": "^5.0.0-alpha.4",
|
||||
"mongoose": "^8.3.3",
|
||||
"multer": "^1.4.5-lts.1",
|
||||
"nodemailer": "8.0.1",
|
||||
"pg": "8.20.0",
|
||||
"nodemailer": "8.0.5",
|
||||
"ping": "0.4.4",
|
||||
"sharp": "0.33.5",
|
||||
"ssl-checker": "2.0.10",
|
||||
"super-simple-scheduler": "1.4.5",
|
||||
"super-simple-scheduler": "1.9.1",
|
||||
"swagger-ui-express": "5.0.1",
|
||||
"winston": "^3.13.0",
|
||||
"ws": "^8.19.0",
|
||||
@@ -78,13 +79,11 @@
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/nodemailer": "7.0.1",
|
||||
"@types/papaparse": "^5.5.2",
|
||||
"@types/pg": "8.20.0",
|
||||
"@types/ping": "0.4.4",
|
||||
"@types/swagger-ui-express": "4.1.8",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.56.1",
|
||||
"@typescript-eslint/parser": "^8.56.1",
|
||||
"c8": "10.1.3",
|
||||
"eslint": "^9.17.0",
|
||||
"eslint-plugin-mocha": "^10.5.0",
|
||||
"esm": "3.2.25",
|
||||
|
||||
@@ -29,6 +29,8 @@ import {
|
||||
MatrixProvider,
|
||||
TeamsProvider,
|
||||
TelegramProvider,
|
||||
PushoverProvider,
|
||||
TwilioProvider,
|
||||
// Interfaces
|
||||
INetworkService,
|
||||
IEmailService,
|
||||
@@ -98,19 +100,6 @@ import {
|
||||
MongoTeamsRepository,
|
||||
MongoMaintenanceWindowsRepository,
|
||||
MongoSettingsRepository,
|
||||
TimescaleMonitorsRepository,
|
||||
TimescaleChecksRepository,
|
||||
TimescaleGeoChecksRepository,
|
||||
TimescaleMonitorStatsRepository,
|
||||
TimescaleStatusPagesRepository,
|
||||
TimescaleUsersRepository,
|
||||
TimescaleInvitesRepository,
|
||||
TimescaleRecoveryTokensRepository,
|
||||
TimescaleNotificationsRepository,
|
||||
TimescaleIncidentsRepository,
|
||||
TimescaleTeamsRepository,
|
||||
TimescaleMaintenanceWindowsRepository,
|
||||
TimescaleSettingsRepository,
|
||||
IMonitorsRepository,
|
||||
IChecksRepository,
|
||||
IGeoChecksRepository,
|
||||
@@ -126,7 +115,6 @@ import {
|
||||
IMaintenanceWindowsRepository,
|
||||
} from "@/repositories/index.js";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
import TimescaleDB from "@/db/TimescaleDB.js";
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
|
||||
export type InitializedServices = {
|
||||
@@ -183,8 +171,6 @@ export const initializeServices = async ({
|
||||
|
||||
if (dbType === "mongodb") {
|
||||
db = new MongoDB(logger, envSettings);
|
||||
} else if (dbType === "timescaledb") {
|
||||
db = new TimescaleDB(logger, envSettings);
|
||||
}
|
||||
|
||||
if (!db) {
|
||||
@@ -224,23 +210,7 @@ export const initializeServices = async ({
|
||||
teamsRepository = new MongoTeamsRepository();
|
||||
maintenanceWindowsRepository = new MongoMaintenanceWindowsRepository();
|
||||
} else {
|
||||
const pool = db.getPool();
|
||||
if (!pool) {
|
||||
throw new Error("Failed to get database pool");
|
||||
}
|
||||
monitorsRepository = new TimescaleMonitorsRepository(pool);
|
||||
checksRepository = new TimescaleChecksRepository(pool);
|
||||
geoChecksRepository = new TimescaleGeoChecksRepository(pool);
|
||||
monitorStatsRepository = new TimescaleMonitorStatsRepository(pool);
|
||||
statusPagesRepository = new TimescaleStatusPagesRepository(pool);
|
||||
usersRepository = new TimescaleUsersRepository(pool);
|
||||
invitesRepository = new TimescaleInvitesRepository(pool);
|
||||
recoveryTokensRepository = new TimescaleRecoveryTokensRepository(pool);
|
||||
settingsRepository = new TimescaleSettingsRepository(pool);
|
||||
notificationsRepository = new TimescaleNotificationsRepository(pool);
|
||||
incidentsRepository = new TimescaleIncidentsRepository(pool);
|
||||
teamsRepository = new TimescaleTeamsRepository(pool);
|
||||
maintenanceWindowsRepository = new TimescaleMaintenanceWindowsRepository(pool);
|
||||
throw new AppError({ message: "Unsupported database type", status: 500 });
|
||||
}
|
||||
|
||||
// Inject settings repository into settings service (now that DB is connected)
|
||||
@@ -298,6 +268,8 @@ export const initializeServices = async ({
|
||||
const matrixProvider = new MatrixProvider(logger);
|
||||
const teamsProvider = new TeamsProvider(logger);
|
||||
const telegramProvider = new TelegramProvider(logger);
|
||||
const pushoverProvider = new PushoverProvider(logger);
|
||||
const twilioProvider = new TwilioProvider(logger);
|
||||
|
||||
const notificationsService = new NotificationsService(
|
||||
notificationsRepository,
|
||||
@@ -310,6 +282,8 @@ export const initializeServices = async ({
|
||||
matrixProvider,
|
||||
teamsProvider,
|
||||
telegramProvider,
|
||||
pushoverProvider,
|
||||
twilioProvider,
|
||||
settingsService,
|
||||
logger,
|
||||
notificationMessageBuilder
|
||||
@@ -364,7 +338,6 @@ export const initializeServices = async ({
|
||||
});
|
||||
const monitorService = new MonitorService({
|
||||
jobQueue: superSimpleQueue,
|
||||
emailService,
|
||||
logger,
|
||||
games,
|
||||
monitorsRepository,
|
||||
|
||||
@@ -29,6 +29,14 @@ export const optionalString = (value: unknown, fieldName: string): string | unde
|
||||
throw new AppError({ message: `${fieldName} must be a string`, status: 400 });
|
||||
};
|
||||
|
||||
export const extractString = (value: unknown): string | undefined => {
|
||||
const candidate = Array.isArray(value) ? value[0] : value;
|
||||
if (typeof candidate !== "string" || candidate.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return candidate;
|
||||
};
|
||||
|
||||
export const optionalNumber = (value: unknown, fieldName: string): number | undefined => {
|
||||
if (value === undefined) {
|
||||
return undefined;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AppError } from "@/utils/AppError.js";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import { requireTeamId, requireUserId, requireUserEmail } from "./controllerUtils.js";
|
||||
import { requireTeamId, requireUserId, requireUserEmail, extractString } from "./controllerUtils.js";
|
||||
import { IIncidentService } from "@/service/index.js";
|
||||
import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } from "@/validation/incidentValidation.js";
|
||||
|
||||
@@ -85,12 +85,13 @@ class IncidentController implements IIncidentController {
|
||||
const teamId = requireTeamId(req.user?.teamId);
|
||||
const userId = requireUserId(req.user?.id);
|
||||
const userEmail = requireUserEmail(req.user?.email);
|
||||
const incidentId = req.params?.incidentId;
|
||||
const incidentId = extractString(req.params?.incidentId);
|
||||
if (!incidentId) {
|
||||
throw new AppError({ message: "Incident ID is required", service: SERVICE_NAME, status: 400 });
|
||||
}
|
||||
|
||||
const resolvedIncident = await this.incidentService.resolveIncident(incidentId, userId, teamId, req?.body?.comment, userEmail);
|
||||
const comment = extractString(req.body?.comment);
|
||||
const resolvedIncident = await this.incidentService.resolveIncident(incidentId, userId, teamId, comment, userEmail);
|
||||
|
||||
return res.status(200).json({
|
||||
success: true,
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { Pool } from "pg";
|
||||
|
||||
export interface IDb {
|
||||
connect(): Promise<void>;
|
||||
disconnect(): Promise<void>;
|
||||
getPool(): Pool | null;
|
||||
}
|
||||
@@ -5,7 +5,6 @@ import { ILogger } from "@/utils/logger.js";
|
||||
import { EnvConfig } from "@/service/system/settingsService.js";
|
||||
const SERVICE_NAME = "MongoDB";
|
||||
import { IDb } from "@/db/IDb.js";
|
||||
import { Pool } from "pg";
|
||||
|
||||
class MongoDB implements IDb {
|
||||
static SERVICE_NAME = SERVICE_NAME;
|
||||
@@ -35,6 +34,12 @@ class MongoDB implements IDb {
|
||||
setDefaultsOnInsert: true,
|
||||
}
|
||||
);
|
||||
// Run migrations BEFORE syncIndexes. The 0006 dedupe migration must run before
|
||||
// the unique index on MonitorStats.monitorId is enforced, otherwise databases
|
||||
// with stray duplicates (caused by the pre-fix race in updateRunningStats)
|
||||
// will crash at startup when the unique index fails to build.
|
||||
await runMigrations();
|
||||
|
||||
// Sync indexes
|
||||
const models = mongoose.modelNames();
|
||||
for (const modelName of models) {
|
||||
@@ -47,8 +52,6 @@ class MongoDB implements IDb {
|
||||
service: SERVICE_NAME,
|
||||
method: "connect",
|
||||
});
|
||||
|
||||
await runMigrations();
|
||||
} catch (error: unknown) {
|
||||
this.logger.error({
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
@@ -75,10 +78,6 @@ class MongoDB implements IDb {
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
getPool = () => {
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
export default MongoDB;
|
||||
@@ -1,105 +0,0 @@
|
||||
import { IDb } from "@/db/IDb.js";
|
||||
import { ILogger } from "@/utils/logger.js";
|
||||
import { EnvConfig } from "@/service/system/settingsService.js";
|
||||
import { Client, Pool } from "pg";
|
||||
import { runTimescaleDBMigrations } from "@/db/migration/timescaledb/index.js";
|
||||
|
||||
const SERVICE_NAME = "TimescaleDB";
|
||||
|
||||
class TimescaleDB implements IDb {
|
||||
private pool: Pool | null = null;
|
||||
|
||||
constructor(
|
||||
private logger: ILogger,
|
||||
private envSettings: EnvConfig
|
||||
) {}
|
||||
|
||||
private ensureDatabaseExists = async (connectionString: string) => {
|
||||
const url = new URL(connectionString);
|
||||
const dbName = url.pathname.slice(1); // Remove leading "/"
|
||||
|
||||
// Connect to the default "postgres" database
|
||||
url.pathname = "/postgres";
|
||||
const client = new Client({ connectionString: url.toString() });
|
||||
await client.connect();
|
||||
|
||||
const result = await client.query("SELECT 1 FROM pg_database WHERE datname = $1", [dbName]);
|
||||
if (result.rowCount === 0) {
|
||||
await client.query(`CREATE DATABASE "${dbName}"`);
|
||||
this.logger.info({
|
||||
message: `Created database "${dbName}"`,
|
||||
service: SERVICE_NAME,
|
||||
method: "ensureDatabaseExists",
|
||||
});
|
||||
}
|
||||
await client.end();
|
||||
};
|
||||
|
||||
connect = async () => {
|
||||
try {
|
||||
const connectionString = this.envSettings.dbConnectionString || "postgresql://postgres:password@localhost:5432/checkmate";
|
||||
|
||||
await this.ensureDatabaseExists(connectionString);
|
||||
|
||||
this.pool = new Pool({ connectionString });
|
||||
|
||||
const client = await this.pool.connect();
|
||||
client.release();
|
||||
|
||||
// Ensure TimescaleDB extension is enabled
|
||||
await this.pool.query("CREATE EXTENSION IF NOT EXISTS timescaledb;");
|
||||
|
||||
this.logger.info({
|
||||
message: "Connected to TimescaleDB",
|
||||
service: SERVICE_NAME,
|
||||
method: "connect",
|
||||
});
|
||||
|
||||
await runTimescaleDBMigrations(this.pool, this.logger);
|
||||
} catch (error: unknown) {
|
||||
this.logger.error({
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
service: SERVICE_NAME,
|
||||
method: "connect",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
disconnect = async () => {
|
||||
try {
|
||||
if (this.pool === null) {
|
||||
return;
|
||||
}
|
||||
this.logger.info({
|
||||
message: "Disconnecting from TimescaleDB",
|
||||
service: SERVICE_NAME,
|
||||
method: "disconnect",
|
||||
});
|
||||
await this.pool.end();
|
||||
this.pool = null;
|
||||
this.logger.info({
|
||||
message: "Disconnected from TimescaleDB",
|
||||
service: SERVICE_NAME,
|
||||
method: "disconnect",
|
||||
});
|
||||
} catch (error: unknown) {
|
||||
this.logger.error({
|
||||
message: error instanceof Error ? error.message : "Unknown error",
|
||||
service: SERVICE_NAME,
|
||||
method: "disconnect",
|
||||
stack: error instanceof Error ? error.stack : undefined,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
getPool = (): Pool => {
|
||||
if (this.pool === null) {
|
||||
throw new Error("TimescaleDB is not connected. Call connect() first.");
|
||||
}
|
||||
return this.pool;
|
||||
};
|
||||
}
|
||||
|
||||
export default TimescaleDB;
|
||||
@@ -0,0 +1,78 @@
|
||||
import { MonitorStatsModel } from "../models/MonitorStats.js";
|
||||
import { logger } from "@/utils/logger.js";
|
||||
|
||||
/**
|
||||
* Second-pass dedupe of MonitorStats before the unique index on monitorId is enforced.
|
||||
*
|
||||
* 0003 ran the same dedupe, but the pre-fix race in StatusService.updateRunningStats
|
||||
* could create new duplicates after 0003 completed. This migration re-runs the dedupe so
|
||||
* the unique index added in the same PR can be built without failing.
|
||||
*
|
||||
* Keeps the document with the highest totalChecks for each monitorId (most data) and
|
||||
* deletes the rest. Falls back to most recent updatedAt when totalChecks tie.
|
||||
*/
|
||||
export async function cleanupDuplicateMonitorStatsForUniqueIndex(): Promise<void> {
|
||||
const SERVICE_NAME = "Migration:CleanupDuplicateMonitorStatsForUniqueIndex";
|
||||
|
||||
try {
|
||||
logger.info({ service: SERVICE_NAME, message: "Starting pre-unique-index dedupe of MonitorStats" });
|
||||
|
||||
const duplicates = await MonitorStatsModel.aggregate([
|
||||
{
|
||||
$group: {
|
||||
_id: "$monitorId",
|
||||
docs: { $push: { id: "$_id", totalChecks: "$totalChecks", updatedAt: "$updatedAt" } },
|
||||
count: { $sum: 1 },
|
||||
},
|
||||
},
|
||||
{ $match: { count: { $gt: 1 } } },
|
||||
]);
|
||||
|
||||
if (duplicates.length === 0) {
|
||||
logger.info({ service: SERVICE_NAME, message: "No duplicate MonitorStats found" });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info({
|
||||
service: SERVICE_NAME,
|
||||
message: `Found ${duplicates.length} monitors with duplicate stats`,
|
||||
});
|
||||
|
||||
let totalDeleted = 0;
|
||||
|
||||
type DocInfo = { id: string; totalChecks: number; updatedAt: Date };
|
||||
|
||||
for (const duplicate of duplicates) {
|
||||
const monitorId = duplicate._id;
|
||||
const docs: DocInfo[] = duplicate.docs;
|
||||
|
||||
// Sort by totalChecks desc, then updatedAt desc — keep the winner, delete the rest.
|
||||
docs.sort((a, b) => {
|
||||
const checksDiff = (b.totalChecks ?? 0) - (a.totalChecks ?? 0);
|
||||
if (checksDiff !== 0) return checksDiff;
|
||||
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime();
|
||||
});
|
||||
|
||||
const toDelete = docs.slice(1).map((doc) => doc.id);
|
||||
|
||||
if (toDelete.length > 0) {
|
||||
const result = await MonitorStatsModel.deleteMany({ _id: { $in: toDelete } });
|
||||
totalDeleted += result.deletedCount ?? 0;
|
||||
|
||||
logger.debug({
|
||||
service: SERVICE_NAME,
|
||||
message: `Deleted ${result.deletedCount} duplicate stats for monitor ${monitorId}`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.info({
|
||||
service: SERVICE_NAME,
|
||||
message: `Cleanup complete. Deleted ${totalDeleted} duplicate MonitorStats documents`,
|
||||
});
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
logger.error({ service: SERVICE_NAME, message: `Error during MonitorStats dedupe: ${errorMessage}` });
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import { cleanupDuplicateMonitorStats } from "./0003_cleanupDuplicateMonitorStat
|
||||
import { fixInfrastructureThresholds } from "./0004_fixInfrastructureThresholds.js";
|
||||
import MigrationModel from "../models/Migration.js";
|
||||
import { migrateStatusPageTypeToArray } from "./0005_migrateStatusPageTypeToArray.js";
|
||||
import { cleanupDuplicateMonitorStatsForUniqueIndex } from "./0006_cleanupDuplicateMonitorStatsForUniqueIndex.js";
|
||||
import type { ILogger } from "@/utils/logger.js";
|
||||
|
||||
type MigrationEntry = {
|
||||
@@ -17,6 +18,7 @@ const migrations: MigrationEntry[] = [
|
||||
{ name: "0003_cleanupDuplicateMonitorStats", execute: cleanupDuplicateMonitorStats },
|
||||
{ name: "0004_fixInfrastructureThresholds", execute: fixInfrastructureThresholds },
|
||||
{ name: "0005_migrateStatusPageTypeToArray", execute: migrateStatusPageTypeToArray },
|
||||
{ name: "0006_cleanupDuplicateMonitorStatsForUniqueIndex", execute: cleanupDuplicateMonitorStatsForUniqueIndex },
|
||||
];
|
||||
|
||||
const runMigrations = async (logger?: ILogger) => {
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
import type { Pool } from "pg";
|
||||
|
||||
export const createEnums = async (pool: Pool) => {
|
||||
await pool.query(`
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE monitor_type AS ENUM ('http', 'ping', 'pagespeed', 'hardware', 'docker', 'port', 'game', 'grpc', 'websocket', 'unknown');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE monitor_status AS ENUM ('up', 'down', 'paused', 'initializing', 'maintenance', 'breached');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE notification_type AS ENUM ('email', 'slack', 'discord', 'webhook', 'pager_duty', 'matrix', 'teams');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE user_role AS ENUM ('user', 'admin', 'superadmin', 'demo');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE incident_resolution_type AS ENUM ('automatic', 'manual');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE duration_unit AS ENUM ('seconds', 'minutes', 'hours', 'days');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE status_page_type AS ENUM ('uptime', 'infrastructure');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
|
||||
DO $$ BEGIN
|
||||
CREATE TYPE match_method AS ENUM ('equal', 'include', 'regex');
|
||||
EXCEPTION WHEN duplicate_object THEN NULL; END $$;
|
||||
`);
|
||||
};
|
||||
|
||||
export const dropEnums = async (pool: Pool) => {
|
||||
await pool.query(`
|
||||
DROP TYPE IF EXISTS match_method;
|
||||
DROP TYPE IF EXISTS status_page_type;
|
||||
DROP TYPE IF EXISTS duration_unit;
|
||||
DROP TYPE IF EXISTS incident_resolution_type;
|
||||
DROP TYPE IF EXISTS user_role;
|
||||
DROP TYPE IF EXISTS notification_type;
|
||||
DROP TYPE IF EXISTS monitor_status;
|
||||
DROP TYPE IF EXISTS monitor_type;
|
||||
`);
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { Pool } from "pg";
|
||||
|
||||
export const createTeams = async (pool: Pool) => {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS teams (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_teams_email ON teams (email);
|
||||
`);
|
||||
};
|
||||
|
||||
export const dropTeams = async (pool: Pool) => {
|
||||
await pool.query(`DROP TABLE IF EXISTS teams;`);
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { Pool } from "pg";
|
||||
|
||||
export const createUsers = async (pool: Pool) => {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
team_id UUID REFERENCES teams(id) ON DELETE SET NULL,
|
||||
first_name TEXT NOT NULL,
|
||||
last_name TEXT NOT NULL,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
password TEXT NOT NULL,
|
||||
avatar_image TEXT,
|
||||
profile_image BYTEA,
|
||||
profile_image_content_type TEXT,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
is_verified BOOLEAN NOT NULL DEFAULT FALSE,
|
||||
roles user_role[] NOT NULL DEFAULT ARRAY['user']::user_role[],
|
||||
check_ttl INTEGER,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_users_team_id ON users (team_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_email ON users (email);
|
||||
`);
|
||||
};
|
||||
|
||||
export const dropUsers = async (pool: Pool) => {
|
||||
await pool.query(`DROP TABLE IF EXISTS users;`);
|
||||
};
|
||||
@@ -1,69 +0,0 @@
|
||||
import type { Pool } from "pg";
|
||||
|
||||
export const createMonitors = async (pool: Pool) => {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS monitors (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id),
|
||||
team_id UUID NOT NULL REFERENCES teams(id),
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
type monitor_type NOT NULL,
|
||||
status monitor_status NOT NULL DEFAULT 'initializing',
|
||||
|
||||
-- HTTP / Website monitoring
|
||||
url TEXT,
|
||||
port INTEGER,
|
||||
ignore_tls_errors BOOLEAN DEFAULT FALSE,
|
||||
use_advanced_matching BOOLEAN DEFAULT FALSE,
|
||||
json_path TEXT,
|
||||
expected_value TEXT,
|
||||
match_method match_method,
|
||||
secret TEXT,
|
||||
|
||||
-- Scheduling
|
||||
interval_ms INTEGER NOT NULL DEFAULT 60000,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
|
||||
-- Uptime tracking
|
||||
status_window BOOLEAN[],
|
||||
status_window_size INTEGER DEFAULT 5,
|
||||
status_window_threshold INTEGER DEFAULT 60,
|
||||
uptime_percentage DOUBLE PRECISION,
|
||||
|
||||
-- Infrastructure alert thresholds
|
||||
cpu_alert_threshold INTEGER,
|
||||
cpu_alert_counter INTEGER,
|
||||
memory_alert_threshold INTEGER,
|
||||
memory_alert_counter INTEGER,
|
||||
disk_alert_threshold INTEGER,
|
||||
disk_alert_counter INTEGER,
|
||||
temp_alert_threshold INTEGER,
|
||||
temp_alert_counter INTEGER,
|
||||
selected_disks TEXT[],
|
||||
|
||||
-- Game monitoring
|
||||
game_id TEXT,
|
||||
grpc_service_name TEXT,
|
||||
|
||||
-- Grouping
|
||||
monitor_group TEXT,
|
||||
|
||||
-- Geo checks
|
||||
geo_check_enabled BOOLEAN DEFAULT FALSE,
|
||||
geo_check_locations TEXT[],
|
||||
geo_check_interval_ms INTEGER DEFAULT 300000,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_monitors_team_type ON monitors (team_id, type);
|
||||
CREATE INDEX IF NOT EXISTS idx_monitors_user_id ON monitors (user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_monitors_status ON monitors (status);
|
||||
`);
|
||||
};
|
||||
|
||||
export const dropMonitors = async (pool: Pool) => {
|
||||
await pool.query(`DROP TABLE IF EXISTS monitors;`);
|
||||
};
|
||||
@@ -1,30 +0,0 @@
|
||||
import type { Pool } from "pg";
|
||||
|
||||
export const createNotifications = async (pool: Pool) => {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
user_id UUID NOT NULL REFERENCES users(id),
|
||||
team_id UUID NOT NULL REFERENCES teams(id),
|
||||
type notification_type NOT NULL,
|
||||
notification_name TEXT,
|
||||
address TEXT,
|
||||
phone TEXT,
|
||||
|
||||
-- Matrix-specific fields
|
||||
homeserver_url TEXT,
|
||||
room_id TEXT,
|
||||
access_token TEXT,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_team ON notifications (team_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_notifications_user ON notifications (user_id);
|
||||
`);
|
||||
};
|
||||
|
||||
export const dropNotifications = async (pool: Pool) => {
|
||||
await pool.query(`DROP TABLE IF EXISTS notifications;`);
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { Pool } from "pg";
|
||||
|
||||
export const createMonitorNotifications = async (pool: Pool) => {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS monitor_notifications (
|
||||
monitor_id UUID NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
|
||||
notification_id UUID NOT NULL REFERENCES notifications(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (monitor_id, notification_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_monitor_notifications_notification
|
||||
ON monitor_notifications (notification_id);
|
||||
`);
|
||||
};
|
||||
|
||||
export const dropMonitorNotifications = async (pool: Pool) => {
|
||||
await pool.query(`DROP TABLE IF EXISTS monitor_notifications;`);
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { Pool } from "pg";
|
||||
|
||||
export const createIncidents = async (pool: Pool) => {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS incidents (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
monitor_id UUID NOT NULL REFERENCES monitors(id) ON DELETE CASCADE,
|
||||
team_id UUID NOT NULL REFERENCES teams(id) ON DELETE CASCADE,
|
||||
start_time TIMESTAMPTZ NOT NULL,
|
||||
end_time TIMESTAMPTZ,
|
||||
status BOOLEAN,
|
||||
message TEXT,
|
||||
status_code INTEGER,
|
||||
resolution_type incident_resolution_type,
|
||||
resolved_by UUID REFERENCES users(id) ON DELETE SET NULL,
|
||||
resolved_by_email TEXT,
|
||||
comment TEXT,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_incidents_monitor_status ON incidents (monitor_id, status);
|
||||
CREATE INDEX IF NOT EXISTS idx_incidents_team_status ON incidents (team_id, status);
|
||||
CREATE INDEX IF NOT EXISTS idx_incidents_team_start ON incidents (team_id, start_time DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_incidents_status_start ON incidents (status, start_time DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_incidents_resolution ON incidents (resolution_type, status);
|
||||
CREATE INDEX IF NOT EXISTS idx_incidents_resolved_by ON incidents (resolved_by, status);
|
||||
CREATE INDEX IF NOT EXISTS idx_incidents_created ON incidents (created_at DESC);
|
||||
`);
|
||||
};
|
||||
|
||||
export const dropIncidents = async (pool: Pool) => {
|
||||
await pool.query(`DROP TABLE IF EXISTS incidents;`);
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { Pool } from "pg";
|
||||
|
||||
export const createMonitorStats = async (pool: Pool) => {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS monitor_stats (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
monitor_id UUID NOT NULL UNIQUE REFERENCES monitors(id) ON DELETE CASCADE,
|
||||
avg_response_time DOUBLE PRECISION DEFAULT 0,
|
||||
max_response_time DOUBLE PRECISION DEFAULT 0,
|
||||
total_checks BIGINT DEFAULT 0,
|
||||
total_up_checks BIGINT DEFAULT 0,
|
||||
total_down_checks BIGINT DEFAULT 0,
|
||||
uptime_percentage DOUBLE PRECISION DEFAULT 0,
|
||||
last_check_timestamp TIMESTAMPTZ,
|
||||
last_response_time DOUBLE PRECISION DEFAULT 0,
|
||||
time_of_last_failure TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
`);
|
||||
};
|
||||
|
||||
export const dropMonitorStats = async (pool: Pool) => {
|
||||
await pool.query(`DROP TABLE IF EXISTS monitor_stats;`);
|
||||
};
|
||||
@@ -1,105 +0,0 @@
|
||||
import type { Pool } from "pg";
|
||||
|
||||
export const createChecks = async (pool: Pool) => {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS checks (
|
||||
id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
monitor_id UUID NOT NULL,
|
||||
team_id UUID NOT NULL,
|
||||
monitor_type monitor_type NOT NULL,
|
||||
status BOOLEAN,
|
||||
response_time INTEGER,
|
||||
status_code INTEGER,
|
||||
message TEXT,
|
||||
|
||||
-- HTTP request timings
|
||||
timing_start DOUBLE PRECISION,
|
||||
timing_socket DOUBLE PRECISION,
|
||||
timing_lookup DOUBLE PRECISION,
|
||||
timing_connect DOUBLE PRECISION,
|
||||
timing_secure_connect DOUBLE PRECISION,
|
||||
timing_upload DOUBLE PRECISION,
|
||||
timing_response DOUBLE PRECISION,
|
||||
timing_end DOUBLE PRECISION,
|
||||
phase_wait DOUBLE PRECISION,
|
||||
phase_dns DOUBLE PRECISION,
|
||||
phase_tcp DOUBLE PRECISION,
|
||||
phase_tls DOUBLE PRECISION,
|
||||
phase_request DOUBLE PRECISION,
|
||||
phase_first_byte DOUBLE PRECISION,
|
||||
phase_download DOUBLE PRECISION,
|
||||
phase_total DOUBLE PRECISION,
|
||||
|
||||
-- CPU metrics
|
||||
cpu_physical_core INTEGER,
|
||||
cpu_logical_core INTEGER,
|
||||
cpu_frequency DOUBLE PRECISION,
|
||||
cpu_current_frequency DOUBLE PRECISION,
|
||||
cpu_temperature DOUBLE PRECISION[],
|
||||
cpu_free_percent DOUBLE PRECISION,
|
||||
cpu_usage_percent DOUBLE PRECISION,
|
||||
|
||||
-- Memory metrics
|
||||
mem_total_bytes BIGINT,
|
||||
mem_available_bytes BIGINT,
|
||||
mem_used_bytes BIGINT,
|
||||
mem_usage_percent DOUBLE PRECISION,
|
||||
|
||||
-- Host info
|
||||
host_os TEXT,
|
||||
host_platform TEXT,
|
||||
host_kernel_version TEXT,
|
||||
host_pretty_name TEXT,
|
||||
|
||||
-- Capture agent info
|
||||
capture_version TEXT,
|
||||
capture_mode TEXT,
|
||||
|
||||
-- Lighthouse / PageSpeed scores
|
||||
lighthouse_performance DOUBLE PRECISION,
|
||||
lighthouse_accessibility DOUBLE PRECISION,
|
||||
lighthouse_best_practices DOUBLE PRECISION,
|
||||
lighthouse_seo DOUBLE PRECISION,
|
||||
|
||||
-- Lighthouse audit details
|
||||
audit_cls_score DOUBLE PRECISION,
|
||||
audit_cls_value DOUBLE PRECISION,
|
||||
audit_cls_display TEXT,
|
||||
audit_si_score DOUBLE PRECISION,
|
||||
audit_si_value DOUBLE PRECISION,
|
||||
audit_si_display TEXT,
|
||||
audit_fcp_score DOUBLE PRECISION,
|
||||
audit_fcp_value DOUBLE PRECISION,
|
||||
audit_fcp_display TEXT,
|
||||
audit_lcp_score DOUBLE PRECISION,
|
||||
audit_lcp_value DOUBLE PRECISION,
|
||||
audit_lcp_display TEXT,
|
||||
audit_tbt_score DOUBLE PRECISION,
|
||||
audit_tbt_value DOUBLE PRECISION,
|
||||
audit_tbt_display TEXT,
|
||||
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
SELECT create_hypertable('checks', 'created_at',
|
||||
chunk_time_interval => INTERVAL '7 days',
|
||||
if_not_exists => TRUE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_monitor_time
|
||||
ON checks (monitor_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_team_time
|
||||
ON checks (team_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_monitor_type_time
|
||||
ON checks (monitor_id, monitor_type, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_team_status_time
|
||||
ON checks (team_id, status, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_checks_status_code
|
||||
ON checks (status_code, created_at DESC);
|
||||
`);
|
||||
};
|
||||
|
||||
export const dropChecks = async (pool: Pool) => {
|
||||
await pool.query(`DROP TABLE IF EXISTS checks;`);
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import type { Pool } from "pg";
|
||||
|
||||
export const createCheckDisks = async (pool: Pool) => {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS check_disks (
|
||||
id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
check_id UUID NOT NULL,
|
||||
check_created_at TIMESTAMPTZ NOT NULL,
|
||||
device TEXT,
|
||||
mountpoint TEXT,
|
||||
total_bytes BIGINT,
|
||||
free_bytes BIGINT,
|
||||
used_bytes BIGINT,
|
||||
usage_percent DOUBLE PRECISION,
|
||||
total_inodes BIGINT,
|
||||
free_inodes BIGINT,
|
||||
used_inodes BIGINT,
|
||||
inodes_usage_percent DOUBLE PRECISION,
|
||||
read_bytes BIGINT,
|
||||
write_bytes BIGINT,
|
||||
read_time BIGINT,
|
||||
write_time BIGINT
|
||||
);
|
||||
|
||||
SELECT create_hypertable('check_disks', 'check_created_at',
|
||||
chunk_time_interval => INTERVAL '7 days',
|
||||
if_not_exists => TRUE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_check_disks_check
|
||||
ON check_disks (check_id, check_created_at);
|
||||
`);
|
||||
};
|
||||
|
||||
export const dropCheckDisks = async (pool: Pool) => {
|
||||
await pool.query(`DROP TABLE IF EXISTS check_disks;`);
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import type { Pool } from "pg";
|
||||
|
||||
export const createCheckNetworkInterfaces = async (pool: Pool) => {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS check_network_interfaces (
|
||||
id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
check_id UUID NOT NULL,
|
||||
check_created_at TIMESTAMPTZ NOT NULL,
|
||||
name TEXT,
|
||||
bytes_sent BIGINT,
|
||||
bytes_recv BIGINT,
|
||||
packets_sent BIGINT,
|
||||
packets_recv BIGINT,
|
||||
err_in BIGINT,
|
||||
err_out BIGINT,
|
||||
drop_in BIGINT,
|
||||
drop_out BIGINT,
|
||||
fifo_in BIGINT,
|
||||
fifo_out BIGINT
|
||||
);
|
||||
|
||||
SELECT create_hypertable('check_network_interfaces', 'check_created_at',
|
||||
chunk_time_interval => INTERVAL '7 days',
|
||||
if_not_exists => TRUE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_check_net_check
|
||||
ON check_network_interfaces (check_id, check_created_at);
|
||||
`);
|
||||
};
|
||||
|
||||
export const dropCheckNetworkInterfaces = async (pool: Pool) => {
|
||||
await pool.query(`DROP TABLE IF EXISTS check_network_interfaces;`);
|
||||
};
|
||||
@@ -1,25 +0,0 @@
|
||||
import type { Pool } from "pg";
|
||||
|
||||
export const createCheckErrors = async (pool: Pool) => {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS check_errors (
|
||||
id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
check_id UUID NOT NULL,
|
||||
check_created_at TIMESTAMPTZ NOT NULL,
|
||||
metrics TEXT[],
|
||||
error TEXT
|
||||
);
|
||||
|
||||
SELECT create_hypertable('check_errors', 'check_created_at',
|
||||
chunk_time_interval => INTERVAL '7 days',
|
||||
if_not_exists => TRUE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_check_errors_check
|
||||
ON check_errors (check_id, check_created_at);
|
||||
`);
|
||||
};
|
||||
|
||||
export const dropCheckErrors = async (pool: Pool) => {
|
||||
await pool.query(`DROP TABLE IF EXISTS check_errors;`);
|
||||
};
|
||||
@@ -1,29 +0,0 @@
|
||||
import type { Pool } from "pg";
|
||||
|
||||
export const createGeoChecks = async (pool: Pool) => {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS geo_checks (
|
||||
id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
monitor_id UUID NOT NULL,
|
||||
team_id UUID NOT NULL,
|
||||
monitor_type monitor_type NOT NULL,
|
||||
expiry TIMESTAMPTZ,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
SELECT create_hypertable('geo_checks', 'created_at',
|
||||
chunk_time_interval => INTERVAL '7 days',
|
||||
if_not_exists => TRUE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_geo_checks_monitor_time
|
||||
ON geo_checks (monitor_id, created_at DESC);
|
||||
CREATE INDEX IF NOT EXISTS idx_geo_checks_team_time
|
||||
ON geo_checks (team_id, created_at DESC);
|
||||
`);
|
||||
};
|
||||
|
||||
export const dropGeoChecks = async (pool: Pool) => {
|
||||
await pool.query(`DROP TABLE IF EXISTS geo_checks;`);
|
||||
};
|
||||
@@ -1,46 +0,0 @@
|
||||
import type { Pool } from "pg";
|
||||
|
||||
export const createGeoCheckResults = async (pool: Pool) => {
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS geo_check_results (
|
||||
id UUID NOT NULL DEFAULT gen_random_uuid(),
|
||||
geo_check_id UUID NOT NULL,
|
||||
geo_check_created_at TIMESTAMPTZ NOT NULL,
|
||||
|
||||
-- Status
|
||||
status BOOLEAN,
|
||||
status_code INTEGER,
|
||||
|
||||
-- Location
|
||||
location_continent TEXT,
|
||||
location_region TEXT,
|
||||
location_country TEXT,
|
||||
location_state TEXT,
|
||||
location_city TEXT,
|
||||
location_longitude DOUBLE PRECISION,
|
||||
location_latitude DOUBLE PRECISION,
|
||||
|
||||
-- Timings
|
||||
timing_total DOUBLE PRECISION,
|
||||
timing_dns DOUBLE PRECISION,
|
||||
timing_tcp DOUBLE PRECISION,
|
||||
timing_tls DOUBLE PRECISION,
|
||||
timing_first_byte DOUBLE PRECISION,
|
||||
timing_download DOUBLE PRECISION
|
||||
);
|
||||
|
||||
SELECT create_hypertable('geo_check_results', 'geo_check_created_at',
|
||||
chunk_time_interval => INTERVAL '7 days',
|
||||
if_not_exists => TRUE
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_geo_results_check
|
||||
ON geo_check_results (geo_check_id, geo_check_created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_geo_results_country
|
||||
ON geo_check_results (location_country, geo_check_created_at DESC);
|
||||
`);
|
||||
};
|
||||
|
||||
export const dropGeoCheckResults = async (pool: Pool) => {
|
||||
await pool.query(`DROP TABLE IF EXISTS geo_check_results;`);
|
||||
};
|
||||
Loaded 100 of 222 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user