diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..f67f79287 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,6 @@ +**/node_modules +**/dist +**/.env +**/*.env +.git +docker/dev/mongo/data diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml new file mode 100644 index 000000000..f7e204159 --- /dev/null +++ b/.github/workflows/build-image.yml @@ -0,0 +1,103 @@ +name: Build image + +on: + push: + branches: ["develop", "master"] + tags: ["v*"] + workflow_dispatch: + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # checkmate-backend-mono is a transitional alias so existing deployments + # keep receiving updates; drop it after one release cycle. + - name: Docker metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: | + ghcr.io/${{ github.repository_owner }}/checkmate + ghcr.io/${{ github.repository_owner }}/checkmate-backend-mono + flavor: | + latest=false + tags: | + type=ref,event=branch + type=ref,event=tag + type=semver,pattern={{version}} + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} + + - name: Build and push multi-arch image + uses: docker/build-push-action@v6 + with: + context: . + file: ./docker/Dockerfile + push: true + platforms: linux/amd64,linux/arm64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + # The hosted staging/prod instances share one host and one compose file; each + # deploy pulls and recreates only its own service. + deploy-staging: + needs: build-and-push + if: github.ref == 'refs/heads/develop' + runs-on: ubuntu-latest + steps: + - name: Deploy staging.checkmate.so + uses: appleboy/ssh-action@v1.2.2 + with: + host: ${{ secrets.DEMO_SERVER_HOST }} + username: ${{ secrets.DEMO_SERVER_USER }} + key: ${{ secrets.DEMO_SERVER_SSH_KEY }} + script: | + cd checkmate + git pull + cd docs/hosting/checkmate-so + docker compose pull checkmate-staging + docker compose up -d checkmate-staging + docker system prune -af + + deploy-prod: + needs: build-and-push + if: github.ref == 'refs/heads/master' + runs-on: ubuntu-latest + steps: + - name: Deploy demo.checkmate.so + uses: appleboy/ssh-action@v1.2.2 + with: + host: ${{ secrets.DEMO_SERVER_HOST }} + username: ${{ secrets.DEMO_SERVER_USER }} + key: ${{ secrets.DEMO_SERVER_SSH_KEY }} + script: | + cd checkmate + git pull + cd docs/hosting/checkmate-so + docker compose pull checkmate-demo + docker compose up -d checkmate-demo + docker system prune -af diff --git a/.github/workflows/deploy-images-on-release.yml b/.github/workflows/deploy-images-on-release.yml deleted file mode 100644 index bf8f5069f..000000000 --- a/.github/workflows/deploy-images-on-release.yml +++ /dev/null @@ -1,148 +0,0 @@ -name: Deploy images on release - -on: - push: - tags: - - "v*" - workflow_dispatch: -jobs: - docker-build-and-push-client: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Extract version from tag - id: extract_tag - run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build Client Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate-client:${{ steps.extract_tag.outputs.version }} \ - -f ./docker/dist/client.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push Client Docker image - run: | - docker push ghcr.io/bluewave-labs/checkmate-client:${{ steps.extract_tag.outputs.version }} - - docker-build-and-push-server: - needs: docker-build-and-push-client - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Extract version - id: extract_tag - run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build Server Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate-backend:${{ steps.extract_tag.outputs.version }} \ - -f ./docker/dist/server.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push Server Docker image - run: | - docker push ghcr.io/bluewave-labs/checkmate-backend:${{ steps.extract_tag.outputs.version }} - - - name: Build Mongo Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate-mongo:${{ steps.extract_tag.outputs.version }} \ - -f ./docker/dist/mongoDB.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push MongoDB Docker image - run: | - docker push ghcr.io/bluewave-labs/checkmate-mongo:${{ steps.extract_tag.outputs.version }} - - docker-build-and-push-server-mono-multiarch: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Extract version - id: extract_tag - run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build and push multi-arch Docker image - uses: docker/build-push-action@v5 - with: - context: . - file: ./docker/dist-arm/server.Dockerfile - push: true - no-cache: true - tags: | - ghcr.io/bluewave-labs/checkmate-backend-mono-multiarch:${{ steps.extract_tag.outputs.version }} - platforms: linux/amd64,linux/arm64 - labels: | - org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate - build-args: | - VITE_APP_VERSION=${{ steps.extract_tag.outputs.version }} - - docker-build-and-push-server-mono: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Extract version - id: extract_tag - run: echo "version=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build Server Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate-backend-mono:${{ steps.extract_tag.outputs.version }} \ - -f ./docker/dist-mono/server.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - --build-arg VITE_APP_VERSION=${{ steps.extract_tag.outputs.version }} \ - . - - - name: Push Server Docker image - run: docker push ghcr.io/bluewave-labs/checkmate-backend-mono:${{ steps.extract_tag.outputs.version }} diff --git a/.github/workflows/deploy-images.yml b/.github/workflows/deploy-images.yml deleted file mode 100644 index 5bd4ba1d5..000000000 --- a/.github/workflows/deploy-images.yml +++ /dev/null @@ -1,143 +0,0 @@ -name: Deploy images - -on: - push: - branches: ["master"] - workflow_dispatch: -jobs: - docker-build-and-push-client: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Get version - id: vars - run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV - - name: Build Client Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate-client:latest \ - -f ./docker/dist/client.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - --build-arg VITE_APP_VERSION=${{ env.VERSION }} \ - . - - - name: Push Client Docker image - run: | - docker push ghcr.io/bluewave-labs/checkmate-client:latest - - docker-build-and-push-server: - needs: docker-build-and-push-client - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build Server Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate-backend:latest \ - -f ./docker/dist/server.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push Server Docker image - run: | - docker push ghcr.io/bluewave-labs/checkmate-backend:latest - - - name: Build Mongo Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate-mongo:latest \ - -f ./docker/dist/mongoDB.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push MongoDB Docker image - run: | - docker push ghcr.io/bluewave-labs/checkmate-mongo:latest - - docker-build-and-push-server-mono-multiarch: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Get version - id: vars - run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV - - - name: Build and push multi-arch Docker image - uses: docker/build-push-action@v5 - with: - context: . - file: ./docker/dist-arm/server.Dockerfile - push: true - no-cache: true - tags: | - ghcr.io/bluewave-labs/checkmate-backend-mono-multiarch:latest - platforms: linux/amd64,linux/arm64 - labels: | - org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate - build-args: | - VITE_APP_VERSION=${{ env.VERSION }} - - docker-build-and-push-server-mono: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Get version - id: vars - run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV - - - name: Build Server Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate-backend-mono:latest \ - -f ./docker/dist-mono/server.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - --build-arg VITE_APP_VERSION=${{ env.VERSION }} \ - . - - - name: Push Server Docker image - run: docker push ghcr.io/bluewave-labs/checkmate-backend-mono:latest diff --git a/.github/workflows/production-deploy.yml b/.github/workflows/production-deploy.yml deleted file mode 100644 index a42efc4a4..000000000 --- a/.github/workflows/production-deploy.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: Demo deploy - -on: - push: - branches: [ "demo" ] - workflow_dispatch: -jobs: - docker-build-and-push-client: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Get version - id: vars - run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV - - name: Build Client Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate:frontend-demo \ - -f ./docker/prod/client.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - --build-arg VITE_APP_VERSION=${{ env.VERSION }} \ - . - - - name: Push Client Docker image - run: docker push ghcr.io/bluewave-labs/checkmate:frontend-demo - - docker-build-and-push-server: - needs: docker-build-and-push-client - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build Server Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate:backend-demo \ - -f ./docker/prod/server.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push Server Docker image - run: docker push ghcr.io/bluewave-labs/checkmate:backend-demo - - - name: Build Mongo Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate:mongo-demo \ - -f ./docker/prod/mongoDB.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push MongoDB Docker image - run: docker push ghcr.io/bluewave-labs/checkmate:mongo-demo - - deploy-to-demo: - needs: docker-build-and-push-server - runs-on: ubuntu-latest - steps: - - name: SSH into server and restart container using Docker Compose - uses: appleboy/ssh-action@v1.2.2 - with: - host: ${{ secrets.DEMO_SERVER_HOST }} - username: ${{ secrets.DEMO_SERVER_USER }} - key: ${{ secrets.DEMO_SERVER_SSH_KEY }} - script: | - cd checkmate - git pull - cd docker/unified - docker compose down - docker compose pull - docker compose up -d - docker system prune -af diff --git a/.github/workflows/staging-deploy.yml b/.github/workflows/staging-deploy.yml deleted file mode 100644 index e7983f249..000000000 --- a/.github/workflows/staging-deploy.yml +++ /dev/null @@ -1,91 +0,0 @@ -name: Staging deploy - -on: - push: - branches: [ "develop" ] - -jobs: - docker-build-and-push-client: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Get version - id: vars - run: echo "VERSION=$(git describe --tags --abbrev=0)" >> $GITHUB_ENV - - name: Build Client Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate:frontend-staging \ - -f ./docker/staging/client.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - --build-arg VITE_APP_VERSION=${{ env.VERSION }} \ - . - - - name: Push Client Docker image - run: docker push ghcr.io/bluewave-labs/checkmate:frontend-staging - - docker-build-and-push-server: - needs: docker-build-and-push-client - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build Server Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate:backend-staging \ - -f ./docker/staging/server.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push Server Docker image - run: docker push ghcr.io/bluewave-labs/checkmate:backend-staging - - - name: Build Mongo Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate:mongo-staging \ - -f ./docker/staging/mongoDB.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate \ - . - - - name: Push MongoDB Docker image - run: docker push ghcr.io/bluewave-labs/checkmate:mongo-staging - - deploy-to-staging: - needs: docker-build-and-push-server - runs-on: ubuntu-latest - steps: - - name: SSH into server and restart container using Docker Compose - uses: appleboy/ssh-action@v1.2.2 - with: - host: ${{ secrets.DEMO_SERVER_HOST }} - username: ${{ secrets.DEMO_SERVER_USER }} - key: ${{ secrets.DEMO_SERVER_SSH_KEY }} - script: | - cd checkmate - git pull - cd docker/unified - docker compose down - docker compose pull - docker compose up -d - docker system prune -af diff --git a/CLAUDE.md b/CLAUDE.md index 1adfd703f..3e601b453 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -35,9 +35,9 @@ npm run format # Prettier formatting ### Docker Development ```bash -cd docker/dev -./build_images.sh -docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:6.0 +docker build -f docker/Dockerfile -t checkmate . # build the mono image (server + built client) +docker compose -f docker/dev/docker-compose.yaml up # local full stack (builds image, runs mongo + worker) +docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:8.0 # just a dev database ``` ## Environment Setup diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2e536da5..ea93f8100 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,34 +88,10 @@ cd Checkmate #### Step 2: Set Up Docker Containers (MongoDB) -Navigate to the Docker dev directory: - -```bash -cd docker/dev -``` - -Build the Docker images: - -Linux: -```bash -./build_images.sh -``` - -Windows: -```bash -./build_images.bat -``` - Run MongoDB container: ```bash -docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:6.0 -``` - -Navigate back to the root directory: - -```bash -cd ../.. +docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:8.0 ``` #### Step 3: Set Up the Backend (Server) @@ -256,6 +232,35 @@ docker rm uptime_database_mongo Start with [good first issues](https://github.com/bluewave-labs/checkmate/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). +### Improve the documentation? + +Documentation contributions are very welcome โ€” fixing a typo, clarifying a step, or adding a missing example is a valuable PR. + +- Small edits (typos, wording, formatting): open a PR directly against `develop`. +- Larger changes (restructuring a section, adding a new guide): open an issue first so we can align on scope, then a PR. +- Screenshots are helpful when documenting UI behaviour. +- README translations live in `docs/translations/`. If you're adding a new language, please also add a flag + link to the language bar in the root `README.md` and every existing translated README so they stay in sync. + +### Help with translations? + +The application uses `i18next` with one JSON file per language under `client/src/locales/` (`en.json` is the source of truth). English source strings are the definitive copy; other languages are generated as one-time machine translations that anyone is welcome to refine. + +**To improve an existing language:** + +1. Edit the relevant `client/src/locales/.json` file directly. +2. Keep the JSON key structure identical to `en.json` โ€” only translate the string values. +3. Open a PR against `develop`. No language-selector code changes are needed. + +**To add a new language:** + +1. Add `client/src/locales/.json` with the same key structure as `en.json`, translated. If you can't translate every key, leave unfamiliar strings in English rather than removing keys โ€” i18next falls back to the key path when a value is missing. +2. Add the display name to `languageNames` in `client/src/Components/inputs/LanguageSelector.tsx` (e.g. `it: "Italiano"`). The selector picks up any language present in `resources` automatically. +3. Open a PR against `develop`. + +If translating a whole file is more than you want to take on, open an issue asking us to add the language and we'll generate an initial machine-translated file that you (or others) can refine over time. + +**Note:** an older workflow that synced translations with PoEditor is still in the repo but no longer authoritative. Please treat the `.json` files in `client/src/locales/` as the source of truth. + ### Submit a pull request? Follow the [pull request checklist](#pull-request-checklist). Your PR should: diff --git a/README.md b/README.md index c6830f566..bcbd557d4 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ ๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol | ๐Ÿ‡ซ๐Ÿ‡ฎ Suomi | ๐Ÿ‡ซ๐Ÿ‡ท Franรงais | + ๐Ÿ‡ฎ๐Ÿ‡น Italiano | ๐Ÿ‡ฏ๐Ÿ‡ต ๆ—ฅๆœฌ่ชž | ๐Ÿ‡ง๐Ÿ‡ท Portuguรชs (Brasil) | ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน | @@ -79,7 +80,37 @@ Usage instructions can be found [here](https://checkmate.so/docs). ## Installation -See installation instructions in [Checkmate documentation portal](https://checkmate.so/docs). +The quickest way to run Checkmate is the reference Docker Compose file, which runs the all-in-one image (`ghcr.io/bluewave-labs/checkmate`) plus MongoDB: + +```bash +curl -O https://raw.githubusercontent.com/bluewave-labs/checkmate/master/docker/docker-compose.yaml +JWT_SECRET="$(openssl rand -hex 32)" docker compose up -d +``` + +Then open http://localhost:52345. If the app is reached at another origin (domain or LAN IP), set `CLIENT_HOST` accordingly. To build the image yourself, run `docker build -f docker/Dockerfile -t checkmate .` from a checkout. For TLS, put any reverse proxy (Caddy, Traefik, nginx) in front of port 52345. + +### Configuration + +The image is configured entirely through environment variables on the server container: + +| Variable | Required | Description | +|---|---|---| +| `DB_CONNECTION_STRING` | Yes | MongoDB connection string, e.g. `mongodb://mongodb:27017/uptime_db` | +| `JWT_SECRET` | Yes | Secret used to sign auth tokens; generate one with `openssl rand -hex 32` | +| `CLIENT_HOST` | Yes | The URL users reach the app at, e.g. `https://checkmate.example.com`; used for CORS and for links in notifications and emails | +| `LOG_LEVEL` | No | Server log level: `error`, `warn`, `info`, or `debug` (default `debug`) | + +The web client needs no configuration by default: it calls the API on the same origin it was served from (`/api/v1`). For setups where the defaults don't apply โ€” for example, the API is reached through a different origin than the page โ€” the server renders overrides into the client at runtime via these optional variables: + +| Variable | Description | +|---|---| +| `CLIENT_CONFIG_API_BASE_URL` | Full base URL the client calls the API at, e.g. `https://api.example.com/api/v1`; defaults to same-origin `/api/v1` | +| `CLIENT_CONFIG_CLIENT_HOST` | Origin used when the client builds absolute links (invites, status pages); defaults to the browser's current origin | +| `CLIENT_CONFIG_LOG_LEVEL` | Browser console log level: `error`, `warn`, `info`, or `debug` (default `error`) | + +> **Upgrading from an older image?** The `UPTIME_APP_*` variables (`UPTIME_APP_API_BASE_URL`, `UPTIME_APP_CLIENT_HOST`, `UPTIME_APP_LOG_LEVEL`) are no longer read. In most setups no replacement is needed โ€” the same-origin defaults cover them; if you pointed the client at a different origin, use the `CLIENT_CONFIG_*` equivalents above. The `checkmate-client`, `checkmate-backend`, `checkmate-mongo`, and `checkmate-backend-mono-multiarch` images are no longer updated โ€” switch to `ghcr.io/bluewave-labs/checkmate`, keeping your existing MongoDB service and data volume. + +See full installation instructions in the [Checkmate documentation portal](https://checkmate.so/docs). 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. diff --git a/charts/helm/checkmate/Chart.yaml b/charts/helm/checkmate/Chart.yaml index c9433f690..286f1c908 100644 --- a/charts/helm/checkmate/Chart.yaml +++ b/charts/helm/checkmate/Chart.yaml @@ -3,4 +3,8 @@ name: checkmate-chart description: A Helm chart for Checkmate App type: application version: 0.2.0 +# DO NOT bump appVersion past v3.8.1 until this chart is migrated to the all-in-one +# image (ghcr.io/bluewave-labs/checkmate). The checkmate-client / checkmate-backend / +# checkmate-mongo images this chart references are frozen at v3.8.1 โ€” later versions +# are never published, so a bumped chart will fail to pull. appVersion: "v3.8.1" # "v" prefix: published image tags are v-prefixed (ghcr.io/.../checkmate-*:v3.8.1) diff --git a/client/.env.production b/client/.env.production index 25849c7b9..d5b77ff28 100644 --- a/client/.env.production +++ b/client/.env.production @@ -1,4 +1,6 @@ -VITE_APP_API_BASE_URL=UPTIME_APP_API_BASE_URL -VITE_APP_API_V2_BASE_URL=UPTIME_APP_API_V2_BASE_URL -VITE_APP_CLIENT_HOST=UPTIME_APP_CLIENT_HOST -VITE_APP_LOG_LEVEL=UPTIME_APP_LOG_LEVEL \ No newline at end of file +# Production builds use same-origin defaults: API at /api/v1, client host from +# window.location.origin. The empty values below override any local .env so a +# production build never bakes in a developer's localhost URLs. +VITE_APP_API_BASE_URL= +VITE_APP_CLIENT_HOST= +VITE_APP_LOG_LEVEL= diff --git a/client/.gitignore b/client/.gitignore index 23b6e49af..8882da3ee 100644 --- a/client/.gitignore +++ b/client/.gitignore @@ -28,5 +28,4 @@ dist-ssr # Scripts *.sh -!env.sh .env.development \ No newline at end of file diff --git a/client/env.sh b/client/env.sh deleted file mode 100755 index d95f8ee8f..000000000 --- a/client/env.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh -for i in $(env | grep UPTIME_APP_) -do - key=$(echo $i | cut -d '=' -f 1) - value=$(echo $i | cut -d '=' -f 2-) - echo $key=$value - find /usr/share/nginx/html -type f \( -name '*.js' -o -name '*.css' \) -exec sed -i "s|${key}|${value}|g" '{}' + -done \ No newline at end of file diff --git a/client/index.html b/client/index.html index 4f949edbb..5f48a1bd3 100644 --- a/client/index.html +++ b/client/index.html @@ -12,6 +12,7 @@ /> Checkmate + diff --git a/client/package-lock.json b/client/package-lock.json index 8af4fb08a..bca8463c0 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -1,12 +1,12 @@ { "name": "client", - "version": "3.9.2", + "version": "3.10.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "client", - "version": "3.9.2", + "version": "3.10.0", "dependencies": { "@csstools/css-tokenizer": "^3.0.4", "@emotion/react": "^11.13.3", @@ -92,7 +92,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz", "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -408,7 +407,6 @@ "node_modules/@emotion/react": { "version": "11.14.0", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -446,7 +444,6 @@ "node_modules/@emotion/styled": { "version": "11.14.1", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.18.3", "@emotion/babel-plugin": "^11.13.5", @@ -1226,7 +1223,6 @@ "node_modules/@mui/material": { "version": "7.3.7", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/core-downloads-tracker": "^7.3.7", @@ -1331,7 +1327,6 @@ "node_modules/@mui/system": { "version": "7.3.7", "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.28.4", "@mui/private-theming": "^7.3.7", @@ -2376,7 +2371,6 @@ "version": "24.5.2", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.12.0" } @@ -2392,7 +2386,6 @@ "node_modules/@types/react": { "version": "18.3.27", "license": "MIT", - "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.2.2" @@ -2483,7 +2476,6 @@ "integrity": "sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==", "dev": true, "license": "BSD-2-Clause", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "7.18.0", "@typescript-eslint/types": "7.18.0", @@ -2764,7 +2756,6 @@ "version": "8.15.0", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -3121,7 +3112,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001800", @@ -3565,8 +3555,7 @@ }, "node_modules/dayjs": { "version": "1.11.13", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", @@ -3965,7 +3954,6 @@ "version": "8.57.1", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.6.1", @@ -4778,7 +4766,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "@babel/runtime": "^7.27.6" }, @@ -5590,7 +5577,6 @@ "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-5.19.0.tgz", "integrity": "sha512-REhYUN8gNP3HlcIZS6QU2uy8iovl31cXsrNDkCcqWSQbCkcpdYLczqDz5PVIwNH42UQNyvukjes/RoHPDrOUmQ==", "license": "BSD-3-Clause", - "peer": true, "dependencies": { "@mapbox/geojson-rewind": "^0.5.2", "@mapbox/jsonlint-lines-primitives": "^2.0.2", @@ -6372,7 +6358,6 @@ "node_modules/react-dom": { "version": "18.3.1", "license": "MIT", - "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -6384,7 +6369,6 @@ "node_modules/react-hook-form": { "version": "7.71.1", "license": "MIT", - "peer": true, "engines": { "node": ">=18.0.0" }, @@ -6588,8 +6572,7 @@ }, "node_modules/redux": { "version": "5.0.1", - "license": "MIT", - "peer": true + "license": "MIT" }, "node_modules/redux-persist": { "version": "6.0.0", @@ -6739,7 +6722,6 @@ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", "license": "MIT", - "peer": true, "dependencies": { "@types/estree": "1.0.9" }, @@ -7581,7 +7563,6 @@ "version": "5.9.3", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -7719,7 +7700,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", diff --git a/client/package.json b/client/package.json index 6b2a9e704..ee290cae8 100644 --- a/client/package.json +++ b/client/package.json @@ -1,7 +1,7 @@ { "name": "client", "private": true, - "version": "3.9.2", + "version": "3.10.0", "type": "module", "scripts": { "dev": "vite", diff --git a/client/public/config.js b/client/public/config.js new file mode 100644 index 000000000..30686d2e0 --- /dev/null +++ b/client/public/config.js @@ -0,0 +1,4 @@ +// Fallback so /config.js always resolves (e.g. under the Vite dev server). In +// the Docker image the server shadows this path and renders the real config +// from its CLIENT_CONFIG_* environment variables. +window.__CHECKMATE_CONFIG__ = {}; diff --git a/client/src/Components/design-elements/BaseChart.tsx b/client/src/Components/design-elements/BaseChart.tsx index c6d8298a7..46abe5d39 100644 --- a/client/src/Components/design-elements/BaseChart.tsx +++ b/client/src/Components/design-elements/BaseChart.tsx @@ -13,6 +13,7 @@ type BaseChartProps = React.PropsWithChildren<{ width?: number | string; maxWidth?: number | string; padding?: number | string | ResponsiveStyleValue; + onClick?: () => void; }>; export const BaseChart = ({ @@ -21,16 +22,22 @@ export const BaseChart = ({ width = "100%", maxWidth = "100%", padding, + onClick, }: BaseChartProps) => { const theme = useTheme(); return ( {title} diff --git a/client/src/Components/inputs/LanguageSelector.tsx b/client/src/Components/inputs/LanguageSelector.tsx index a83f3e0fa..2798e0539 100644 --- a/client/src/Components/inputs/LanguageSelector.tsx +++ b/client/src/Components/inputs/LanguageSelector.tsx @@ -13,7 +13,9 @@ export const languageNames: Record = { ca: "Catalร ", es: "Espaรฑol", fr: "Franรงais", + it: "Italiano", "pt-BR": "Portuguรชs (BR)", + pl: "Polski", cs: "ฤŒeลกtina", fi: "Suomi", tr: "Tรผrkรงe", diff --git a/client/src/Components/monitors/charts/ChartUtils.ts b/client/src/Components/monitors/charts/ChartUtils.ts new file mode 100644 index 000000000..fb9d1779c --- /dev/null +++ b/client/src/Components/monitors/charts/ChartUtils.ts @@ -0,0 +1,33 @@ +export const computeYAxisCap = (values: number[]): number | undefined => { + const sorted = values.slice().sort((a, b) => a - b); + if (sorted.length < 2) return undefined; + const p95 = sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.95))]; + return p95 > 0 ? Math.ceil(p95 * 2) : undefined; +}; + +export const PHASE_KEYS = [ + "avgDns", + "avgTcp", + "avgTls", + "avgRequest", + "avgFirstByte", + "avgDownload", +] as const; + +export const PHASE_COLOR_KEYS = { + avgDns: "dns", + avgTcp: "tcp", + avgTls: "tls", + avgRequest: "request", + avgFirstByte: "firstByte", + avgDownload: "download", +} as const; + +export const PHASE_LABELS: Record<(typeof PHASE_KEYS)[number], string> = { + avgDns: "DNS", + avgTcp: "TCP", + avgTls: "TLS", + avgRequest: "Request", + avgFirstByte: "First byte", + avgDownload: "Download", +}; diff --git a/client/src/Components/monitors/charts/HistogramDetails.tsx b/client/src/Components/monitors/charts/HistogramDetails.tsx index bd78f3289..0736abd6c 100644 --- a/client/src/Components/monitors/charts/HistogramDetails.tsx +++ b/client/src/Components/monitors/charts/HistogramDetails.tsx @@ -1,53 +1,39 @@ +import Stack from "@mui/material/Stack"; import { BaseChart, BaseBox } from "@/Components/design-elements"; import { Clock } from "lucide-react"; +import { Dot } from "@/Components/design-elements"; import { AreaChart, Area, XAxis, + YAxis, Tooltip, CartesianGrid, ResponsiveContainer, - Text, } from "recharts"; import type { TooltipProps } from "recharts"; import type { NameType, ValueType } from "recharts/types/component/DefaultTooltipContent"; import Typography from "@mui/material/Typography"; - +import prettyMilliseconds from "pretty-ms"; +import { XTick } from "@/Components/monitors/charts/XTick"; import { useSelector } from "react-redux"; -import { - formatDateWithTz, - tickDateFormatLookup, - tooltipDateFormatLookup, -} from "@/Utils/TimeUtils"; +import { formatDateWithTz, tooltipDateFormatLookup } from "@/Utils/TimeUtils"; import { useTheme } from "@mui/material/styles"; import type { Theme } from "@mui/material/styles"; -import type { GroupedCheck } from "@/Types/Check"; +import type { GroupedCheck, GroupedUptimeCheck } from "@/Types/Check"; import type { RootState } from "@/Types/state"; +import { computeYAxisCap } from "@/Components/monitors/charts/ChartUtils"; -type XTickProps = { - x: number; - y: number; - payload: { value: string }; - range: string; -}; +import { useMemo, useState } from "react"; +import { SPACING } from "@/Utils/Theme/constants"; +import { + PHASE_COLOR_KEYS, + PHASE_KEYS, + PHASE_LABELS, +} from "@/Components/monitors/charts/ChartUtils"; -export const XTick = ({ x, y, payload, range }: XTickProps) => { - const format = tickDateFormatLookup(range); - const theme = useTheme(); - const uiTimezone = useSelector((state: RootState) => state.ui.timezone); - return ( - - {formatDateWithTz(payload?.value, format, uiTimezone)} - - ); -}; +type DetailsCheck = GroupedCheck & + Partial>; type ResponseTimeToolTipProps = TooltipProps & { range: string; @@ -68,12 +54,59 @@ const ResponseTimeToolTip = ({ if (!active) return null; const format = tooltipDateFormatLookup(range); - console.log(payload); - const responseTime = Math.floor(payload[0]?.payload?.originalAvgResponseTime || 0); + const responseTime = Math.floor(payload[0]?.payload?.avgResponseTime || 0); return ( {formatDateWithTz(String(label), format, uiTimezone)} - Response time: {responseTime} ms + + Response time:{" "} + {prettyMilliseconds(responseTime, { formatSubMilliseconds: true, compact: true })} + + + ); +}; + +type TimingPhaseToolTipProps = TooltipProps & { + range: string; + theme: Theme; + uiTimezone: string; +}; + +const TimingPhasesToolTip = ({ + active, + payload, + range, + theme, + uiTimezone, +}: TimingPhaseToolTipProps) => { + if (!active || !payload?.length) return null; + const bucket = payload[0].payload as GroupedUptimeCheck; + const total = PHASE_KEYS.reduce((sum, key) => sum + bucket[key], 0); + const format = tooltipDateFormatLookup(range); + return ( + + {formatDateWithTz(bucket.bucketDate, format, uiTimezone)} + Total: {prettyMilliseconds(total)} + {PHASE_KEYS.map((key) => ( + + + + {PHASE_LABELS[key]}:{" "} + {prettyMilliseconds(bucket[key], { + formatSubMilliseconds: true, + compact: true, + })} + + + ))} ); }; @@ -82,11 +115,25 @@ export const HistogramDetails = ({ checks, range, }: { - checks: GroupedCheck[]; + checks: DetailsCheck[]; range: string; }) => { + const [showTimingPhases, setShowTimingPhases] = useState(false); const theme = useTheme(); const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + const yMax = useMemo(() => { + const totals = checks.map((check) => { + const phaseSum = PHASE_KEYS.reduce((sum, key) => sum + (check[key] ?? 0), 0); + return Math.max(check.avgResponseTime ?? 0, phaseSum); + }); + return computeYAxisCap(totals); + }, [checks]); + + const hasTimingPhases = useMemo( + () => (checks ?? []).some((check) => PHASE_KEYS.some((key) => (check[key] ?? 0) > 0)), + [checks] + ); + return ( } - title="Response times" + title={showTimingPhases ? "Timing Phases" : "Response Time"} + onClick={() => { + if (!hasTimingPhases) return; + setShowTimingPhases((prev) => !prev); + }} > + {PHASE_KEYS.map((key) => ( + + + + + ))} + - ( - - )} - /> - + {showTimingPhases ? ( + ( + + )} + /> + ) : ( + ( + + )} + /> + )} + {!showTimingPhases && ( + + )} + {hasTimingPhases && + showTimingPhases && + PHASE_KEYS.map((key) => { + return ( + + ); + })} diff --git a/client/src/Components/monitors/charts/HistogramDetailsStacked.tsx b/client/src/Components/monitors/charts/HistogramDetailsStacked.tsx new file mode 100644 index 000000000..2ef74f56b --- /dev/null +++ b/client/src/Components/monitors/charts/HistogramDetailsStacked.tsx @@ -0,0 +1,207 @@ +import type { GroupedUptimeCheck } from "@/Types/Check"; +import type { TooltipProps } from "recharts"; +import type { NameType, ValueType } from "recharts/types/component/DefaultTooltipContent"; +import type { Theme } from "@mui/material/styles"; +import type { RootState } from "@/Types/state"; +import { Clock } from "lucide-react"; +import prettyMilliseconds from "pretty-ms"; + +import Stack from "@mui/material/Stack"; +import { + Area, + AreaChart, + CartesianGrid, + ResponsiveContainer, + YAxis, + XAxis, + Tooltip, +} from "recharts"; +import { BaseBox, BaseChart, Dot } from "@/Components/design-elements"; +import Typography from "@mui/material/Typography"; +import { XTick } from "@/Components/monitors/charts/XTick"; + +import { formatDateWithTz, tooltipDateFormatLookup } from "@/Utils/TimeUtils"; +import { useSelector } from "react-redux"; +import { useMemo } from "react"; +import { useTheme } from "@mui/material"; +import { SPACING } from "@/Utils/Theme/constants"; +import { computeYAxisCap } from "@/Components/monitors/charts/ChartUtils"; + +export const PHASE_KEYS = [ + "avgDns", + "avgTcp", + "avgTls", + "avgRequest", + "avgFirstByte", + "avgDownload", +] as const; + +const PHASE_COLOR_KEYS = { + avgDns: "dns", + avgTcp: "tcp", + avgTls: "tls", + avgRequest: "request", + avgFirstByte: "firstByte", + avgDownload: "download", +} as const; + +const PHASE_LABELS: Record<(typeof PHASE_KEYS)[number], string> = { + avgDns: "DNS", + avgTcp: "TCP", + avgTls: "TLS", + avgRequest: "Request", + avgFirstByte: "First byte", + avgDownload: "Download", +}; + +type TimingPhaseToolTipProps = TooltipProps & { + range: string; + theme: Theme; + uiTimezone: string; +}; + +const TimingPhasesToolTip = ({ + active, + payload, + range, + theme, + uiTimezone, +}: TimingPhaseToolTipProps) => { + if (!active || !payload?.length) return null; + const bucket = payload[0].payload as GroupedUptimeCheck; + const total = PHASE_KEYS.reduce((sum, key) => sum + bucket[key], 0); + const format = tooltipDateFormatLookup(range); + return ( + + {formatDateWithTz(bucket.bucketDate, format, uiTimezone)} + Total: {prettyMilliseconds(total)} + {PHASE_KEYS.map((key) => ( + + + + {PHASE_LABELS[key]}:{" "} + {prettyMilliseconds(bucket[key], { + formatSubMilliseconds: true, + compact: true, + })} + + + ))} + + ); +}; + +export const HistogramDetailsStacked = ({ + checks = [], + range, +}: { + checks: GroupedUptimeCheck[]; + range: string; +}) => { + const theme = useTheme(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + + // Cap the y domain at 2x the p95 of bucket totals, so typical stacks sit + // around mid-height with headroom above + const yMax = useMemo(() => { + const totals = checks.map((check) => + PHASE_KEYS.reduce((sum, key) => sum + check[key], 0) + ); + return computeYAxisCap(totals); + }, [checks]); + + return ( + + } + title="Timing phases" + > + + + + + {PHASE_KEYS.map((key) => ( + + + + + ))} + + + ( + + )} + /> + ( + + )} + /> + {PHASE_KEYS.map((key) => ( + + ))} + + + + ); +}; diff --git a/client/src/Components/monitors/charts/HistogramInfrastructure.tsx b/client/src/Components/monitors/charts/HistogramInfrastructure.tsx index 8dd15ae03..8163898aa 100644 --- a/client/src/Components/monitors/charts/HistogramInfrastructure.tsx +++ b/client/src/Components/monitors/charts/HistogramInfrastructure.tsx @@ -8,7 +8,7 @@ import { ResponsiveContainer, } from "recharts"; import { Fragment, useId } from "react"; -import { XTick } from "@/Components/monitors"; +import { XTick } from "@/Components/monitors/charts/XTick"; import { useTheme } from "@mui/material/styles"; import type { HardwareCheckStats } from "@/Types/Monitor"; diff --git a/client/src/Components/monitors/charts/HistogramPageSpeedDetails.tsx b/client/src/Components/monitors/charts/HistogramPageSpeedDetails.tsx index f89994437..22001602c 100644 --- a/client/src/Components/monitors/charts/HistogramPageSpeedDetails.tsx +++ b/client/src/Components/monitors/charts/HistogramPageSpeedDetails.tsx @@ -1,6 +1,6 @@ import { BaseChart } from "@/Components/design-elements"; import { TrendingUp } from "lucide-react"; -import { XTick } from "@/Components/monitors"; +import { XTick } from "@/Components/monitors/charts/XTick"; import { XAxis, AreaChart, diff --git a/client/src/Components/monitors/charts/RadialAvgResponse.tsx b/client/src/Components/monitors/charts/RadialAvgResponse.tsx index 9a88fb42c..dccb18984 100644 --- a/client/src/Components/monitors/charts/RadialAvgResponse.tsx +++ b/client/src/Components/monitors/charts/RadialAvgResponse.tsx @@ -20,7 +20,7 @@ export const RadialAvgResponse = ({ avg, max }: { avg: number; max: number }) => const msg: Record = { success: "Excellent", warning: "Average", - danger: "Poor", + error: "Poor", }; return ( diff --git a/client/src/Components/monitors/charts/XTick.tsx b/client/src/Components/monitors/charts/XTick.tsx new file mode 100644 index 000000000..9cb57ffab --- /dev/null +++ b/client/src/Components/monitors/charts/XTick.tsx @@ -0,0 +1,30 @@ +import { formatDateWithTz, tickDateFormatLookup } from "@/Utils/TimeUtils"; +import { useSelector } from "react-redux"; +import { Text } from "recharts"; +import { useTheme } from "@mui/material"; +import type { RootState } from "@/Types/state"; + +type XTickProps = { + x: number; + y: number; + payload: { value: string }; + range: string; +}; + +export const XTick = ({ x, y, payload, range }: XTickProps) => { + const format = tickDateFormatLookup(range); + const theme = useTheme(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + return ( + + {formatDateWithTz(payload?.value, format, uiTimezone)} + + ); +}; diff --git a/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx b/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx index e23f24e53..d41bf3ca3 100644 --- a/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx +++ b/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx @@ -10,8 +10,9 @@ import { useInviteForm } from "@/Hooks/useInviteForm"; import type { InviteFormData } from "@/Validation/invite"; import { usePost } from "@/Hooks/UseApi"; import { LAYOUT } from "@/Utils/Theme/constants"; +import { runtimeConfig } from "@/Utils/runtimeConfig"; -const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST; +const CLIENT_HOST = runtimeConfig.clientHost || import.meta.env.VITE_APP_CLIENT_HOST; interface InviteResponse { token: string; @@ -56,7 +57,7 @@ export const InviteTeamMemberDialog = ({ const result = await generateToken("/invite", data); if (result?.data?.token) { const token = result.data.token; - const link = CLIENT_HOST ? `${CLIENT_HOST}/register/${token}` : token; + const link = `${CLIENT_HOST || window.location.origin}/register/${token}`; setInviteLink(link); } }; diff --git a/client/src/Pages/Infrastructure/Details/Components/ChartsNetwork.tsx b/client/src/Pages/Infrastructure/Details/Components/ChartsNetwork.tsx index c7ebac5be..c84a327fa 100644 --- a/client/src/Pages/Infrastructure/Details/Components/ChartsNetwork.tsx +++ b/client/src/Pages/Infrastructure/Details/Components/ChartsNetwork.tsx @@ -63,10 +63,20 @@ const getChartConfigs = ( return configs; }; -const hasAnyNetworkTraffic = (checks: HardwareCheckStats[]): boolean => - checks.some((c) => - c.net?.some((iface) => iface.bytesSentPerSecond > 0 || iface.deltaBytesRecv > 0) - ); +// Loopback / bridge names Capture sees when it can only observe the container's +// own network namespace. When ALL reported interfaces match this set we assume +// the "no real interfaces visible" case and surface the notice. +const CONTAINER_ONLY_IFACE_NAMES = new Set(["lo", "lo0", "docker0"]); + +const onlyContainerVisibleInterfaces = (checks: HardwareCheckStats[]): boolean => { + const named = new Set(); + checks.forEach((c) => c.net?.forEach((iface) => named.add(iface.name))); + if (named.size === 0) return false; + for (const name of named) { + if (!CONTAINER_ONLY_IFACE_NAMES.has(name)) return false; + } + return true; +}; export const InfraNetworkCharts = ({ checks, @@ -82,13 +92,14 @@ export const InfraNetworkCharts = ({ () => getChartConfigs(theme, checks, t), [theme, checks, t] ); - const showNoTrafficNotice = chartConfigs.length > 0 && !hasAnyNetworkTraffic(checks); + const showLoopbackOnlyNotice = + chartConfigs.length > 0 && onlyContainerVisibleInterfaces(checks); return ( - {showNoTrafficNotice && ( + {showLoopbackOnlyNotice && ( - {t("pages.infrastructure.charts.labels.netNoTraffic")} + {t("pages.infrastructure.charts.labels.loopbackOnlyNotice")} )} { // Public route: render directly on the viewport, themed background covers everything. if (isPublic) { const themeConfig = THEME_CONFIGS[resolveStatusPageTheme(statusPage.theme)]; + const customCss = + statusPage.customCSS && !cssReferencesExternalResource(statusPage.customCSS) + ? statusPage.customCSS + : ""; return ( + {customCss && } { + const validOverride = brandColor && HEX_COLOR.test(brandColor) ? brandColor : undefined; + const brand = validOverride ?? tokens.brand ?? tokens.up; + const brandStrong = validOverride + ? darken(validOverride, 0.2) + : (tokens.brandStrong ?? tokens.upStrong); + const brandSoft = validOverride + ? alpha(validOverride, 0.15) + : (tokens.brandSoft ?? tokens.upSoft); + return { ...tokens, brand, brandStrong, brandSoft }; +}; + export const StatusPageThemeProvider = ({ theme, themeMode, timezone, + brandColor, paintBody = false, transparent = false, children, @@ -109,7 +141,11 @@ export const StatusPageThemeProvider = ({ const resolvedMode: ResolvedMode = resolvedThemeMode === "auto" ? systemMode : resolvedThemeMode; - const tokens = themeTokens[resolvedTheme][resolvedMode]; + const baseTokens = themeTokens[resolvedTheme][resolvedMode]; + const tokens = useMemo( + () => resolveBrandTokens(baseTokens, brandColor), + [baseTokens, brandColor] + ); const value = useMemo( () => ({ diff --git a/client/src/Pages/StatusPage/Status/themes/refined/styles.ts b/client/src/Pages/StatusPage/Status/themes/refined/styles.ts index 0cf754af6..457b86ede 100644 --- a/client/src/Pages/StatusPage/Status/themes/refined/styles.ts +++ b/client/src/Pages/StatusPage/Status/themes/refined/styles.ts @@ -119,7 +119,7 @@ export const refinedStyles = ( width: 28, height: 28, borderRadius: "8px", - background: tokens.up, + background: tokens.brand, display: "grid", placeItems: "center", color: "#fff", @@ -191,16 +191,16 @@ export const refinedStyles = ( whiteSpace: "nowrap", textAlign: "center", border: 0, - background: active ? tokens.upSoft : "transparent", + background: active ? tokens.brandSoft : "transparent", fontFamily: "inherit", fontSize: 11, padding: { xs: "5px 6px", md: "5px 14px" }, cursor: "pointer", - color: active ? tokens.up : tokens.textMuted, + color: active ? tokens.brand : tokens.textMuted, borderRadius: "5px", transition: "background 0.15s ease, color 0.15s ease", fontWeight: active ? 600 : 500, - "&:hover": { color: active ? tokens.up : tokens.text }, + "&:hover": { color: active ? tokens.brand : tokens.text }, }), monitorList: { @@ -259,9 +259,9 @@ export const refinedStyles = ( }, pillHardware: { ...pillBase, - color: tokens.up, + color: tokens.brand, border: `1px solid ${tokens.border}`, - background: tokens.upSoft, + background: tokens.brandSoft, }, monitorUrl: { fontSize: 12, @@ -382,11 +382,11 @@ export const refinedStyles = ( fontSize: 12, mt: "40px", "& a": { - color: tokens.up, + color: tokens.brand, textDecoration: "underline", textUnderlineOffset: "3px", fontWeight: 600, - "&:hover": { color: tokens.upStrong || tokens.up }, + "&:hover": { color: tokens.brandStrong }, }, }, }; diff --git a/client/src/Pages/StatusPage/Status/themes/tokens.ts b/client/src/Pages/StatusPage/Status/themes/tokens.ts index fdb251c07..3ec90abde 100644 --- a/client/src/Pages/StatusPage/Status/themes/tokens.ts +++ b/client/src/Pages/StatusPage/Status/themes/tokens.ts @@ -9,6 +9,14 @@ export interface StatusPageThemeTokens { up: string; upStrong: string; upSoft: string; + // Accent used for brand-tinted surfaces (hero background, active tab + // underline, "view all" links). Distinct from `up` so a red/orange + // brand color cannot recolor the semantic OK/up status indicators. + // Themes leave these undefined by default; the provider falls back + // to `up`/`upStrong`/`upSoft` so styles have a value to consume. + brand?: string; + brandStrong?: string; + brandSoft?: string; degraded: string; degradedSoft: string; down: string; diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx index 8828aa29a..56724259a 100644 --- a/client/src/Pages/Uptime/Details/index.tsx +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -57,7 +57,6 @@ const UptimeDetailsPage = () => { } const params = new URLSearchParams(); params.append("dateRange", dateRange); - params.append("normalize", "true"); return `/monitors/uptime/details/${monitorId}?${params.toString()}`; }, [monitorId, dateRange]); @@ -72,6 +71,7 @@ const UptimeDetailsPage = () => { ); const monitorData = monitorDetailsData?.monitorData; + const monitor = monitorData?.monitor; const monitorStats = monitorDetailsData?.monitorStats ?? null; @@ -165,10 +165,18 @@ const UptimeDetailsPage = () => { { keepPreviousData: true, revalidateOnFocus: false } ); + if (!monitorData) { + return null; + } + + if (!monitor) { + return null; + } + const geoChecksForTable = geoChecksTableData?.geoChecks ?? []; const geoChecksCount = geoChecksTableData?.geoChecksCount ?? 0; - const geoLocations = monitor?.geoCheckLocations; + const geoLocations = monitor.geoCheckLocations; const checks = checksData?.checks ?? []; const checksCount = checksData?.checksCount ?? 0; @@ -215,7 +223,7 @@ const UptimeDetailsPage = () => { /> = T & { originalResponseTime: number; }; - -export type NormalizedUptimeCheck = T & { - originalAvgResponseTime: number; -}; diff --git a/client/src/Types/Monitor.ts b/client/src/Types/Monitor.ts index fd9b521d5..b9aaf417a 100644 --- a/client/src/Types/Monitor.ts +++ b/client/src/Types/Monitor.ts @@ -1,4 +1,4 @@ -import type { GroupedCheck, CheckSnapshot } from "@/Types/Check"; +import type { GroupedCheck, CheckSnapshot, GroupedUptimeCheck } from "@/Types/Check"; import type { PageSpeedGroupedCheck } from "@/Types/Check"; import type { GeoContinent } from "@/Types/GeoCheck"; export type { GeoContinent } from "@/Types/GeoCheck"; @@ -137,7 +137,7 @@ export interface MonitorStats { export interface MonitorData { monitor: Monitor; - groupedChecks: GroupedCheck[]; + groupedChecks: GroupedUptimeCheck[]; groupedUpChecks: GroupedCheck[]; groupedDownChecks: GroupedCheck[]; groupedAvgResponseTime: number; diff --git a/client/src/Types/env.d.ts b/client/src/Types/env.d.ts index af8f983fb..0e753a580 100644 --- a/client/src/Types/env.d.ts +++ b/client/src/Types/env.d.ts @@ -3,7 +3,8 @@ declare const __APP_VERSION__: string; interface ImportMetaEnv { - readonly VITE_APP_API_V2_BASE_URL?: string; + readonly VITE_APP_API_BASE_URL?: string; + readonly VITE_APP_CLIENT_HOST?: string; readonly VITE_APP_LOG_LEVEL?: "debug" | "info" | "warn" | "error"; } diff --git a/client/src/Utils/ApiClient.ts b/client/src/Utils/ApiClient.ts index b18f2dfbc..c0468ed0e 100644 --- a/client/src/Utils/ApiClient.ts +++ b/client/src/Utils/ApiClient.ts @@ -1,8 +1,10 @@ import axios from "axios"; import type { AxiosError } from "axios"; import type { AxiosRequestConfig, AxiosResponse } from "axios"; +import { runtimeConfig } from "@/Utils/runtimeConfig"; -const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL; +const BASE_URL = + runtimeConfig.apiBaseUrl || import.meta.env.VITE_APP_API_BASE_URL || "/api/v1"; const api = axios.create({ baseURL: BASE_URL, diff --git a/client/src/Utils/Theme/Palette.ts b/client/src/Utils/Theme/Palette.ts index 55dc1e26e..5257ab92b 100644 --- a/client/src/Utils/Theme/Palette.ts +++ b/client/src/Utils/Theme/Palette.ts @@ -20,8 +20,19 @@ export const colors = { brandGreen: "#13715B", brandGreenLight: "#4DAF94", green200: "#ECF7F2", + green700: "#008300", yellow200: "#FFF4E5", + yellow500: "#EDA100", + yellow600: "#C98500", orange500: "#FFA500", + orange600: "#EB6834", + orange700: "#D95926", + blue400: "#3987E5", + blue500: "#2A78D6", + aqua500: "#1BAF7A", + aqua600: "#199E70", + magenta300: "#E87BA4", + magenta400: "#D55181", }; export const lightPalette = { @@ -38,6 +49,16 @@ export const lightPalette = { running: colors.green200, paused: colors.yellow200, }, + chart: { + phases: { + dns: colors.magenta300, + tcp: colors.orange600, + tls: colors.aqua500, + request: colors.yellow500, + firstByte: colors.blue500, + download: colors.green700, + }, + }, }; export const darkPalette = { @@ -58,4 +79,14 @@ export const darkPalette = { running: alpha(colors.brandGreen, 0.18), paused: alpha(colors.orange500, 0.18), }, + chart: { + phases: { + dns: colors.magenta400, + tcp: colors.orange700, + tls: colors.aqua600, + request: colors.yellow600, + firstByte: colors.blue500, + download: colors.green700, + }, + }, }; diff --git a/client/src/Utils/Theme/Theme.ts b/client/src/Utils/Theme/Theme.ts index 04f4c94eb..33ba66945 100644 --- a/client/src/Utils/Theme/Theme.ts +++ b/client/src/Utils/Theme/Theme.ts @@ -16,10 +16,30 @@ declare module "@mui/material/styles" { interface Palette { sidebar: { accent: string }; rowStatus: { running: string; paused: string }; + chart: { + phases: { + dns: string; + tcp: string; + tls: string; + request: string; + firstByte: string; + download: string; + }; + }; } interface PaletteOptions { sidebar?: { accent: string }; rowStatus?: { running: string; paused: string }; + chart?: { + phases: { + dns: string; + tcp: string; + tls: string; + request: string; + firstByte: string; + download: string; + }; + }; } } diff --git a/client/src/Utils/logger.ts b/client/src/Utils/logger.ts index 4a97dbecb..74be87918 100644 --- a/client/src/Utils/logger.ts +++ b/client/src/Utils/logger.ts @@ -1,4 +1,5 @@ import type { LogLevel } from "@/Types/Log"; +import { runtimeConfig } from "@/Utils/runtimeConfig"; interface LogContext { [key: string]: any; @@ -25,7 +26,8 @@ interface ErrorLogData { userAgent: string; } -const configuredLevel: LogLevel = import.meta.env.VITE_APP_LOG_LEVEL || "error"; +const configuredLevel: LogLevel = + runtimeConfig.logLevel || import.meta.env.VITE_APP_LOG_LEVEL || "error"; const configuredPriority = LOG_LEVEL_PRIORITY[configuredLevel]; const shouldLog = (level: LogLevel): boolean => { diff --git a/client/src/Utils/runtimeConfig.ts b/client/src/Utils/runtimeConfig.ts new file mode 100644 index 000000000..2d5e4be94 --- /dev/null +++ b/client/src/Utils/runtimeConfig.ts @@ -0,0 +1,18 @@ +import type { LogLevel } from "@/Types/Log"; + +// Served by GET /config.js (rendered from server env) or a static config.js +// next to index.html; loaded before the bundle in index.html. +export interface RuntimeConfig { + apiBaseUrl?: string; + clientHost?: string; + logLevel?: LogLevel; +} + +declare global { + interface Window { + __CHECKMATE_CONFIG__?: RuntimeConfig; + } +} + +export const runtimeConfig: RuntimeConfig = + (typeof window !== "undefined" && window.__CHECKMATE_CONFIG__) || {}; diff --git a/client/src/Utils/statusPageUrl.ts b/client/src/Utils/statusPageUrl.ts index d5628d6bc..65109c198 100644 --- a/client/src/Utils/statusPageUrl.ts +++ b/client/src/Utils/statusPageUrl.ts @@ -1,6 +1,9 @@ import { PUBLIC_STATUS_PAGE_PREFIX, type StatusPage } from "@/Types/StatusPage"; +import { runtimeConfig } from "@/Utils/runtimeConfig"; -const CLIENT_HOST = import.meta.env.VITE_APP_CLIENT_HOST as string | undefined; +const CLIENT_HOST = + runtimeConfig.clientHost || + (import.meta.env.VITE_APP_CLIENT_HOST as string | undefined); const getClientOriginProtocol = (): string => { const source = diff --git a/client/src/locales/de.json b/client/src/locales/de.json index e042f9d55..7b2e64945 100644 --- a/client/src/locales/de.json +++ b/client/src/locales/de.json @@ -825,7 +825,7 @@ "netBytesRecv": "{{name}} - Empfangene Bytes", "netBytesSent": "{{name}} - Gesendete Bytes", "netNoTraffic": "Capture meldet null Datenverkehr auf allen Schnittstellen. Das ist normal, wenn Capture in einem Container lรคuft, der nur Loopback-Schnittstellen sieht โ€” fรผhre Capture direkt auf dem Host aus, um echten Netzwerkverkehr zu sehen.", - "temp": "Temp." + "temp": "CPU-Temperatur" } }, "gauges": { @@ -841,13 +841,13 @@ }, "memory": { "lowerLabel": "Frei", - "title": "Arbeitsspeicherauslastung", + "title": "Speicherauslastung", "upperLabel": "Belegt" } }, "statBoxes": { - "avgCpuTemperature": "Durchschn. CPU Termperatur", - "cpuFrequency": "CPU Frequenz", + "avgCpuTemperature": "Durchschn. CPU-Temperatur", + "cpuFrequency": "CPU-Frequenz", "cpuLogical": "CPU (logisch)", "cpuPhysical": "CPU (physisch)", "disk": "Festplatte", diff --git a/client/src/locales/en.json b/client/src/locales/en.json index c320c5767..d940f95fc 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -844,8 +844,8 @@ "memory": "Memory usage", "netBytesRecv": "{{name}} - Bytes Received", "netBytesSent": "{{name}} - Bytes Sent", - "netNoTraffic": "Capture is reporting zero traffic on every interface. This is normal when Capture runs inside a container that can only see loopback interfaces โ€” run Capture directly on the host to see real network activity.", - "temp": "Temp" + "loopbackOnlyNotice": "Capture is only reporting loopback or container-bridge interfaces (lo, lo0, docker0). This is normal when Capture runs inside a container that can only see its own network namespace โ€” run Capture directly on the host to see real network activity.", + "temp": "CPU temperature" } }, "gauges": { diff --git a/client/src/locales/it.json b/client/src/locales/it.json new file mode 100644 index 000000000..cabeb94ef --- /dev/null +++ b/client/src/locales/it.json @@ -0,0 +1,1662 @@ +{ + "common": { + "auth": { + "roles": { + "admin": "Amministratore", + "demo": "Demo", + "superadmin": "Super amministratore", + "user": "Utente" + } + }, + "alerts": { + "pageSpeedApiKey": { + "content": "Attenzione: non hai ancora aggiunto una chiave API di Google PageSpeed. Vai su Impostazioni per aggiungerne una. Senza di essa, il monitor PageSpeed non funzionerร ." + } + }, + "appName": "Checkmate", + "statusCode": { + "down": "Non attivo", + "networkError": { + "timeout": "Il server non ha risposto in tempo (richiesta scaduta).", + "dns": "Impossibile risolvere l'hostname (risoluzione DNS fallita).", + "refused": "Il server ha rifiutato la connessione (porta chiusa o servizio non in ascolto).", + "tls": "Handshake TLS o SSL fallito (problema di certificato o crittografia).", + "reset": "La connessione รจ stata reimpostata prima di ricevere una risposta.", + "unknown": "Nessuna risposta HTTP ricevuta dal target." + } + }, + "breadcrumbs": { + "details": "Dettagli", + "home": "Home" + }, + "buttons": { + "addMember": "Aggiungi membro", + "cancel": "Annulla", + "close": "Chiudi", + "configure": "Configura", + "confirm": "Conferma", + "create": "Crea", + "delete": "Elimina", + "generateToken": "Genera token", + "incidents": "Incidenti", + "inviteMember": "Invita membro", + "pause": "Sospendi", + "resume": "Riprendi", + "save": "Salva", + "next": "Avanti", + "back": "Indietro", + "sendInvite": "Invia invito", + "test": "Prova", + "testNotifications": "Prova notifiche", + "toggleTheme": "Alterna tema chiaro e scuro", + "flushQueue": "Svuota coda", + "notFound": "Vai alla dashboard principale", + "resetPassword": "Reimposta password", + "reset": "Reimposta", + "clear": "Cancella", + "addDemo": "Aggiungi monitor demo", + "removeMonitors": "Rimuovi monitor", + "sendTestEmail": "Invia email di test", + "exportToJSON": "Esporta in JSON", + "importFromJSON": "Importa da JSON", + "clearFilters": "Cancella filtri", + "removeUser": "Rimuovi utente" + }, + "charts": { + "labels": { + "averageResponseTime": "Tempo di risposta medio", + "downtime": "Downtime", + "high": "Alto", + "low": "Basso", + "uptime": "Uptime" + }, + "histogram": { + "avg": "Media: {{value}} ms", + "max": "Max: {{value}} ms" + } + }, + "dialogs": { + "delete": { + "description": "Questa azione non puรฒ essere annullata.", + "title": "Sei sicuro di voler eliminarlo?" + } + }, + "labels": { + "active": "Attivo", + "running": "In esecuzione", + "paused": "In pausa", + "idle": "Inattivo", + "na": "N/A", + "resolved": "Risolto", + "responseTime": "Tempo di risposta" + }, + "form": { + "role": { + "option": { + "role": { + "label": "Ruoli" + } + } + }, + "name": { + "option": { + "firstName": { + "label": "Nome", + "placeholder": "Inserisci il tuo nome" + }, + "lastName": { + "label": "Cognome", + "placeholder": "Inserisci il tuo cognome" + } + } + }, + "email": { + "option": { + "email": { + "label": "Email", + "placeholder": "you@example.com" + } + } + } + }, + "table": { + "empty": "Non c'รจ ancora nulla da mostrare", + "headers": { + "actions": "Azioni", + "dateTime": "Data e ora", + "message": "Messaggio", + "monitor": "Monitor", + "monitorId": "ID monitor", + "name": "Nome", + "status": "Stato", + "type": "Tipo", + "url": "URL", + "interval": "Intervallo", + "active": "Attivo", + "responseTime": "Tempo di risposta", + "tags": "Tag" + } + } + }, + "components": { + "pageHeader": { + "links": { + "support": "Supporto", + "discussions": "Discussioni", + "docs": "Documentazione", + "changelog": "Changelog" + } + }, + "imageUpload": { + "clickToUpload": "Clicca per caricare", + "dragAndDrop": "o trascina e rilascia", + "supportedFormats": "Formati supportati", + "maxSize": "Dimensione massima", + "orDragAndDrop": "o trascina e rilascia", + "errors": { + "invalidFileSize": "La dimensione del file รจ troppo grande!", + "invalidFileFormat": "Formato file non supportato!" + } + }, + "headerStatusPageControls": { + "publicLink": "Link pubblico" + }, + "headerTimeRange": { + "labels": { + "day": "Giorno", + "month": "Mese", + "recent": "Recente", + "week": "Settimana" + }, + "description": { + "recent": "Mostra le statistiche delle ultime 2 ore.", + "day": "Mostra le statistiche delle ultime 24 ore.", + "week": "Mostra le statistiche degli ultimi 7 giorni.", + "month": "Mostra le statistiche degli ultimi 30 giorni." + } + }, + "offlineBanner": { + "serverUnreachable": "Impossibile raggiungere il server", + "retry": "Riprova", + "retrying": "Nuovo tentativo...", + "reconnected": "Connessione ripristinata" + }, + "sidebar": { + "menu": { + "uptime": "Uptime", + "pagespeed": "Pagespeed", + "infrastructure": "Infrastruttura", + "notifications": "Notifiche", + "tags": "Tag", + "checks": "Verifiche", + "incidents": "Incidenti", + "statusPages": "Pagine di stato", + "maintenance": "Manutenzione", + "logs": "Log", + "settings": "Impostazioni" + }, + "bottomMenu": { + "support": "Supporto", + "discussions": "Discussioni", + "docs": "Docs", + "changelog": "Changelog" + }, + "accountMenu": { + "profile": "Profilo", + "password": "Password", + "team": "Team" + }, + "starPrompt": { + "title": "Metti una stella a Checkmate", + "description": "Scopri le ultime release e aiuta a far crescere la community su GitHub" + }, + "authFooter": { + "navControls": "Controlli", + "logOut": "Esci", + "roles": { + "superAdmin": "Super amministratore", + "admin": "Amministratore", + "user": "Utente", + "demoUser": "Utente demo" + } + } + }, + "starPrompt": { + "title": "Metti una stella a Checkmate", + "description": "Scopri le ultime release e aiuta a far crescere la community su GitHub" + } + }, + "pages": { + "notFound": { + "title": "Oh no! Hai fatto cadere il tuo sushi!", + "subtitle": "L'URL non esiste, oppure non hai accesso." + }, + "account": { + "tabs": { + "profile": "Profilo", + "password": "Password", + "team": "Team" + }, + "form": { + "name": { + "title": "Nome", + "description": "Aggiorna le tue informazioni personali" + }, + "photo": { + "title": "Foto profilo", + "description": "Carica una foto profilo" + }, + "currentPassword": { + "title": "Password attuale", + "description": "Inserisci la tua password attuale per verificare la tua identitร ", + "option": { + "label": "Password attuale", + "placeholder": "Inserisci la password attuale" + } + }, + "newPassword": { + "title": "Nuova password", + "description": "Scegli una password sicura con almeno 8 caratteri", + "option": { + "newPassword": { + "label": "Nuova password", + "placeholder": "Inserisci la nuova password" + }, + "confirm": { + "label": "Conferma password", + "placeholder": "Conferma la nuova password" + } + } + }, + "deleteAccount": { + "title": "Elimina account", + "description": "Questa azione รจ permanente e non puรฒ essere annullata" + } + }, + "team": { + "fallback": { + "title": "Nessun membro del team", + "description": "Invita compagni di team via email o aggiungili direttamente. Una volta iscritti, appariranno qui con il loro ruolo." + }, + "filter": { + "placeholder": "Filtra per ruolo", + "all": "Tutti i ruoli", + "admin": "Amministratore", + "member": "Membro" + }, + "table": { + "headers": { + "email": "Email", + "role": "Ruolo", + "created": "Creato il" + } + }, + "addMember": { + "title": "Registra un nuovo membro del team", + "description": "Crea un nuovo account utente. Condividi le credenziali in modo sicuro dopo la creazione." + }, + "invite": { + "title": "Invita un membro del team", + "description": "Invia un invito per unirsi al tuo team", + "email": { + "label": "Indirizzo email", + "placeholder": "Inserisci l'indirizzo email" + }, + "role": { + "label": "Ruolo", + "placeholder": "Seleziona un ruolo" + }, + "linkLabel": "Link di invito" + } + }, + "header": { + "title": "Account", + "description": "Gestisci il tuo profilo, la password, il team e le preferenze dell'area di lavoro." + } + }, + "auth": { + "brandPanel": { + "eyebrow": "Monitoraggio infrastrutturale open source", + "tagline": "Tieni d'occhio ogni mossa dei tuoi server.", + "description": "Uptime in tempo reale, tempo di risposta e tracciamento degli incidenti โ€” self-hosted e tuo per sempre." + }, + "common": { + "passwordRules": { + "length": { + "beginning": "Deve essere lunga almeno", + "highlighted": "8 caratteri" + }, + "lowercase": { + "beginning": "Deve contenere almeno", + "highlighted": "un carattere minuscolo" + }, + "match": { + "beginning": "Le password", + "highlighted": "devono corrispondere" + }, + "number": { + "beginning": "Deve contenere almeno", + "highlighted": "un numero" + }, + "special": { + "beginning": "Deve contenere almeno", + "highlighted": "un carattere speciale (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)" + }, + "uppercase": { + "beginning": "Deve contenere almeno", + "highlighted": "un carattere maiuscolo" + } + }, + "form": { + "option": { + "email": { + "label": "Email", + "placeholder": "me@example.com" + }, + "password": { + "label": "Password", + "placeholder": "********" + }, + "confirmPassword": { + "label": "Conferma password" + } + } + } + }, + "login": { + "title": "Bentornato su Checkmate!", + "subtitle": "Accedi per continuare", + "submit": "Accedi", + "links": { + "forgotPassword": { + "text": "Password dimenticata?", + "linkText": "Reimposta password" + }, + "register": { + "text": "Non hai un account?", + "linkText": "Registrati qui" + } + } + }, + "register": { + "title": "Bentornato su Checkmate!", + "subtitle": "Registrati per iniziare", + "setupNotice": "Questa pagina รจ disponibile solo durante la configurazione iniziale. Una volta registrato un super amministratore, gli altri utenti possono essere invitati dalle impostazioni del team.", + "submit": "Registrati" + }, + "forgotPassword": { + "title": "Password dimenticata?", + "subtitle": "Nessun problema, ti invieremo le istruzioni per il ripristino.", + "submit": "Richiedi recupero", + "links": { + "login": { + "text": "Torna al", + "linkText": "login" + } + } + }, + "setNewPassword": { + "title": "Reimposta la tua password", + "subtitle": "La tua nuova password deve essere diversa dalle password usate in precedenza." + } + }, + "checks": { + "fallback": { + "title": "Nessuna verifica", + "description": "Una volta creato un monitor, ogni sonda di stato e tempo di risposta apparirร  qui come una verifica." + }, + "selects": { + "monitor": { + "all": "Tutti i monitor" + }, + "status": { + "all": "Tutti", + "down": "Non attivo", + "up": "Attivo" + } + }, + "table": { + "empty": "Nessuna verifica fallita in questo intervallo di tempo", + "headers": { + "statusCode": "Codice di stato", + "location": "Posizione" + } + }, + "header": { + "title": "Verifiche", + "description": "Ogni sonda che Checkmate esegue sui tuoi monitor arriva qui, con codice di stato e tempo di risposta." + } + }, + "common": { + "monitors": { + "actions": { + "configure": "Configura", + "delete": "Elimina", + "generateToken": "Genera token", + "details": "Dettagli", + "incidents": "Incidenti", + "inviteMember": "Invita membro", + "openSite": "Apri sito", + "pause": "Sospendi", + "resume": "Riprendi", + "bulkSelected_one": "{{count}} monitor selezionato", + "bulkSelected_other": "{{count}} monitor selezionati", + "selectAll": "Seleziona tutti i monitor", + "selectMonitor": "Seleziona {{name}}" + }, + "bulkPause": { + "alreadyPaused_one": "Il monitor selezionato รจ giร  in pausa", + "alreadyPaused_other": "Tutti i monitor selezionati sono giร  in pausa", + "alreadyRunning_one": "Il monitor selezionato รจ giร  in esecuzione", + "alreadyRunning_other": "Tutti i monitor selezionati sono giร  in esecuzione", + "paused_one": "{{count}} monitor messo in pausa", + "paused_other": "{{count}} monitor messi in pausa", + "resumed_one": "{{count}} monitor ripreso", + "resumed_other": "{{count}} monitor ripresi", + "partialFailure": "{{successCount}} monitor aggiornati, ma {{failedCount}} non sono riusciti a sincronizzarsi con la coda dei job" + }, + "statBoxes": { + "activeFor": "Attivo da", + "certificateExpiry": "Scadenza certificato", + "lastCheck": "Ultima verifica", + "lastResponseTime": "Ultimo tempo di risposta", + "serviceIsDown": "Servizio non attivo" + }, + "status": { + "down": "non attivo", + "breached": "soglia superata", + "initializing": "inizializzazione", + "maintenance": "manutenzione", + "paused": "in pausa", + "total": "totale", + "up": "attivo" + }, + "monitorTypes": { + "optionDocker": "Docker", + "optionGame": "Game", + "optionHttp": "HTTP(S)", + "optionPing": "Ping", + "optionPort": "Porta", + "optionPagespeed": "PageSpeed", + "optionHardware": "Infrastruttura", + "optionGrpc": "gRPC", + "optionWebSocket": "WebSocket", + "optionDns": "DNS" + } + } + }, + "createMonitor": { + "form": { + "advanced": { + "description": "Impostazioni opzionali per casi d'uso avanzati", + "option": { + "method": { + "label": "Metodo di richiesta", + "description": { + "GET": "GET recupera la risposta completa, incluso il corpo. Il matching avanzato รจ completamente supportato.", + "HEAD": "HEAD recupera solo gli header della risposta, riducendo il carico sul target. Supporta il matching sul codice di stato ma non il matching avanzato, poichรฉ non c'รจ un corpo." + } + }, + "advancedMatching": { + "label": "Usa matching avanzato" + }, + "expectedValue": { + "label": "Valore atteso" + }, + "jsonPath": { + "description": "Questa espressione verrร  valutata sui dati JSON della risposta e il risultato sarร  usato per il matching con il valore atteso. Consulta jmespath.org per la documentazione del linguaggio di query.", + "label": "Espressione JSONPath" + }, + "matchMethod": { + "label": "Metodo di matching", + "equal": "Uguale", + "include": "Include", + "regex": "Regex" + }, + "customUpCodes": { + "label": "Codici di stato accettati", + "description": "Aggiungi qualsiasi codice non-2xx che debba essere considerato riuscito.", + "removeAriaLabel": "Rimuovi codice di stato" + } + }, + "title": "Impostazioni avanzate" + }, + "frequency": { + "description": "Con quale frequenza vuoi verificare lo stato di questo monitor?", + "option": { + "frequency": { + "label": "Frequenza di verifica", + "value": { + "fifteenMinutes": "15 minuti", + "fifteenSeconds": "15 secondi", + "fiveMinutes": "5 minuti", + "fourMinutes": "4 minuti", + "oneMinute": "1 minuto", + "tenMinutes": "10 minuti", + "thirtyMinutes": "30 minuti", + "thirtySeconds": "30 secondi", + "threeMinutes": "3 minuti", + "twoMinutes": "2 minuti" + } + } + }, + "title": "Frequenza di verifica" + }, + "general": { + "option": { + "container": { + "label": "Nome/ID container", + "placeholder": "my-app o abcd1234" + }, + "host": { + "label": "Host", + "placeholder": "192.168.1.100 o example.com" + }, + "name": { + "label": "Nome visualizzato", + "placeholder": "es. Il mio sito web" + }, + "secret": { + "label": "Segreto di autorizzazione", + "placeholder": "Inserisci la tua chiave segreta" + }, + "url": { + "label": "URL", + "placeholder": "https://www.google.com" + }, + "game": { + "label": "Scegli gioco", + "placeholder": "Seleziona un gioco" + }, + "port": { + "label": "Porta da monitorare", + "placeholder": 80 + }, + "grpcServiceName": { + "label": "Nome servizio", + "placeholder": "es. my.service.v1 (lascia vuoto per lo stato generale)" + }, + "wsUrl": { + "label": "URL WebSocket", + "placeholder": "wss://example.com/socket" + }, + "strategy": { + "label": "Strategia", + "desktop": "Desktop", + "mobile": "Mobile" + }, + "captureUrl": { + "label": "Endpoint Capture", + "placeholder": "http://192.168.1.10:5001/api/v1/metrics" + }, + "dnsServer": { + "label": "Server DNS", + "placeholder": "8.8.8.8" + }, + "dnsRecordType": { + "label": "Tipo di record DNS", + "value": { + "A": "A", + "AAAA": "AAAA", + "CNAME": "CNAME", + "MX": "MX", + "NS": "NS", + "TXT": "TXT" + } + } + }, + "title": "Impostazioni generali", + "description": { + "http": "Inserisci l'URL o l'IP da monitorare (es. https://example.com/ o 192.168.1.100) e aggiungi un nome visualizzato chiaro che apparirร  nella dashboard.", + "ping": "Inserisci l'indirizzo IP o l'hostname da pingare (es. 192.168.1.100 o example.com) e aggiungi un nome visualizzato chiaro che apparirร  nella dashboard.", + "port": "Inserisci l'URL o l'IP del server, il numero di porta e un nome visualizzato chiaro che apparirร  nella dashboard.", + "docker": "Inserisci il nome o l'ID del container Docker. Puoi usare il nome del container (es. my-app) o l'ID del container (ID completo di 64 caratteri o ID breve).", + "game": "Inserisci l'indirizzo IP o l'hostname e il numero di porta da pingare (es. 192.168.1.100 o example.com) e scegli il tipo di gioco. Alcuni giochi richiedono l'abilitazione delle query del server nel loro file di configurazione โ€” consulta la documentazione del tuo server di gioco. Consulta la lista dei giochi supportati per i dettagli del protocollo.", + "pagespeed": "Traccia le prestazioni di caricamento della pagina, i Core Web Vitals e i punteggi di ottimizzazione per il tuo sito web.", + "grpc": "Inserisci l'hostname e la porta del server gRPC, specifica opzionalmente un nome di servizio per la verifica di stato e aggiungi un nome visualizzato.", + "websocket": "Inserisci l'URL WebSocket da monitorare (es. wss://example.com/socket) e aggiungi un nome visualizzato.", + "hardware": "Monitora CPU, memoria, utilizzo del disco e temperatura per la tua infrastruttura. L'URL dell'endpoint Capture deve includere il percorso completo /api/v1/metrics (es. http://192.168.1.10:5001/api/v1/metrics). Per il segreto di autorizzazione, incolla solo la stringa del token โ€” non includere 'Bearer'.", + "dns": "Monitora se uno specifico server DNS sta rispondendo." + } + }, + "ignoreTls": { + "description": "Configura la validazione del certificato TLS/SSL per le connessioni HTTPS.", + "option": { + "tls": { + "label": "Ignora errori TLS/SSL" + } + }, + "title": "Impostazioni TLS/SSL" + }, + "incidents": { + "description": "Per determinare quando un monitor va giรน viene utilizzata una finestra scorrevole. Lo stato di un monitor cambia solo quando la percentuale di verifiche nella finestra scorrevole raggiunge il valore specificato.", + "option": { + "checks": { + "label": "Numero di verifiche nella finestra scorrevole" + }, + "percentage": { + "label": "Quale percentuale di verifiche nella finestra scorrevole deve fallire/riuscire prima che lo stato del monitor cambi?" + } + }, + "title": "Incidenti" + }, + "notifications": { + "description": "Seleziona i canali di notifica che vuoi utilizzare", + "title": "Notifiche" + }, + "tags": { + "description": "Seleziona i tag che vuoi applicare a questo monitor", + "title": "Tag" + }, + "type": { + "description": "Seleziona il tipo di verifica da eseguire", + "optionDockerDescription": "Usa Docker per monitorare se un container รจ in esecuzione.", + "optionGameDescription": "Monitora se uno specifico game server รจ online.", + "optionGrpcDescription": "Monitora i servizi gRPC usando il protocollo standard Health Checking.", + "optionWebSocketDescription": "Monitora gli endpoint WebSocket per lo stato della connessione e il tempo di risposta.", + "optionHttpDescription": "Usa HTTP(S) per monitorare il tuo sito web o endpoint API.", + "optionPingDescription": "Usa ICMP Ping per monitorare se un server รจ online.", + "optionPortDescription": "Monitora se una specifica porta su un server รจ aperta.", + "optionDnsDescription": "Monitora se uno specifico server DNS sta rispondendo.", + "title": "Tipo" + }, + "thresholds": { + "title": "Soglie di allerta", + "description": "Definisci le soglie alle quali devono essere attivati gli avvisi per questo monitor hardware.", + "option": { + "cpuThreshold": { + "label": "Soglia allerta CPU (%)" + }, + "memoryThreshold": { + "label": "Soglia allerta memoria (%)" + }, + "diskThreshold": { + "label": "Soglia allerta disco (%)" + }, + "tempThreshold": { + "label": "Soglia allerta temperatura (ยฐC)" + } + } + }, + "geoChecks": { + "title": "Verifiche geo-distribuite", + "description": "Esegui verifiche da piรน posizioni geografiche per monitorare disponibilitร  e prestazioni globali.", + "option": { + "enabled": { + "label": "Abilita verifiche geo-distribuite" + }, + "locations": { + "label": "Posizioni", + "placeholder": "Seleziona posizioni", + "options": { + "EU": "Europa", + "NA": "Nord America", + "AS": "Asia", + "SA": "Sud America", + "AF": "Africa", + "OC": "Oceania" + } + }, + "interval": { + "label": "Intervallo di verifica", + "value": { + "fiveMinutes": "5 minuti", + "tenMinutes": "10 minuti", + "fifteenMinutes": "15 minuti", + "thirtyMinutes": "30 minuti" + } + } + } + }, + "url": { + "title": "Monitor IP/URL sulla pagina di stato", + "description": "Visualizza l'indirizzo IP o l'URL del monitor sulla pagina di stato pubblica. Se disabilitato, verrร  mostrato solo il nome del monitor per proteggere le informazioni sensibili.", + "option": { + "showURL": { + "label": "Visualizza IP/URL sulla pagina di stato", + "enabled": "Abilitato", + "disabled": "Disabilitato" + } + } + }, + "stats": { + "title": "Cronologia monitor", + "description": "Cancella tutta la cronologia di monitoraggio e le statistiche per il tuo team. Questa azione รจ irreversibile.", + "buttonText": "Cancella tutte le statistiche", + "dialog": { + "title": "Cancellare tutta la cronologia di monitoraggio?", + "description": "Questa operazione eliminerร  permanentemente tutta la cronologia di monitoraggio, le statistiche e i dati di verifica per il tuo team. Questa azione non puรฒ essere annullata.", + "confirm": "Sรฌ, cancella tutte le statistiche" + } + } + } + }, + "editUser": { + "form": { + "roles": { + "title": "Ruoli", + "description": "Assegna ruoli all'utente. รˆ possibile selezionare piรน ruoli." + } + }, + "dialog": { + "removeUser": { + "title": "Rimuovi utente", + "content": "Sei sicuro di voler rimuovere {{name}} dal tuo team? Questa azione non puรฒ essere annullata." + } + } + }, + "incidents": { + "fallback": { + "title": "Nessun incidente โ€” tutti i sistemi funzionano correttamente", + "description": "Quando un monitor va giรน o si ripristina, l'incidente apparirร  qui con tempistiche, dettagli del monitor e cronologia della risoluzione." + }, + "dialog": { + "resolveIncident": { + "title": "Risolvi incidente", + "option": { + "comment": { + "label": "Commento (opzionale)", + "placeholder": "Aggiungi un commento sulla risoluzione..." + } + } + }, + "details": { + "analysis": "Analisi incidente", + "comment": "Commento:", + "detailsLabel": "Dettagli", + "downtime": "Downtime:", + "message": "Messaggio:", + "monitor": "Monitor:", + "overview": "Panoramica", + "resolutionDetails": "Dettagli risoluzione", + "resolutionType": "Tipo:", + "resolutionTypes": { + "automatic": "Automatica", + "manual": "Manuale" + }, + "resolve": "Risolvi incidente", + "resolvedAt": "Risolto il:", + "resolvedBy": "Risolto da:", + "startedAt": "Iniziato il:", + "status": "Stato:", + "statusCode": "Codice di stato:", + "timeline": "Cronologia", + "title": "Dettagli incidente", + "url": "URL:" + } + }, + "filters": { + "allMonitors": "Tutti i monitor", + "monitor": "Monitor", + "resolutionType": "Tipo di risoluzione", + "resolutionTypes": { + "manual": "Manuale", + "automatic": "Automatica", + "all": "Tutti" + } + }, + "summaryCard": { + "activeIncidents": { + "title": "Incidenti attivi", + "active_zero": "Nessun incidente attivo", + "active_one": "{{count}} incidente attivo", + "active_other": "{{count}} incidenti attivi" + }, + "incidentStats": { + "avgResolutionTime": "Tempo medio di risoluzione", + "mostAffectedMonitor": "Monitor piรน colpito", + "title": "Statistiche incidenti", + "totalIncidents": "Incidenti totali" + }, + "latestIncidents": { + "title": "Ultimi incidenti" + } + }, + "table": { + "actions": { + "details": "Dettagli", + "goToMonitor": "Vai al monitor", + "resolveManually": "Risolvi manualmente" + }, + "activeIncidents": "Incidenti attivi", + "headers": { + "endTime": "Ora di fine", + "resolutionType": "Tipo di risoluzione", + "startTime": "Ora di inizio", + "statusCode": "Codice di stato" + }, + "resolvedIncidents": "Incidenti risolti", + "status": { + "active": "Attivo", + "resolved": "Risolto" + } + }, + "header": { + "title": "Incidenti", + "description": "Quando i monitor vanno giรน o si ripristinano, gli incidenti appaiono qui con tempistiche, dettagli del monitor e cronologia della risoluzione." + } + }, + "infrastructure": { + "charts": { + "labels": { + "cpu": "Utilizzo CPU", + "disk": "Utilizzo disco", + "memory": "Utilizzo memoria", + "netBytesRecv": "{{name}} - Byte ricevuti", + "netBytesSent": "{{name}} - Byte inviati", + "loopbackOnlyNotice": "Capture sta segnalando solo interfacce loopback o bridge di container (lo, lo0, docker0). Questo รจ normale quando Capture viene eseguito all'interno di un container che puรฒ vedere solo il proprio namespace di rete โ€” esegui Capture direttamente sull'host per vedere l'attivitร  di rete reale.", + "temp": "Temp" + } + }, + "gauges": { + "cpu": { + "lowerLabel": "Frequenza massima", + "title": "Utilizzo CPU", + "upperLabel": "Frequenza attuale" + }, + "disk": { + "lowerLabel": "Libero", + "title": "Utilizzo disco {{idx}}", + "upperLabel": "Utilizzato" + }, + "memory": { + "lowerLabel": "Libero", + "title": "Utilizzo memoria", + "upperLabel": "Utilizzato" + } + }, + "statBoxes": { + "avgCpuTemperature": "Temperatura media CPU", + "cpuFrequency": "Frequenza CPU", + "cpuLogical": "CPU (logica)", + "cpuPhysical": "CPU (fisica)", + "disk": "Disco", + "memory": "Memoria", + "os": "OS" + }, + "table": { + "headers": { + "cpu": "CPU", + "disk": "Disco", + "memory": "Memoria" + } + }, + "tabs": { + "labels": { + "network": "Rete", + "overview": "Panoramica" + } + }, + "fallback": { + "actionButton": "Crea un monitor", + "description": "Traccia le prestazioni del server, identifica i colli di bottiglia e garantisci l'affidabilitร  con il monitoraggio infrastrutturale in tempo reale.", + "title": "Nessun monitor infrastruttura" + }, + "header": { + "title": "Monitor infrastruttura", + "description": "Traccia le prestazioni del server, identifica i colli di bottiglia e garantisci l'affidabilitร  con il monitoraggio infrastrutturale in tempo reale." + } + }, + "tags": { + "header": { + "title": "Tag", + "description": "Organizza i tuoi monitor con i tag" + }, + "fallback": { + "title": "Nessun tag", + "description": "Organizza i tuoi monitor con i tag. Una volta creato un tag e assegnato a un monitor, potrai filtrare per quel tag in tutta l'app e vedere i dettagli del tag qui.", + "actionButton": "Crea un tag" + }, + "table": { + "headers": { + "color": "Colore", + "appearance": "Aspetto" + } + }, + "form": { + "name": { + "title": "Nome tag", + "description": "Scegli un nome per il tuo tag", + "optionName": "Nome tag", + "placeholder": "es. Gruppo 1" + }, + "color": { + "title": "Colore tag", + "description": "Scegli un colore per il tuo tag", + "optionName": "Colore tag", + "placeholder": "es. #13715B" + } + } + }, + "logs": { + "fallback": { + "title": "Nessun log", + "description": "Attivitร  del server, avvisi ed errori appariranno qui man mano che Checkmate viene eseguito." + }, + "queueFallback": { + "title": "Nessun job nella coda", + "description": "Le verifiche del monitor pianificate o in esecuzione appariranno qui. Una coda vuota significa che tutto รจ aggiornato." + }, + "tabs": { + "diagnostics": "Diagnostica", + "logs": "Log", + "queue": "Coda job" + }, + "logLevelSelect": { + "label": "Livello di log" + }, + "workers": "Worker", + "workersExplainers": "Mostra i processi worker. Gli ID worker sono troncati; passa il mouse sopra per vedere l'ID completo.", + "jobQueue": "Coda job", + "jobQueueExplainer": "Le righe evidenziate sono job attualmente in elaborazione da parte di un worker.", + "failedJobs": "Job falliti", + "failedJobsExplainer": "Errori recenti per monitor โ€” orario dell'ultimo errore, conteggio errori e motivo.", + "noLogs": "Nessun log trovato", + "metrics": { + "jobs": "Job", + "activeJobs": "Job attivi", + "failingJobs": "Job non riusciti", + "totalRuns": "Esecuzioni totali", + "totalFailures": "Errori totali" + }, + "diagnostics": { + "sections": { + "runtimeStats": { + "title": "Statistiche runtime", + "description": "Istantanea del processo Node.js e della macchina host. Aggiornata ogni 5 secondi." + }, + "memoryCpu": { + "title": "Memoria e CPU", + "description": "Come il processo sta utilizzando la memoria allocata e la CPU dell'host." + }, + "mongoDBStats": { + "title": "Statistiche MongoDB", + "description": "Istantanea delle metriche MongoDB" + }, + "mongoDBCollectionStats": { + "title": "Statistiche collezioni MongoDB", + "description": "Statistiche per le collezioni MongoDB" + } + }, + "stats": { + "eventLoopDelay": "Ritardo event loop", + "uptime": "Uptime", + "usedHeapSize": "Dimensione heap utilizzata", + "totalHeapSize": "Dimensione heap totale", + "osMemoryLimit": "Limite memoria OS" + }, + "mongoDBStats": { + "readyState": "Stato di connessione", + "readsPerSecond": "Letture/s", + "insertsPerSecond": "Inserimenti/s", + "updatesPerSecond": "Aggiornamenti/s", + "deletesPerSecond": "Eliminazioni/s", + "writesPerSecond": "Scritture totali/s", + "host": "Host", + "port": "Porta", + "dbName": "Nome database", + "storageSize": "Dimensione archiviazione" + }, + "mongoDBCollectionStats": { + "documents": "Documenti: {{count}}", + "bucketCount": "Conteggio bucket: {{count}}", + "storageSize": "Dimensione archiviazione: {{size}}", + "indexSize": "Dimensione indice: {{size}}", + "totalSize": "Dimensione totale: {{size}}", + "dbUsageTooltip": "{{percentage}}% dell'archiviazione DB" + }, + "gauges": { + "heapAllocation": "Allocazione heap", + "heapUsage": "Utilizzo heap", + "heapUtilization": "Utilizzo heap", + "instantCpuUsage": "Utilizzo CPU istantaneo", + "availableMemoryPercentage": "% memoria disponibile", + "allocatedPercentage": "% allocato", + "usedSPercentage": "% di 1s usato dalla CPU", + "total": "Totale", + "used": "Utilizzato" + } + }, + "table": { + "headers": { + "host": "Host", + "processId": "ID processo", + "workerId": "ID worker", + "workerMode": "Modalitร  worker", + "lastSeenAt": "Visto l'ultima volta", + "state": "Stato", + "timestamp": "Timestamp", + "level": "Livello", + "service": "Servizio", + "method": "Metodo", + "monitorId": "ID monitor", + "runCount": "Esecuzioni", + "failCount": "Errori", + "nextScheduledAt": "Prossima esecuzione", + "lockedBy": "Bloccato da", + "lastFinishedAt": "Ultimo completamento", + "lastFailedAt": "Ultimo errore", + "failReason": "Motivo errore" + } + }, + "header": { + "title": "Log e coda", + "description": "Ispeziona i log del server, la coda dei job del monitor e la diagnostica runtime in un unico posto." + } + }, + "maintenanceWindow": { + "fallback": { + "actionButton": "Crea una finestra di manutenzione", + "description": "Pianifica downtime programmati in modo che gli avvisi vengano soppressi e il tuo team e gli utenti sappiano cosa aspettarsi.", + "title": "Nessuna finestra di manutenzione pianificata" + }, + "table": { + "headers": { + "nextWindow": "Prossima finestra", + "repeat": "Ripeti" + } + }, + "form": { + "general": { + "title": "Impostazioni generali", + "description": "Imposta un nome e un'opzione di ripetizione per la tua finestra di manutenzione.", + "option": { + "name": { + "label": "Nome", + "placeholder": "es. Manutenzione settimanale" + }, + "repeat": { + "label": "Ripeti" + } + } + }, + "startDate": { + "title": "Data di inizio", + "description": "Seleziona la data di inizio per la tua finestra di manutenzione.", + "option": { + "startDate": { + "label": "Data di inizio" + } + } + }, + "startTime": { + "title": "Ora di inizio", + "description": "Imposta l'ora di inizio e la durata per la tua finestra di manutenzione. Tutti i valori sono in UTC", + "option": { + "duration": { + "label": "Durata" + }, + "startTime": { + "label": "Ora di inizio" + } + }, + "monitors": { + "title": "Monitor", + "description": "Monitor a cui deve essere applicata la finestra di manutenzione", + "option": { + "addMonitors": { + "label": "Aggiungi monitor" + } + } + } + } + }, + "header": { + "title": "Finestre di manutenzione", + "description": "Pianifica downtime programmati in modo che gli avvisi vengano soppressi e il tuo team e gli utenti sappiano cosa aspettarsi." + } + }, + "notifications": { + "fallback": { + "actionButton": "Crea un canale", + "description": "Connetti Slack, Discord, email, webhook e altro ancora affinchรฉ le persone giuste siano informate degli incidenti nel momento in cui si verificano.", + "title": "Nessun canale di notifica" + }, + "form": { + "accessToken": { + "optionAccessToken": "Token di accesso", + "placeholder": "syt_YWxleF9ob2xsaWRheQ_VmtScmV0U2VjcmV0S2V5_abc123" + }, + "address": { + "description": "L'indirizzo a cui verranno inviate le notifiche.", + "optionAddress": "Indirizzo", + "placeholderEmail": "example@example.com", + "placeholderWebhook": "https://your-server.com/webhook", + "title": "Indirizzo" + }, + "homeServer": { + "optionHomeServer": "Home server", + "placeholder": "example.com" + }, + "matrix": { + "description": "Configura la connessione al tuo homeserver Matrix per le notifiche.", + "title": "Configurazione Matrix" + }, + "notificationName": { + "description": "Un nome descrittivo per il canale di notifica", + "optionName": "Nome canale", + "placeholder": "es. Avvisi produzione", + "title": "Nome canale" + }, + "pagerDuty": { + "description": "La tua chiave di integrazione PagerDuty per ricevere avvisi.", + "optionIntegrationKey": "Chiave di integrazione", + "placeholder": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + "title": "Chiave di integrazione" + }, + "roomId": { + "optionRoomId": "ID stanza", + "placeholder": "!abcdefg:matrix.org" + }, + "type": { + "description": "Seleziona il tipo di canale di notifica da creare.", + "optionType": "Tipo", + "title": "Tipo di canale" + }, + "telegram": { + "title": "Configurazione Telegram", + "description": "Configura il tuo bot Telegram per inviare notifiche.", + "optionBotToken": "Token bot", + "placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ", + "optionChatId": "ID chat", + "placeholderChatId": "-1001234567890" + }, + "pushover": { + "title": "Configurazione Pushover", + "description": "Configura Pushover per ricevere notifiche push sui tuoi dispositivi.", + "optionAppToken": "Token API applicazione", + "placeholderAppToken": "azGDORePK8gMaC0QOYAMyEEuzJnyUi", + "optionUserKey": "Chiave utente", + "placeholderUserKey": "uQiRzpo4DXghDmr9QzzfQu27cmVRsG" + }, + "twilio": { + "title": "Configurazione SMS Twilio", + "description": "Configura Twilio per inviare notifiche SMS a un numero di telefono.", + "optionAccountSid": "SID account", + "placeholderAccountSid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "optionAuthToken": "Token di autenticazione", + "placeholderAuthToken": "your_auth_token", + "optionFromNumber": "Numero mittente (Twilio)", + "placeholderFromNumber": "+15551234567", + "optionToNumber": "Numero destinatario", + "placeholderToNumber": "+15559876543" + }, + "ntfy": { + "title": "Configurazione ntfy", + "description": "Configura un server e un topic ntfy per le notifiche push.", + "optionServerUrl": "URL server", + "placeholderServerUrl": "https://ntfy.sh", + "optionTopic": "Topic", + "placeholderTopic": "checkmate-alerts" + } + }, + "table": { + "headers": { + "destination": "Destinazione" + } + }, + "header": { + "title": "Notifiche", + "description": "Connetti Slack, Discord, email, webhook e altro ancora affinchรฉ le persone giuste siano informate degli incidenti nel momento in cui si verificano." + } + }, + "pageSpeed": { + "strategy": { + "desktop": "Desktop", + "mobile": "Mobile" + }, + "charts": { + "common": { + "cls": "Cumulative Layout Shift (CLS)", + "fcp": "First Contentful Paint (FCP)", + "lcp": "Largest Contentful Paint (LCP)", + "si": "Speed Index (SI)", + "tbt": "Total Blocking Time (TBT)" + }, + "legend": { + "title": "Report PageSpeed", + "weight": "Peso" + }, + "pie": { + "title": "Report prestazioni" + } + }, + "table": { + "headers": { + "pageSpeedScore": "Punteggio PageSpeed" + } + }, + "fallback": { + "actionButton": "Crea un monitor", + "description": "Traccia i Core Web Vitals e i punteggi Lighthouse nel tempo per individuare le regressioni prima degli utenti.", + "title": "Nessun monitor PageSpeed" + }, + "header": { + "title": "Monitor PageSpeed", + "description": "Traccia i Core Web Vitals e i punteggi Lighthouse nel tempo per individuare le regressioni prima degli utenti." + } + }, + "settings": { + "form": { + "timezone": { + "title": "Fuso orario di visualizzazione", + "description": "Seleziona il fuso orario usato per visualizzare date e orari in tutta l'applicazione.", + "option": { + "timezone": { + "label": "Fuso orario di visualizzazione" + } + } + }, + "ui": { + "title": "Aspetto", + "description": "Passa tra modalitร  chiara e scura, cambia lingua o personalizza il tipo di grafico.", + "option": { + "theme": { + "label": "Modalitร  tema", + "light": "Chiaro", + "dark": "Scuro" + }, + "language": { + "label": "Lingua" + }, + "chartType": { + "label": "Tipo di grafico", + "histogram": "Istogramma", + "heatmap": "Heatmap" + } + } + }, + "pagespeed": { + "title": "Chiave API di Google PageSpeed", + "description": "Inserisci la tua chiave API di Google PageSpeed per abilitare il monitoraggio Google PageSpeed. Clicca su Reimposta per aggiornare la chiave.", + "option": { + "apiKey": { + "label": "Chiave API PageSpeed", + "labelSet": "La chiave API รจ impostata. Clicca su Reimposta per modificarla.", + "placeholder": "Inserisci la tua chiave API di Google PageSpeed" + } + } + }, + "url": { + "title": "Monitor IP/URL sulla pagina di stato", + "description": "Visualizza l'indirizzo IP o l'URL del monitor sulla pagina di stato pubblica. Se disabilitato, verrร  mostrato solo il nome del monitor per proteggere le informazioni sensibili.", + "option": { + "showURL": { + "label": "Visualizza IP/URL sulla pagina di stato", + "enabled": "Abilitato", + "disabled": "Disabilitato" + } + } + }, + "stats": { + "title": "Cronologia monitor", + "description": "Cancella tutta la cronologia di monitoraggio e le statistiche per il tuo team. Questa azione รจ irreversibile.", + "option": { + "clear": { + "label": "Cancella tutte le statistiche. Questa azione รจ irreversibile." + } + }, + "dialog": { + "title": "Cancellare tutta la cronologia di monitoraggio?", + "description": "Questa azione non puรฒ essere annullata." + } + }, + "retention": { + "title": "Conservazione delle verifiche", + "description": "Imposta per quanto tempo i dati delle verifiche vengono conservati prima di essere eliminati automaticamente.", + "option": { + "days": { + "label": "Periodo di conservazione (giorni)", + "unlimited": "Illimitato" + } + } + }, + "thresholds": { + "title": "Soglie globali", + "description": "Imposta soglie globali di CPU, memoria, disco e temperatura per il monitoraggio infrastrutturale. Si applicano a tutti i monitor hardware a meno che non vengano sovrascritte.", + "option": { + "cpu": { + "label": "Soglia CPU (%)", + "placeholder": "1 - 100" + }, + "memory": { + "label": "Soglia memoria (%)", + "placeholder": "1 - 100" + }, + "disk": { + "label": "Soglia disco (%)", + "placeholder": "1 - 100" + }, + "temperature": { + "label": "Soglia temperatura (ยฐC)", + "placeholder": "1 - 150" + } + } + }, + "email": { + "title": "Impostazioni email", + "description": "Configura le impostazioni email per il tuo sistema. Vengono utilizzate per inviare notifiche e avvisi.", + "descriptionTransport": "Costruisce un trasporto SMTP per NodeMailer", + "descriptionTransportLink": "Consulta le specifiche qui", + "option": { + "host": { + "label": "Host email - Hostname o indirizzo IP a cui connettersi", + "placeholder": "smtp.gmail.com" + }, + "port": { + "label": "Porta email - Porta a cui connettersi", + "placeholder": "587" + }, + "address": { + "label": "Indirizzo email - Usato per l'autenticazione", + "placeholder": "you@example.com" + }, + "displayName": { + "label": "Nome mittente (opzionale) - Nome descrittivo mostrato nel campo Da", + "placeholder": "Notifiche Checkmate" + }, + "user": { + "label": "Utente email - Username per l'autenticazione, sovrascrive l'indirizzo email se specificato", + "placeholder": "Lascia vuoto se non richiesto" + }, + "password": { + "label": "Password email - Password per l'autenticazione", + "labelSet": "La password รจ impostata. Clicca su Reimposta per modificarla.", + "placeholder": "Inserisci la tua password" + }, + "tlsServername": { + "label": "Servername TLS - Hostname opzionale per la validazione TLS quando l'host รจ un IP", + "placeholder": "example.com" + }, + "connectionHost": { + "label": "Host di connessione email - Hostname da usare nel saluto HELO/EHLO", + "placeholder": "localhost" + }, + "secure": { + "label": "Usa SSL (consigliato): cripta la connessione con SSL/TLS" + }, + "pool": { + "label": "Abilita il pooling delle connessioni: riutilizza le connessioni esistenti per migliorare le prestazioni" + }, + "ignoreTLS": { + "label": "Disabilita STARTTLS: non usare TLS anche se il server lo supporta" + }, + "requireTLS": { + "label": "Forza STARTTLS: richiedi l'aggiornamento a TLS, fallisce se non supportato" + }, + "rejectUnauthorized": { + "label": "Rifiuta certificati non validi: rifiuta connessioni con certificati auto-firmati o non fidati" + } + } + }, + "demoMonitors": { + "title": "Monitor demo", + "description": "Aggiungi monitor di esempio a scopo dimostrativo." + }, + "removeMonitors": { + "title": "Reimposta il sistema", + "description": "Rimuovi tutti i monitor dal tuo sistema.", + "dialog": { + "title": "Rimuovere tutti i monitor?", + "description": "Questa operazione non puรฒ essere annullata.", + "paragraph": "Verranno rimossi anche tutta la cronologia delle verifiche, gli incidenti, le finestre di manutenzione pianificate e qualsiasi pagina di stato che facesse riferimento a questi monitor. Esporta prima i tuoi monitor per conservare un backup dei loro dati." + } + }, + "exportMonitors": { + "title": "Esporta monitor" + }, + "importExportMonitors": { + "title": "Importa / Esporta monitor", + "description": "Importa o esporta i dati dei tuoi monitor come file JSON per backup o trasferimento." + }, + "about": { + "title": "Informazioni", + "developedBy": "Sviluppato da Bluewave Labs" + }, + "validation": { + "errorMessage": "Correggi i seguenti errori di validazione:" + } + }, + "header": { + "title": "Impostazioni", + "description": "Configura SMTP, integrazioni, conservazione e comportamento globale dell'app." + } + }, + "statusPages": { + "deleteSuccess": "Pagina di stato eliminata correttamente", + "fallback": { + "title": "Nessuna pagina di stato ancora", + "description": "Pubblica una pagina pubblica che mostra l'uptime in tempo reale e la cronologia degli incidenti ai tuoi clienti e stakeholder.", + "actionButton": "Crea una pagina di stato" + }, + "monitorsList": { + "chartTypeHeatmap": "Heatmap", + "chartTypeHistogram": "Istogramma", + "noData": "Nessun dato disponibile", + "status": { + "up": "Operativo", + "down": "Non attivo", + "breached": "Soglia superata", + "maintenance": "Manutenzione", + "paused": "In pausa", + "initializing": "Inizializzazione" + }, + "chart": { + "avg": "media {{value}}ms", + "max": "max {{value}}ms", + "downTooltip": "Non attivo", + "heatmapAria": "Heatmap del tempo di risposta" + }, + "infrastructure": { + "title": "Infrastruttura", + "cpu": "CPU", + "memory": "RAM", + "memoryText": "Memoria", + "disk": "Disco", + "usage": "Utilizzo", + "used": "Utilizzato", + "total": "Totale" + }, + "uptime": { + "title": "Uptime", + "responseTime": "Tempo di risposta" + } + }, + "statusBar": { + "monitoringSummary": "Monitoraggio di {{count}} servizio ยท aggiornamenti ogni 10 secondi", + "monitoringSummary_other": "Monitoraggio di {{count}} servizi ยท aggiornamenti ogni 10 secondi", + "allUp": "Tutti i sistemi operativi", + "allBreached": "Tutti i sistemi hanno superato le soglie", + "allMaintenance": "Tutti i sistemi sono in manutenzione", + "allDown": "Tutti i sistemi sono non attivi", + "allPaused": "Tutti i sistemi sono in pausa", + "allInitializing": "Tutti i sistemi sono in inizializzazione", + "breachedAndDown": "Alcuni sistemi hanno superato le soglie e sono non attivi", + "breached": "Alcuni sistemi hanno superato le soglie", + "degraded": "Alcuni sistemi stanno riscontrando problemi", + "maintenanceAndDown": "Alcuni sistemi sono non attivi durante la manutenzione", + "maintenance": "Alcuni sistemi sono in manutenzione", + "partiallyPaused": "Alcuni sistemi sono in pausa", + "initializing": "Alcuni sistemi sono in inizializzazione", + "noMonitors": "Nessun monitor รจ stato configurato", + "unknown": "Impossibile determinare lo stato del sistema" + }, + "table": { + "headers": { + "name": "Nome pagina di stato", + "url": "URL pubblico" + }, + "published": "Pubblicata", + "unpublished": "Non pubblicata" + }, + "title": "Pagine di stato", + "details": { + "empty": { + "title": "Ancora nulla qui", + "addMonitor": "Aggiungi un monitor per iniziare" + } + }, + "footer": { + "poweredBy": "Powered by" + }, + "editorial": { + "reportTitle": "Rapporto di servizio", + "allUp": "Tutti i sistemi funzionano normalmente." + }, + "form": { + "access": { + "title": "Accesso", + "description": "Se la tua pagina di stato รจ pronta, puoi contrassegnarla come pubblicata.", + "option": { + "published": { + "name": "Pubblicata e visibile al pubblico" + } + } + }, + "basicInfo": { + "title": "Informazioni di base", + "description": "Definisci il nome dell'azienda e il sottodominio a cui punta la tua pagina di stato.", + "option": { + "name": { + "label": "Nome azienda", + "placeholder": "Acme Inc." + }, + "url": { + "label": "Indirizzo della tua pagina di stato", + "placeholder": "my-status-page" + }, + "customDomain": { + "label": "Dominio personalizzato", + "placeholder": "status.example.com", + "helper": "Opzionale. Punta un record DNS a questa istanza di Checkmate per servire la pagina di stato sul tuo dominio." + } + } + }, + "monitors": { + "title": "Monitor", + "description": "Seleziona i monitor da visualizzare sulla tua pagina di stato.", + "noMonitors": "Nessun monitor selezionato", + "option": { + "monitors": { + "label": "Seleziona monitor", + "placeholder": "Cerca e seleziona monitor..." + } + } + }, + "timezone": { + "title": "Fuso orario", + "description": "Seleziona il fuso orario in cui verrร  visualizzata la tua pagina di stato.", + "option": { + "timezone": { + "label": "Fuso orario", + "placeholder": "Seleziona fuso orario..." + } + } + }, + "appearance": { + "title": "Aspetto", + "description": "Definisci l'aspetto predefinito della tua pagina di stato pubblica.", + "option": { + "logo": { + "label": "Logo" + }, + "color": { + "label": "Colore del brand" + } + } + }, + "theme": { + "title": "Tema pagina", + "description": "Scegli lo stile visivo per la tua pagina di stato pubblica.", + "label": "Tema", + "options": { + "refined": { + "name": "Raffinato", + "description": "Pulito, professionale, vicino al classico look di Checkmate." + }, + "modern": { + "name": "Moderno", + "description": "Livello SaaS con un punto di stato pulsante e schede in dissolvenza morbida." + }, + "bold": { + "name": "Deciso", + "description": "Caratteri grandi e spaziatura decisa. Ottimo per pagine focalizzate sul brand." + }, + "editorial": { + "name": "Editoriale", + "description": "Intestazioni con serif e divisori sottili. Distintivo e sobrio." + }, + "minimal": { + "name": "Minimale", + "description": "Piatto, focalizzato e compatto, con solo l'essenziale." + } + } + }, + "themeMode": { + "label": "Modalitร  tema", + "auto": "Auto (segue il dispositivo del visitatore)", + "light": "Solo chiaro", + "dark": "Solo scuro" + }, + "customCSS": { + "title": "CSS personalizzato", + "description": "Aggiungi CSS personalizzato per rifinire l'aspetto della tua pagina di stato pubblica.", + "option": { + "customCSS": { + "label": "CSS personalizzato", + "placeholder": "footer { display: none; }" + } + } + }, + "features": { + "title": "Funzionalitร ", + "description": "Configura quali informazioni verranno visualizzate sulla tua pagina di stato.", + "option": { + "showCharts": { + "label": "Mostra grafici del tempo di risposta" + }, + "showUptimePercentage": { + "label": "Mostra percentuale di uptime" + }, + "showAdminLoginLink": { + "label": "Mostra link login amministratore" + }, + "showInfrastructure": { + "label": "Mostra metriche infrastrutturali" + } + } + } + }, + "header": { + "title": "Pagine di stato", + "description": "Pubblica pagine pubbliche che mostrano l'uptime in tempo reale e la cronologia degli incidenti ai tuoi clienti e stakeholder." + }, + "preview": { + "title": "Anteprima pagina di stato" + } + }, + "uptime": { + "filters": { + "search": { + "placeholder": "Cerca monitor..." + }, + "activeTags": "Tag attivi" + }, + "table": { + "headers": { + "responseTime": "Tempo di risposta", + "uptime": "Uptime %" + } + }, + "fallback": { + "actionButton": "Crea un monitor", + "description": "Controlla endpoint HTTP, ping, container e porte โ€” e ricevi un avviso nel momento in cui qualcosa va giรน.", + "title": "Nessun monitor uptime" + }, + "header": { + "title": "Monitor uptime", + "description": "Controlla endpoint HTTP, ping, container e porte โ€” e ricevi un avviso nel momento in cui qualcosa va giรน." + } + } + } +} diff --git a/client/src/locales/pl.json b/client/src/locales/pl.json new file mode 100644 index 000000000..9a74b6cdb --- /dev/null +++ b/client/src/locales/pl.json @@ -0,0 +1,1663 @@ +{ + "common": { + "alerts": { + "pageSpeedApiKey": { + "content": "Ostrzeลผenie: Nie dodaล‚eล› jeszcze klucza Google PageSpeed API. Przejdลบ do Ustawienia, aby go dodaฤ‡. Bez niego monitor PageSpeed nie bฤ™dzie dziaล‚aล‚." + } + }, + "appName": "Checkmate", + "auth": { + "roles": { + "admin": "Administrator", + "demo": "Demo", + "superadmin": "Super administrator", + "user": "Uลผytkownik" + } + }, + "breadcrumbs": { + "details": "Szczegรณล‚y", + "home": "Strona gล‚รณwna" + }, + "buttons": { + "addDemo": "Dodaj demo monitory", + "addMember": "Dodaj uลผytkownika", + "back": "Wstecz", + "cancel": "Anuluj", + "clear": "Wyczyล›ฤ‡", + "clearFilters": "Wyczyล›ฤ‡ filtry", + "close": "Zamknij", + "configure": "Konfiguruj", + "confirm": "Potwierdลบ", + "create": "Utwรณrz", + "delete": "Usuล„", + "exportToJSON": "Eksportuj do JSON", + "flushQueue": "Oprรณลผnij kolejkฤ™", + "generateToken": "Wygeneruj token", + "importFromJSON": "Importuj z JSON", + "incidents": "Incydenty", + "inviteMember": "Zaproล› uลผytkownika", + "next": "Dalej", + "notFound": "Przejdลบ do gล‚รณwnego panelu", + "pause": "Wstrzymaj", + "removeMonitors": "Usuล„ monitory", + "removeUser": "Usuล„ uลผytkownika", + "reset": "Zresetuj", + "resetPassword": "Zresetuj hasล‚o", + "resume": "Wznรณw", + "save": "Zapisz", + "sendInvite": "Wyล›lij zaproszenie", + "sendTestEmail": "Wyล›lij testowy e-mail", + "test": "Test", + "testNotifications": "Przetestuj powiadomienia", + "toggleTheme": "Przeล‚ฤ…cza jasny i ciemny" + }, + "charts": { + "histogram": { + "avg": "ลšrednia: {{value}} ms", + "max": "Maks: {{value}} ms" + }, + "labels": { + "averageResponseTime": "ลšredni czas odpowiedzi", + "downtime": "Czas przestoju", + "high": "Wysoki", + "low": "Niski", + "uptime": "Czas pracy" + } + }, + "dialogs": { + "delete": { + "description": "Tej operacji nie moลผna cofnฤ…ฤ‡.", + "title": "Czy na pewno chcesz to usunฤ…ฤ‡?" + } + }, + "form": { + "email": { + "option": { + "email": { + "label": "Email", + "placeholder": "you@example.com" + } + } + }, + "name": { + "option": { + "firstName": { + "label": "Imiฤ™", + "placeholder": "Wpisz swoje imiฤ™" + }, + "lastName": { + "label": "Nazwisko", + "placeholder": "Wprowadลบ swoje nazwisko" + } + } + }, + "role": { + "option": { + "role": { + "label": "Role" + } + } + } + }, + "labels": { + "active": "Aktywny", + "idle": "Bezczynny", + "na": "Brak danych", + "paused": "Wstrzymano", + "resolved": "Rozwiฤ…zano", + "responseTime": "Czas reakcji", + "running": "Uruchomiony" + }, + "statusCode": { + "down": "Niedostฤ™pny", + "networkError": { + "dns": "Nie moลผna rozpoznaฤ‡ nazwy hosta (bล‚ฤ…d wyszukiwania DNS).", + "refused": "Serwer odrzuciล‚ poล‚ฤ…czenie (port zamkniฤ™ty lub usล‚uga nie nasล‚uchuje).", + "reset": "Poล‚ฤ…czenie zostaล‚o zresetowane, zanim otrzymano odpowiedลบ.", + "timeout": "Serwer nie odpowiedziaล‚ na czas (ลผฤ…danie przekroczyล‚o limit czasu).", + "tls": "Nie powiodล‚o siฤ™ uzgadnianie TLS lub SSL (problem z certyfikatem lub szyfrowaniem).", + "unknown": "Nie otrzymano odpowiedzi HTTP z docelowego serwera." + } + }, + "table": { + "empty": "Brak danych do wyล›wietlenia", + "headers": { + "actions": "Akcje", + "active": "Aktywny", + "dateTime": "Data i godzina", + "interval": "Przedziaล‚", + "message": "Wiadomoล›ฤ‡", + "monitor": "Monitor", + "monitorId": "ID monitora", + "name": "Nazwa", + "responseTime": "Czas odpowiedzi", + "status": "Status", + "tags": "Tagi", + "type": "Typ", + "url": "Url" + } + } + }, + "components": { + "headerStatusPageControls": { + "publicLink": "Publiczny link" + }, + "headerTimeRange": { + "description": { + "day": "Wyล›wietlanie statystyk z ostatnich 24 godzin.", + "month": "Wyล›wietlanie statystyk z ostatnich 30 dni.", + "recent": "Wyล›wietlanie statystyk z ostatnich 2 godzin.", + "week": "Wyล›wietlanie statystyk z ostatnich 7 dni." + }, + "labels": { + "day": "Dzieล„", + "month": "Miesiฤ…c", + "recent": "Najnowsze", + "week": "Tydzieล„" + } + }, + "imageUpload": { + "clickToUpload": "Kliknij, aby przesล‚aฤ‡", + "dragAndDrop": "lub przeciฤ…gnij i upuล›ฤ‡", + "errors": { + "invalidFileFormat": "Nieobsล‚ugiwany format pliku!", + "invalidFileSize": "Rozmiar pliku jest za duลผy!" + }, + "maxSize": "Maksymalny rozmiar", + "orDragAndDrop": "lub przeciฤ…gnij i upuล›ฤ‡", + "supportedFormats": "Obsล‚ugiwane formaty" + }, + "offlineBanner": { + "reconnected": "Poล‚ฤ…czenie przywrรณcone", + "retry": "Ponรณw prรณbฤ™", + "retrying": "Ponawianie prรณby...", + "serverUnreachable": "Nie moลผna poล‚ฤ…czyฤ‡ siฤ™ z serwerem" + }, + "pageHeader": { + "links": { + "changelog": "Dziennik zmian", + "discussions": "Dyskusje", + "docs": "Dokumentacja", + "support": "Pomoc" + } + }, + "sidebar": { + "accountMenu": { + "password": "Hasล‚o", + "profile": "Profil", + "team": "Zespรณล‚" + }, + "authFooter": { + "logOut": "Wyloguj siฤ™", + "navControls": "Sterowanie", + "roles": { + "admin": "Administrator", + "demoUser": "Uลผytkownik demo", + "superAdmin": "Super administrator", + "user": "Uลผytkownik" + } + }, + "bottomMenu": { + "changelog": "Dziennik zmian", + "discussions": "Dyskusje", + "docs": "Dokumentacja", + "support": "Wsparcie" + }, + "menu": { + "checks": "Kontrole", + "incidents": "Incydenty", + "infrastructure": "Infrastruktura", + "logs": "Dzienniki", + "maintenance": "Konserwacja", + "notifications": "Powiadomienia", + "pagespeed": "Pagespeed", + "settings": "Ustawienia", + "statusPages": "Strony statusu", + "tags": "Tagi", + "uptime": "Czas dziaล‚ania" + }, + "starPrompt": { + "description": "Zobacz najnowsze wydania i pomรณลผ rozwijaฤ‡ spoล‚ecznoล›ฤ‡ na GitHub", + "title": "Zagwiazdkuj Checkmate" + } + }, + "starPrompt": { + "description": "Zobacz najnowsze wydania i wspieraj rozwรณj spoล‚ecznoล›ci na GitHub", + "title": "Zagwiazdkuj Checkmate" + } + }, + "pages": { + "account": { + "form": { + "currentPassword": { + "description": "Wprowadลบ swoje obecne hasล‚o, aby potwierdziฤ‡ swojฤ… toลผsamoล›ฤ‡", + "option": { + "label": "Obecne hasล‚o", + "placeholder": "Wprowadลบ obecne hasล‚o" + }, + "title": "Obecne hasล‚o" + }, + "deleteAccount": { + "description": "Ta akcja jest nieodwracalna i nie moลผna jej cofnฤ…ฤ‡", + "title": "Usuล„ konto" + }, + "name": { + "description": "Zaktualizuj swoje dane osobowe", + "title": "Imiฤ™" + }, + "newPassword": { + "description": "Wybierz silne hasล‚o skล‚adajฤ…ce siฤ™ z co najmniej 8 znakรณw", + "option": { + "confirm": { + "label": "Potwierdลบ hasล‚o", + "placeholder": "Potwierdลบ nowe hasล‚o" + }, + "newPassword": { + "label": "Nowe hasล‚o", + "placeholder": "Wprowadลบ nowe hasล‚o" + } + }, + "title": "Nowe hasล‚o" + }, + "photo": { + "description": "Przeล›lij zdjฤ™cie profilowe", + "title": "Zdjฤ™cie profilowe" + } + }, + "header": { + "description": "Zarzฤ…dzaj swoim profilem, hasล‚em, zespoล‚em i preferencjami przestrzeni roboczej.", + "title": "Konto" + }, + "tabs": { + "password": "Hasล‚o", + "profile": "Profil", + "team": "Zespรณล‚" + }, + "team": { + "addMember": { + "description": "Utwรณrz nowe konto uลผytkownika. Nastฤ™pnie przekaลผ dane logowania w bezpieczny sposรณb.", + "title": "Zarejestruj nowego czล‚onka zespoล‚u" + }, + "fallback": { + "description": "Zaproล› czล‚onkรณw zespoล‚u przez e-mail lub dodaj ich bezpoล›rednio. Po doล‚ฤ…czeniu pojawiฤ… siฤ™ tutaj wraz ze swojฤ… rolฤ….", + "title": "Brak czล‚onkรณw zespoล‚u" + }, + "filter": { + "admin": "Administrator", + "all": "Wszystkie role", + "member": "Uลผytkownik", + "placeholder": "Filtruj wedล‚ug roli" + }, + "invite": { + "description": "Wyล›lij zaproszenie do doล‚ฤ…czenia do swojego zespoล‚u", + "email": { + "label": "Adres e-mail", + "placeholder": "Wprowadลบ adres e-mail" + }, + "linkLabel": "Link do zaproszenia", + "role": { + "label": "Rola", + "placeholder": "Wybierz rolฤ™" + }, + "title": "Zaproล› czล‚onka zespoล‚u" + }, + "table": { + "headers": { + "created": "Utworzono dnia", + "email": "E-mail", + "role": "Rola" + } + } + } + }, + "auth": { + "brandPanel": { + "description": "Monitorowanie dziaล‚ania, reakcji i incydentรณw w czasie rzeczywistym - do samodzielnego hostowania i na wล‚asnoล›ฤ‡.", + "eyebrow": "Monitoring infrastruktury open source", + "tagline": "Obserwuj kaลผdy ruch swoich serwerรณw." + }, + "common": { + "form": { + "option": { + "confirmPassword": { + "label": "Potwierdลบ hasล‚o" + }, + "email": { + "label": "E-mail", + "placeholder": "me@example.com" + }, + "password": { + "label": "Hasล‚o", + "placeholder": "********" + } + } + }, + "passwordRules": { + "length": { + "beginning": "Musi mieฤ‡ co najmniej", + "highlighted": "8 znakรณw" + }, + "lowercase": { + "beginning": "Musi zawieraฤ‡ co najmniej", + "highlighted": "jednฤ… maล‚ฤ… literฤ™" + }, + "match": { + "beginning": "Hasล‚a", + "highlighted": "musi pasowaฤ‡" + }, + "number": { + "beginning": "Musi zawieraฤ‡ co najmniej", + "highlighted": "jednฤ… cyfrฤ™" + }, + "special": { + "beginning": "Musi zawieraฤ‡ co najmniej", + "highlighted": "jeden znak specjalny (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)" + }, + "uppercase": { + "beginning": "Musi zawieraฤ‡ co najmniej", + "highlighted": "jednฤ… wielkฤ… literฤ™" + } + } + }, + "forgotPassword": { + "links": { + "login": { + "linkText": "zaloguj siฤ™", + "text": "Wrรณฤ‡ do" + } + }, + "submit": "Wyล›lij proล›bฤ™ o odzyskanie hasล‚a", + "subtitle": "Nie martw siฤ™, wyล›lemy Ci instrukcje resetowania.", + "title": "Zapomniaล‚eล› hasล‚a?" + }, + "login": { + "links": { + "forgotPassword": { + "linkText": "Resetuj hasล‚o", + "text": "Zapomniaล‚eล› hasล‚a?" + }, + "register": { + "linkText": "Zarejestruj siฤ™ tutaj", + "text": "Nie masz konta?" + } + }, + "submit": "Zaloguj siฤ™", + "subtitle": "Zaloguj siฤ™, aby kontynuowaฤ‡", + "title": "Witamy ponownie w Checkmate!" + }, + "register": { + "setupNotice": "Ta strona jest dostฤ™pna tylko podczas poczฤ…tkowej konfiguracji. Po zarejestrowaniu super administratora moลผna zapraszaฤ‡ dodatkowych uลผytkownikรณw z poziomu ustawieล„ zespoล‚u.", + "submit": "Zarejestruj siฤ™", + "subtitle": "Zarejestruj siฤ™, aby rozpoczฤ…ฤ‡", + "title": "Witamy ponownie w Checkmate!" + }, + "setNewPassword": { + "subtitle": "Twoje nowe hasล‚o musi rรณลผniฤ‡ siฤ™ od wczeล›niej uลผywanych haseล‚.", + "title": "Zresetuj hasล‚o" + } + }, + "checks": { + "fallback": { + "description": "Po utworzeniu monitora wszystkie statusy i czasy odpowiedzi bฤ™dฤ… wyล›wietlane tutaj jako checki.", + "title": "Brak checkรณw" + }, + "header": { + "description": "Kaลผde sprawdzenie wykonywane przez Checkmate na Twoich monitorach pojawia siฤ™ tutaj wraz z kodem statusu i czasem odpowiedzi.", + "title": "Checki" + }, + "selects": { + "monitor": { + "all": "Wszystkie monitory" + }, + "status": { + "all": "Wszystkie", + "down": "Niedostฤ™pny", + "up": "Dostฤ™pny" + } + }, + "table": { + "empty": "Brak awarii w tym przedziale czasowym", + "headers": { + "location": "Lokalizacja", + "statusCode": "Kod statusu" + } + } + }, + "common": { + "monitors": { + "actions": { + "bulkSelected_one": "Wybrano {{count}} monitor", + "bulkSelected_other": "Wybrano {{count}} monitor(รณw)", + "configure": "Skonfiguruj", + "delete": "Usuล„", + "details": "Szczegรณล‚y", + "generateToken": "Wygeneruj token", + "incidents": "Incydenty", + "inviteMember": "Zaproล› czล‚onka", + "openSite": "Otwรณrz stronฤ™", + "pause": "Wstrzymaj", + "resume": "Wznรณw", + "selectAll": "Zaznacz wszystkie monitory", + "selectMonitor": "Wybierz {{name}}" + }, + "bulkPause": { + "alreadyPaused_one": "Wybrany monitor jest juลผ wstrzymany", + "alreadyPaused_other": "Wszystkie wybrane monitory sฤ… juลผ wstrzymane", + "alreadyRunning_one": "Wybrany monitor jest juลผ uruchomiony", + "alreadyRunning_other": "Wszystkie wybrane monitory sฤ… juลผ uruchomione", + "partialFailure": "Zaktualizowano {{successCount}} monitor(รณw), ale {{failedCount}} nie udaล‚o siฤ™ zsynchronizowaฤ‡ z kolejkฤ… zadaล„", + "paused_one": "{{count}} monitor(รณw) wstrzymany", + "paused_other": "Wstrzymano {{count}} monitor(รณw)", + "resumed_one": "{{count}} monitor(รณw) wznowiony", + "resumed_other": "Wznowiono {{count}} monitor(รณw)" + }, + "monitorTypes": { + "optionDns": "DNS", + "optionDocker": "Docker", + "optionGame": "Gra", + "optionGrpc": "gRPC", + "optionHardware": "Infrastruktura", + "optionHttp": "HTTP(S)", + "optionPagespeed": "PageSpeed", + "optionPing": "Ping", + "optionPort": "Port", + "optionWebSocket": "WebSocket" + }, + "statBoxes": { + "activeFor": "Aktywne przez", + "certificateExpiry": "Wygaล›niฤ™cie certyfikatu", + "lastCheck": "Ostatnia kontrola", + "lastResponseTime": "Ostatni czas odpowiedzi", + "serviceIsDown": "Usล‚uga nie dziaล‚a" + }, + "status": { + "breached": "przekroczono prรณg", + "down": "niedostฤ™pny", + "initializing": "inicjowanie", + "maintenance": "konserwacja", + "paused": "wstrzymano", + "total": "ล‚ฤ…cznie", + "up": "dziaล‚a" + } + } + }, + "createMonitor": { + "form": { + "advanced": { + "description": "Opcjonalne ustawienia dla zaawansowanych przypadkรณw uลผycia", + "option": { + "advancedMatching": { + "label": "Uลผyj zaawansowanego dopasowania" + }, + "customUpCodes": { + "description": "Dodaj dowolne kody inne niลผ 2xx, ktรณre rรณwnieลผ powinny byฤ‡ uznane za pomyล›lne.", + "label": "Zaakceptowane kody statusu", + "removeAriaLabel": "Usuล„ kod statusu" + }, + "expectedValue": { + "label": "Oczekiwana wartoล›ฤ‡" + }, + "jsonPath": { + "description": "To wyraลผenie zostanie ocenione wzglฤ™dem danych JSON odpowiedzi, a wynik bฤ™dzie uลผyty do porรณwnania z oczekiwanฤ… wartoล›ciฤ…. Zobacz jmespath.org po dokumentacjฤ™ jฤ™zyka zapytaล„.", + "label": "Wyraลผenie JSONPath" + }, + "matchMethod": { + "equal": "Rรณwne", + "include": "Uwzglฤ™dnij", + "label": "Metoda dopasowania", + "regex": "Wyraลผenie regularne" + }, + "method": { + "description": { + "GET": "GET pobiera peล‚nฤ… odpowiedลบ, ล‚ฤ…cznie z treล›ciฤ…. Zaawansowane dopasowywanie jest w peล‚ni obsล‚ugiwane", + "HEAD": "HEAD pobiera tylko nagล‚รณwki odpowiedzi, aby zmniejszyฤ‡ obciฤ…ลผenie testowanego serwera. Wspiera dopasowywanie status kodu. Nie obsล‚uguje zaawansowanych dopasowywaล„, poniewaลผ nie ma treล›ci odpowiedzi." + }, + "label": "Metoda ลผฤ…dania" + } + }, + "title": "Ustawienia zaawansowane" + }, + "frequency": { + "description": "Jak czฤ™sto chcesz sprawdzaฤ‡ status tego monitora?", + "option": { + "frequency": { + "label": "Sprawdลบ czฤ™stotliwoล›ฤ‡", + "value": { + "fifteenMinutes": "15 minut", + "fifteenSeconds": "15 sekund", + "fiveMinutes": "5 minut", + "fourMinutes": "4 minuty", + "oneMinute": "1 minuta", + "tenMinutes": "10 minut", + "thirtyMinutes": "30 minut", + "thirtySeconds": "30 sekund", + "threeMinutes": "3 minuty", + "twoMinutes": "2 minuty" + } + } + }, + "title": "Czฤ™stotliwoล›ฤ‡ sprawdzania" + }, + "general": { + "description": { + "dns": "Monitoruj, czy okreล›lony serwer DNS odpowiada.", + "docker": "Wprowadลบ nazwฤ™ lub identyfikator kontenera Docker. Moลผesz uลผyฤ‡ nazwy kontenera (np. my-app) lub identyfikatora kontenera (peล‚ny 64-znakowy identyfikator lub skrรณcony identyfikator).", + "game": "Wprowadลบ adres IP lub nazwฤ™ hosta oraz numer portu do pingowania (np. 192.168.1.100 lub example.com) i wybierz typ gry. Niektรณre gry wymagajฤ… wล‚ฤ…czenia zapytaล„ serwera w pliku konfiguracyjnym โ€” sprawdลบ dokumentacjฤ™ swojego serwera gry. Zobacz listฤ™ obsล‚ugiwanych gier, aby uzyskaฤ‡ szczegรณล‚y dotyczฤ…ce protokoล‚u.", + "grpc": "Wprowadลบ nazwฤ™ hosta i port serwera gRPC, opcjonalnie okreล›l nazwฤ™ usล‚ugi do sprawdzania stanu oraz dodaj nazwฤ™.", + "hardware": "Monitoruj uลผycie procesora, pamiฤ™ci, dysku oraz temperaturฤ™ w swojej infrastrukturze. Adres URL punktu koล„cowego Capture musi zawieraฤ‡ peล‚nฤ… ล›cieลผkฤ™ /api/v1/metrics (np. http://192.168.1.10:5001/api/v1/metrics). Dla klucza autoryzacyjnego wklej tylko token โ€” nie dodawaj 'Bearer'.", + "http": "Wprowadลบ adres URL lub IP do monitorowania (np. https://example.com/ lub 192.168.1.100) oraz dodaj nazwฤ™, ktรณra pojawi siฤ™ย na dashboardzie.", + "pagespeed": "Monitoruj wydajnoล›ฤ‡ ล‚adowania strony, Core Web Vitals oraz wyniki optymalizacji swojej witryny.", + "ping": "Wprowadลบ adres IP lub nazwฤ™ hosta do pingowania (np. 192.168.1.100 lub example.com) oraz dodaj nazwฤ™ na dashboardzie.", + "port": "Wprowadลบ adres URL lub IP serwera, numer portu oraz nazwฤ™ na dashboardzie.", + "websocket": "Wprowadลบ adres URL WebSocket do monitorowania (np. wss://example.com/socket) i dodaj nazwฤ™." + }, + "option": { + "captureUrl": { + "label": "Punkt koล„cowy przechwytywania", + "placeholder": "http://192.168.1.10:5001/api/v1/metrics" + }, + "container": { + "label": "Nazwa/ID kontenera", + "placeholder": "moja-aplikacja lub abcd1234" + }, + "dnsRecordType": { + "label": "Typ rekordu DNS", + "value": { + "A": "A", + "AAAA": "AAAA", + "CNAME": "CNAME", + "MX": "MX", + "NS": "NS", + "TXT": "TXT" + } + }, + "dnsServer": { + "label": "Serwer DNS", + "placeholder": "8.8.8.8" + }, + "game": { + "label": "Wybierz grฤ™", + "placeholder": "Wybierz grฤ™" + }, + "grpcServiceName": { + "label": "Nazwa usล‚ugi", + "placeholder": "np. my.service.v1 (pozostaw puste, aby monitorowaฤ‡ ogรณlny stan zdrowia)" + }, + "host": { + "label": "Host", + "placeholder": "192.168.1.100 lub example.com" + }, + "name": { + "label": "Nazwa", + "placeholder": "np. Moja strona internetowa" + }, + "port": { + "label": "Port do monitorowania", + "placeholder": "80" + }, + "secret": { + "label": "Klucz autoryzacyjny", + "placeholder": "Wprowadลบ swรณj klucz" + }, + "strategy": { + "desktop": "Desktop", + "label": "Strategia", + "mobile": "Mobile" + }, + "url": { + "label": "URL", + "placeholder": "https://www.google.com" + }, + "wsUrl": { + "label": "WebSocket URL", + "placeholder": "wss://example.com/socket" + } + }, + "title": "Ustawienia ogรณlne" + }, + "geoChecks": { + "description": "Przeprowadzaj sprawdzenia z rรณลผnych lokalizacji geograficznych, aby monitorowaฤ‡ globalnฤ… dostฤ™pnoล›ฤ‡ i wydajnoล›ฤ‡.", + "option": { + "enabled": { + "label": "Wล‚ฤ…cz kontrole rozproszone geograficznie" + }, + "interval": { + "label": "Interwaล‚ sprawdzania", + "value": { + "fifteenMinutes": "15 minut", + "fiveMinutes": "5 minut", + "tenMinutes": "10 minut", + "thirtyMinutes": "30 minut" + } + }, + "locations": { + "label": "Lokalizacje", + "options": { + "AF": "Afryka", + "AS": "Azja", + "EU": "Europa", + "NA": "Ameryka Pรณล‚nocna", + "OC": "Oceania", + "SA": "Ameryka Poล‚udniowa" + }, + "placeholder": "Wybierz lokalizacje" + } + }, + "title": "Kontrole rozproszone geograficznie" + }, + "ignoreTls": { + "description": "Skonfiguruj weryfikacjฤ™ certyfikatu TLS/SSL dla poล‚ฤ…czeล„ HTTPS.", + "option": { + "tls": { + "label": "Ignoruj bล‚ฤ™dy TLS/SSL" + } + }, + "title": "Ustawienia TLS/SSL" + }, + "incidents": { + "description": "Do okreล›lenia, kiedy monitor przestaje dziaล‚aฤ‡, uลผywane jest okno przesuwne. Status monitora zmieni siฤ™ tylko wtedy, gdy procent sprawdzeล„ w oknie czasowym osiฤ…gnie okreล›lonฤ… wartoล›ฤ‡.", + "option": { + "checks": { + "label": "Liczba sprawdzeล„ w oknie czasowym" + }, + "percentage": { + "label": "Jaki procent sprawdzeล„ w oknie czasowym zakoล„czy siฤ™ niepowodzeniem/powodzeniem, zanim zmieni siฤ™ status monitora?" + } + }, + "title": "Incydenty" + }, + "notifications": { + "description": "Wybierz kanaล‚y powiadomieล„, ktรณrych chcesz uลผywaฤ‡", + "title": "Powiadomienia" + }, + "stats": { + "buttonText": "Wyczyล›ฤ‡ wszystkie statystyki", + "description": "Wyczyล›ฤ‡ caล‚ฤ… historiฤ™ monitorowania i statystyki dla Twojego zespoล‚u. Ta operacja jest nieodwracalna.", + "dialog": { + "confirm": "Tak, wyczyล›ฤ‡ wszystkie statystyki", + "description": "To spowoduje trwaล‚e usuniฤ™cie caล‚ej historii monitorowania, statystyk i danych sprawdzajฤ…cych dla Twojego zespoล‚u. Tej operacji nie moลผna cofnฤ…ฤ‡.", + "title": "Wyczyล›ciฤ‡ caล‚ฤ… historiฤ™ monitorowania?" + }, + "title": "Historia monitora" + }, + "tags": { + "description": "Wybierz tagi, ktรณre chcesz przypisaฤ‡ do tego monitora", + "title": "Tagi" + }, + "thresholds": { + "description": "Okreล›l progi, przy ktรณrych majฤ… byฤ‡ wyzwalane alerty dla tego monitora sprzฤ™tu.", + "option": { + "cpuThreshold": { + "label": "Prรณg alertu CPU (%)" + }, + "diskThreshold": { + "label": "Prรณg alertu dysku (%)" + }, + "memoryThreshold": { + "label": "Prรณg alertu pamiฤ™ci (%)" + }, + "tempThreshold": { + "label": "Prรณg alarmowy temperatury (ยฐC)" + } + }, + "title": "Progi alertรณw" + }, + "type": { + "description": "Wybierz typ sprawdzenia do wykonania", + "optionDnsDescription": "Monitoruj, czy okreล›lony serwer DNS odpowiada.", + "optionDockerDescription": "Uลผyj Dockera, aby monitorowaฤ‡, czy kontener jest uruchomiony.", + "optionGameDescription": "Monitoruj, czy konkretny serwer gry jest online.", + "optionGrpcDescription": "Monitoruj usล‚ugi gRPC za pomocฤ… standardowego protokoล‚u kontroli stanu.", + "optionHttpDescription": "Uลผyj protokoล‚u HTTP(S), aby monitorowaฤ‡ swojฤ… stronฤ™ internetowฤ… lub punkt koล„cowy API.", + "optionPingDescription": "Uลผyj ICMP Ping, aby monitorowaฤ‡, czy serwer jest online.", + "optionPortDescription": "Monitoruj, czy okreล›lony port na serwerze jest otwarty.", + "optionWebSocketDescription": "Monitoruj punkty koล„cowe WebSocket pod kฤ…tem stanu poล‚ฤ…czenia i czasu odpowiedzi.", + "title": "Typ" + }, + "url": { + "description": "Wyล›wietl adres IP lub URL monitora na publicznej stronie Status. Jeล›li ta opcja jest wyล‚ฤ…czona, zostanie wyล›wietlona tylko nazwa monitora, aby chroniฤ‡ poufne informacje.", + "option": { + "showURL": { + "disabled": "Wyล‚ฤ…czone", + "enabled": "Wล‚ฤ…czone", + "label": "Wyล›wietl IP/URL na stronie statusu" + } + }, + "title": "Monitoruj IP/URL na stronie statusu" + } + } + }, + "editUser": { + "dialog": { + "removeUser": { + "content": "Czy na pewno chcesz usunฤ…ฤ‡ {{name}} ze swojego zespoล‚u? Tej operacji nie moลผna cofnฤ…ฤ‡.", + "title": "Usuล„ uลผytkownika" + } + }, + "form": { + "roles": { + "description": "Przypisz role uลผytkownikowi. Moลผna wybraฤ‡ wiele rรณl.", + "title": "Role" + } + } + }, + "incidents": { + "dialog": { + "details": { + "analysis": "Analiza incydentu", + "comment": "Komentarz:", + "detailsLabel": "Szczegรณล‚y", + "downtime": "Czas niedostฤ™pnoล›ci:", + "message": "Wiadomoล›ฤ‡:", + "monitor": "Monitor:", + "overview": "Przeglฤ…d", + "resolutionDetails": "Szczegรณล‚y rozwiฤ…zania", + "resolutionType": "Typ:", + "resolutionTypes": { + "automatic": "Automatyczny", + "manual": "Rฤ™czne" + }, + "resolve": "Rozwiฤ…ลผ incydent", + "resolvedAt": "Rozwiฤ…zano o:", + "resolvedBy": "Rozwiฤ…zane przez:", + "startedAt": "Rozpoczฤ™to o:", + "status": "Status:", + "statusCode": "Kod statusu:", + "timeline": "Oล› czasu", + "title": "Szczegรณล‚y incydentu", + "url": "URL:" + }, + "resolveIncident": { + "option": { + "comment": { + "label": "Komentarz (opcjonalnie)", + "placeholder": "Dodaj komentarz dotyczฤ…cy rozwiฤ…zania..." + } + }, + "title": "Rozwiฤ…ลผ incydent" + } + }, + "fallback": { + "description": "Gdy monitor przestanie dziaล‚aฤ‡ lub zostanie przywrรณcony, incydent pojawi siฤ™ tutaj wraz z czasem, szczegรณล‚ami monitora i historiฤ… rozwiฤ…zaล„.", + "title": "Brak incydentรณw โ€” wszystkie systemy dziaล‚ajฤ… poprawnie" + }, + "filters": { + "allMonitors": "Wszystkie monitory", + "monitor": "Monitor", + "resolutionType": "Typ rozwiฤ…zania", + "resolutionTypes": { + "all": "Wszystkie", + "automatic": "Automatyczne", + "manual": "Rฤ™czny" + } + }, + "header": { + "description": "Gdy monitory przestajฤ… dziaล‚aฤ‡ lub zostajฤ… przywrรณcone, incydenty pojawiajฤ… siฤ™ tutaj wraz z informacjami o czasie, szczegรณล‚ach monitora i historiฤ… rozwiฤ…zaล„.", + "title": "Incydenty" + }, + "summaryCard": { + "activeIncidents": { + "active_one": "{{count}} aktywny incydent", + "active_other": "{{count}} aktywne incydenty", + "active_zero": "Brak aktywnych incydentรณw", + "title": "Aktywne incydenty" + }, + "incidentStats": { + "avgResolutionTime": "ลšredni czas rozwiฤ…zania", + "mostAffectedMonitor": "Najbardziej dotkniฤ™ty monitor", + "title": "Statystyki incydentรณw", + "totalIncidents": "ลฤ…czna liczba incydentรณw" + }, + "latestIncidents": { + "title": "Najnowsze incydenty" + } + }, + "table": { + "actions": { + "details": "Szczegรณล‚y", + "goToMonitor": "Przejdลบ do monitora", + "resolveManually": "Rozwiฤ…ลผ rฤ™cznie" + }, + "activeIncidents": "Aktywne incydenty", + "headers": { + "endTime": "Czas zakoล„czenia", + "resolutionType": "Typ rozwiฤ…zania", + "startTime": "Czas rozpoczฤ™cia", + "statusCode": "Kod statusu" + }, + "resolvedIncidents": "Rozwiฤ…zane incydenty", + "status": { + "active": "Aktywne", + "resolved": "Rozwiฤ…zany" + } + } + }, + "infrastructure": { + "charts": { + "labels": { + "cpu": "Uลผycie CPU", + "disk": "Zuลผycie dysku", + "loopbackOnlyNotice": "Przechwytywanie raportuje tylko interfejsy loopback lub container-bridge (lo, lo0, docker0). To normalne, gdy Capture dziaล‚a wewnฤ…trz kontenera, ktรณry widzi tylko wล‚asnฤ… przestrzeล„ nazw sieci โ€” uruchom Capture bezpoล›rednio na hoล›cie, aby zobaczyฤ‡ rzeczywistฤ… aktywnoล›ฤ‡ sieciowฤ….", + "memory": "Zuลผycie pamiฤ™ci", + "netBytesRecv": "{{name}} - Otrzymane bajty", + "netBytesSent": "{{name}} - Wysล‚ane bajty", + "netNoTraffic": "Capture raportuje zerowy ruch na wszystkich interfejsach. Jest to normalne, gdy Capture dziaล‚a w kontenerze z dostฤ™pem tylko do interfejsรณw loopback โ€” aby zobaczyฤ‡ rzeczywistฤ… aktywnoล›ฤ‡ sieciowฤ…, uruchom Capture bezpoล›rednio na hoล›cie.", + "temp": "Temperatura" + } + }, + "fallback": { + "actionButton": "Utwรณrz monitor", + "description": "Kontroluj wydajnoล›ฤ‡ serwera, znajdลบ wฤ…skie gardล‚a i zapewnij niezawodnoล›ฤ‡ dziฤ™ki monitorowaniu infrastruktury w czasie rzeczywistym.", + "title": "Brak monitorรณw infrastruktury" + }, + "gauges": { + "cpu": { + "lowerLabel": "Maksymalna czฤ™stotliwoล›ฤ‡", + "title": "Wykorzystanie procesora", + "upperLabel": "Aktualna czฤ™stotliwoล›ฤ‡" + }, + "disk": { + "lowerLabel": "Wolne", + "title": "Uลผycie dysku {{idx}}", + "upperLabel": "Uลผywane" + }, + "memory": { + "lowerLabel": "Wolne", + "title": "Zuลผycie pamiฤ™ci", + "upperLabel": "Uลผyte" + } + }, + "header": { + "description": "Kontroluj wydajnoล›ฤ‡ serwera, identyfikuj wฤ…skie gardล‚a i zapewnij niezawodnoล›ฤ‡ dziฤ™ki monitorowaniu infrastruktury w czasie rzeczywistym.", + "title": "Monitory infrastruktury" + }, + "statBoxes": { + "avgCpuTemperature": "ลšrednia temperatura CPU", + "cpuFrequency": "Czฤ™stotliwoล›ฤ‡ CPU", + "cpuLogical": "CPU (Logiczne)", + "cpuPhysical": "CPU (fizyczny)", + "disk": "Dysk", + "memory": "Pamiฤ™ฤ‡", + "os": "OS" + }, + "table": { + "headers": { + "cpu": "CPU", + "disk": "Dysk", + "memory": "Pamiฤ™ฤ‡" + } + }, + "tabs": { + "labels": { + "network": "Sieฤ‡", + "overview": "Przeglฤ…d" + } + } + }, + "logs": { + "diagnostics": { + "gauges": { + "allocatedPercentage": "% przydzielone", + "availableMemoryPercentage": "% dostฤ™pnej pamiฤ™ci", + "heapAllocation": "Alokacja sterty", + "heapUsage": "Zuลผycie sterty", + "heapUtilization": "Wykorzystanie sterty", + "instantCpuUsage": "Bieลผฤ…ce uลผycie CPU", + "total": "Razem", + "used": "Uลผywane", + "usedSPercentage": "% uลผycia 1s przez CPU" + }, + "mongoDBCollectionStats": { + "bucketCount": "Liczba kubeล‚kรณw: {{count}}", + "dbUsageTooltip": "{{percentage}}% przestrzeni DB", + "documents": "Dokumenty: {{count}}", + "indexSize": "Rozmiar indeksu: {{size}}", + "storageSize": "Rozmiar magazynu: {{size}}", + "totalSize": "Caล‚kowity rozmiar: {{size}}" + }, + "mongoDBStats": { + "dbName": "Nazwa bazy danych", + "deletesPerSecond": "Usuniฤ™cia/s", + "host": "Host", + "insertsPerSecond": "Wstawienia/s", + "port": "Port", + "readsPerSecond": "Odczyty/s", + "readyState": "Stan gotowoล›ci", + "storageSize": "Rozmiar pamiฤ™ci", + "updatesPerSecond": "Aktualizacje/s", + "writesPerSecond": "ลฤ…czna liczba zapisรณw/s" + }, + "sections": { + "memoryCpu": { + "description": "Jak proces wykorzystuje przydzielonฤ… mu pamiฤ™ฤ‡ i procesor hosta.", + "title": "Pamiฤ™ฤ‡ i CPU" + }, + "mongoDBCollectionStats": { + "description": "Statystyki dla kolekcji MongoDB", + "title": "Statystyki kolekcji MongoDB" + }, + "mongoDBStats": { + "description": "Migawka metryk MongoDB", + "title": "Statystyki MongoDB" + }, + "runtimeStats": { + "description": "Migawka procesu Node.js i maszyny hosta. Aktualizuje siฤ™ co 5 sekund.", + "title": "Statystyki ล›rodowiska uruchomieniowego" + } + }, + "stats": { + "eventLoopDelay": "Opรณลบnienie pฤ™tli zdarzeล„", + "osMemoryLimit": "Limit pamiฤ™ci OS", + "totalHeapSize": "Caล‚kowity rozmiar sterty", + "uptime": "Czas dziaล‚ania", + "usedHeapSize": "Uลผyty rozmiar sterty" + } + }, + "failedJobs": "Nieudane zadania", + "failedJobsExplainer": "Ostatnie niepowodzenia monitora โ€” czas ostatniego bล‚ฤ™du, liczba niepowodzeล„ oraz powรณd.", + "fallback": { + "description": "Aktywnoล›ฤ‡ serwera, ostrzeลผenia i bล‚ฤ™dy bฤ™dฤ… siฤ™ pojawiaฤ‡ tutaj podczas dziaล‚ania Checkmate.", + "title": "Brak logรณw" + }, + "header": { + "description": "Sprawdzaj logi serwera, monitoruj kolejkฤ™ zadaล„ i diagnozฤ™ dziaล‚ania w jednym miejscu.", + "title": "Dzienniki i kolejka" + }, + "jobQueue": "Kolejka zadaล„", + "jobQueueExplainer": "Wyrรณลผnione wiersze to zadania aktualnie przetwarzane przez pracownika.", + "logLevelSelect": { + "label": "Poziom logowania" + }, + "metrics": { + "activeJobs": "Aktywne zadania", + "failingJobs": "Nieudane zadania", + "jobs": "Zadania", + "totalFailures": "ลฤ…czna liczba bล‚ฤ™dรณw", + "totalRuns": "ลฤ…czna liczba uruchomieล„" + }, + "noLogs": "Nie znaleziono logรณw", + "queueFallback": { + "description": "Sprawdzenia monitorujฤ…ce, ktรณre sฤ… zaplanowane lub trwajฤ…, pojawiฤ… siฤ™ tutaj. Pusta kolejka oznacza, ลผe wszystko jest aktualne.", + "title": "Brak zadaล„ w kolejce" + }, + "table": { + "headers": { + "failCount": "Liczba", + "failReason": "Powรณd niepowodzenia", + "host": "Host", + "lastFailedAt": "Ostatnia nieudana prรณba", + "lastFinishedAt": "Ostatnie zakoล„czenie o", + "lastSeenAt": "Ostatnio widziano", + "level": "Poziom", + "lockedBy": "Zablokowane przez", + "method": "Metoda", + "monitorId": "Identyfikator monitora", + "nextScheduledAt": "Nastฤ™pne uruchomienie o", + "processId": "Identyfikator procesu", + "runCount": "Liczba uruchomieล„", + "service": "Usล‚uga", + "state": "Stan", + "timestamp": "Znacznik czasu", + "workerId": "ID pracownika", + "workerMode": "Tryb pracownika" + } + }, + "tabs": { + "diagnostics": "Diagnostyka", + "logs": "Logi", + "queue": "Kolejka zadaล„" + }, + "workers": "Pracownicy", + "workersExplainers": "Wyล›wietla procesy workerรณw. Identyfikatory workerรณw sฤ… skrรณcone, najedลบ, aby zobaczyฤ‡ peล‚ny identyfikator" + }, + "maintenanceWindow": { + "fallback": { + "actionButton": "Utwรณrz przerwฤ™ technicznฤ…", + "description": "Zaplanuj planowanฤ… przerwฤ™, aby powiadomienia byล‚y wyciszone, a Twรณj zespรณล‚ i uลผytkownicy wiedzieli, czego siฤ™ spodziewaฤ‡.", + "title": "Brak zaplanowanych okien serwisowych" + }, + "form": { + "general": { + "description": "Ustaw nazwฤ™ i opcjฤ™ powtarzania dla przerwy technicznej.", + "option": { + "name": { + "label": "Nazwa", + "placeholder": "np. Cotygodniowa przerwa techniczna" + }, + "repeat": { + "label": "Powtarzaj" + } + }, + "title": "Ustawienia ogรณlne" + }, + "startDate": { + "description": "Wybierz datฤ™ rozpoczฤ™cia przerwy technicznej.", + "option": { + "startDate": { + "label": "Data rozpoczฤ™cia" + } + }, + "title": "Data rozpoczฤ™cia" + }, + "startTime": { + "description": "Ustaw czas rozpoczฤ™cia i czas trwania przerwy technicznej. Wszystkie wartoล›ci sฤ… w UTC", + "monitors": { + "description": "Monitory, do ktรณrych naleลผy zastosowaฤ‡ przerwฤ™ technicznฤ…", + "option": { + "addMonitors": { + "label": "Dodaj monitory" + } + }, + "title": "Monitory" + }, + "option": { + "duration": { + "label": "Czas trwania" + }, + "startTime": { + "label": "Czas rozpoczฤ™cia" + } + }, + "title": "Czas rozpoczฤ™cia" + } + }, + "header": { + "description": "Zaplanuj planowanฤ… przerwฤ™, aby powiadomienia byล‚y wyciszone, a Twรณj zespรณล‚ i uลผytkownicy wiedzieli, czego siฤ™ spodziewaฤ‡.", + "title": "Przerwy techniczne" + }, + "table": { + "headers": { + "nextWindow": "Nastฤ™pna przerwa", + "repeat": "Powtรณrz" + } + } + }, + "notFound": { + "subtitle": "Adres URL nie istnieje lub nie masz do niego dostฤ™pu.", + "title": "Coล› siฤ™ popsuล‚o i nie byล‚o mnie sล‚ychaฤ‡..." + }, + "notifications": { + "fallback": { + "actionButton": "Utwรณrz kanaล‚", + "description": "Poล‚ฤ…cz Slacka, Discorda, e-mail, webhooki i inne, aby odpowiednie osoby dowiedziaล‚y siฤ™ o incydentach natychmiast po ich wystฤ…pieniu.", + "title": "Brak jeszcze kanaล‚รณw powiadomieล„" + }, + "form": { + "accessToken": { + "optionAccessToken": "Token dostฤ™pu", + "placeholder": "syt_YWxleF9ob2xsaWRheQ_VmtScmV0U2VjcmV0S2V5_abc123" + }, + "address": { + "description": "Adres, na ktรณry bฤ™dฤ… wysyล‚ane powiadomienia.", + "optionAddress": "Adres", + "placeholderEmail": "example@example.com", + "placeholderWebhook": "https://your-server.com/webhook", + "title": "Adres" + }, + "homeServer": { + "optionHomeServer": "Serwer domowy", + "placeholder": "example.com" + }, + "matrix": { + "description": "Skonfiguruj poล‚ฤ…czenie z serwerem Matrix dla powiadomieล„.", + "title": "Konfiguracja Matrix" + }, + "notificationName": { + "description": "Opisowa nazwa kanaล‚u powiadomieล„", + "optionName": "Nazwa kanaล‚u", + "placeholder": "np. alerty produkcyjne", + "title": "Nazwa kanaล‚u" + }, + "ntfy": { + "description": "Skonfiguruj serwer ntfy i temat do powiadomieล„ push.", + "optionServerUrl": "Adres URL serwera", + "optionTopic": "Temat", + "placeholderServerUrl": "https://ntfy.sh", + "placeholderTopic": "checkmate-alerts", + "title": "konfiguracja ntfy" + }, + "pagerDuty": { + "description": "Twรณj klucz integracyjny PagerDuty do otrzymywania powiadomieล„.", + "optionIntegrationKey": "Klucz integracyjny", + "placeholder": "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6", + "title": "Klucz integracji" + }, + "pushover": { + "description": "Skonfiguruj Pushover, aby otrzymywaฤ‡ powiadomienia push na swoich urzฤ…dzeniach.", + "optionAppToken": "Token API aplikacji", + "optionUserKey": "Klucz uลผytkownika", + "placeholderAppToken": "azGDORePK8gMaC0QOYAMyEEuzJnyUi", + "placeholderUserKey": "uQiRzpo4DXghDmr9QzzfQu27cmVRsG", + "title": "Konfiguracja Pushover" + }, + "roomId": { + "optionRoomId": "Identyfikator pokoju", + "placeholder": "!abcdefg:matrix.org" + }, + "telegram": { + "description": "Skonfiguruj swojego bota Telegram, aby wysyล‚aล‚ powiadomienia.", + "optionBotToken": "Token bota", + "optionChatId": "Identyfikator czatu", + "placeholderBotToken": "123456789:ABCdefGhIJKlmNoPQRsTUVwxyZ", + "placeholderChatId": "-1001234567890", + "title": "Konfiguracja Telegram" + }, + "twilio": { + "description": "Skonfiguruj Twilio, aby wysyล‚aฤ‡ powiadomienia SMS na numer telefonu.", + "optionAccountSid": "SID konta", + "optionAuthToken": "Token uwierzytelniajฤ…cy", + "optionFromNumber": "Numer nadawcy (Twilio)", + "optionToNumber": "Do numeru (odbiorca)", + "placeholderAccountSid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "placeholderAuthToken": "twรณj_token_uwierzytelniajฤ…cy", + "placeholderFromNumber": "+15551234567", + "placeholderToNumber": "+15559876543", + "title": "Konfiguracja SMS Twilio" + }, + "type": { + "description": "Wybierz typ kanaล‚u powiadomieล„ do utworzenia.", + "optionType": "Typ", + "title": "Typ kanaล‚u" + } + }, + "header": { + "description": "Poล‚ฤ…cz Slacka, Discorda, e-mail, webhooki i inne, aby odpowiednie osoby otrzymywaล‚y informacje o incydentach natychmiast po ich wystฤ…pieniu.", + "title": "Powiadomienia" + }, + "table": { + "headers": { + "destination": "Miejsce docelowe" + } + } + }, + "pageSpeed": { + "charts": { + "common": { + "cls": "Skumulowane przesuniฤ™cie ukล‚adu (CLS)", + "fcp": "Pierwsze wyrenderowanie treล›ci (FCP)", + "lcp": "Najwiฤ™ksze wyrenderowanie treล›ci (LCP)", + "si": "Indeks szybkoล›ci (SI)", + "tbt": "Caล‚kowity czas blokowania (TBT)" + }, + "legend": { + "title": "Raport PageSpeed", + "weight": "Waga" + }, + "pie": { + "title": "Raport wydajnoล›ci" + } + }, + "fallback": { + "actionButton": "Utwรณrz monitor", + "description": "Monitoruj wskaลบniki Core Web Vitals i wyniki Lighthouse w czasie, aby wykryฤ‡ regresje zanim zrobiฤ… to uลผytkownicy.", + "title": "Brak monitorรณw PageSpeed" + }, + "header": { + "description": "Monitoruj Core Web Vitals i wyniki Lighthouse w czasie, aby wykryฤ‡ regresje, zanim zauwaลผฤ… je uลผytkownicy.", + "title": "Monitory PageSpeed" + }, + "strategy": { + "desktop": "Desktop", + "mobile": "Mobile" + }, + "table": { + "headers": { + "pageSpeedScore": "Wynik PageSpeed" + } + } + }, + "settings": { + "form": { + "about": { + "developedBy": "Opracowane przez Bluewave Labs", + "title": "Informacje" + }, + "demoMonitors": { + "description": "Dodaj przykล‚adowe monitory do celรณw demonstracyjnych.", + "title": "Monitory demonstracyjne" + }, + "email": { + "description": "Skonfiguruj ustawienia e-mail dla swojego systemu. Sล‚uลผฤ… one do wysyล‚ania powiadomieล„ i alertรณw.", + "descriptionTransport": "To tworzy transport SMTP dla NodeMailera", + "descriptionTransportLink": "Zobacz specyfikacje tutaj", + "option": { + "address": { + "label": "Adres e-mail โ€“ uลผywany do uwierzytelniania", + "placeholder": "you@example.com" + }, + "connectionHost": { + "label": "Host poล‚ฤ…czenia e-mail โ€“ nazwa hosta uลผywana w powitaniu HELO/EHLO", + "placeholder": "localhost" + }, + "displayName": { + "label": "Nadawca (opcjonalnie) - Nazwa wyล›wietlana w polu 'Od'", + "placeholder": "Powiadomienia Checkmate" + }, + "host": { + "label": "Host e-mail โ€“ nazwa hosta lub adres IP do poล‚ฤ…czenia", + "placeholder": "smtp.gmail.com" + }, + "ignoreTLS": { + "label": "Wyล‚ฤ…cz STARTTLS: Nie uลผywaj TLS, nawet jeล›li serwer go obsล‚uguje" + }, + "password": { + "label": "Hasล‚o e-mail โ€“ Hasล‚o do uwierzytelniania", + "labelSet": "Hasล‚o jest ustawione. Kliknij Resetuj, aby je zmieniฤ‡.", + "placeholder": "Wprowadลบ swoje hasล‚o" + }, + "pool": { + "label": "Wล‚ฤ…cz ล‚ฤ…czenie poล‚ฤ…czeล„: Wykorzystaj istniejฤ…ce poล‚ฤ…czenia, aby poprawiฤ‡ wydajnoล›ฤ‡" + }, + "port": { + "label": "Port e-mail โ€” port do poล‚ฤ…czenia", + "placeholder": "587" + }, + "rejectUnauthorized": { + "label": "Odrzucaj nieprawidล‚owe certyfikaty: Odrzucaj poล‚ฤ…czenia z certyfikatami samopodpisanymi lub niezaufanymi" + }, + "requireTLS": { + "label": "Wymuล› STARTTLS: Wymagaj aktualizacji do TLS, odmรณw w przypadku braku obsล‚ugi" + }, + "secure": { + "label": "Uลผyj SSL (zalecane): Szyfruj poล‚ฤ…czenie za pomocฤ… SSL/TLS" + }, + "tlsServername": { + "label": "Nazwa serwera TLS - opcjonalna nazwa hosta do weryfikacji TLS, gdy hostem jest adres IP", + "placeholder": "example.com" + }, + "user": { + "label": "Uลผytkownik e-mail โ€” Nazwa uลผytkownika do uwierzytelniania, zastฤ™puje adres e-mail, jeล›li zostaล‚a podana", + "placeholder": "Pozostaw puste, jeล›li nie jest wymagane" + } + }, + "title": "Ustawienia e-mail" + }, + "exportMonitors": { + "title": "Eksportuj monitory" + }, + "importExportMonitors": { + "description": "Importuj lub eksportuj dane monitorรณw jako plik JSON w celu wykonania kopii zapasowej lub przeniesienia.", + "title": "Importuj / Eksportuj monitory" + }, + "pagespeed": { + "description": "Wprowadลบ swรณj klucz API Google PageSpeed, aby wล‚ฤ…czyฤ‡ monitorowanie Google PageSpeed. Kliknij Resetuj, aby zaktualizowaฤ‡ klucz.", + "option": { + "apiKey": { + "label": "Klucz API PageSpeed", + "labelSet": "Klucz API jest ustawiony. Kliknij Resetuj, aby go zmieniฤ‡.", + "placeholder": "Wprowadลบ swรณj klucz API Google PageSpeed" + } + }, + "title": "Klucz API Google PageSpeed" + }, + "removeMonitors": { + "description": "Usuล„ wszystkie monitory z systemu.", + "dialog": { + "description": "Tej operacji nie moลผna cofnฤ…ฤ‡.", + "paragraph": "Caล‚a historia sprawdzeล„, incydenty, zaplanowane okna konserwacyjne oraz wszystkie strony statusowe, ktรณre odnosiล‚y siฤ™ do tych monitorรณw, rรณwnieลผ zostanฤ… usuniฤ™te. Najpierw wyeksportuj swoje monitory, aby zachowaฤ‡ kopiฤ™ zapasowฤ… ich danych.", + "title": "Usunฤ…ฤ‡ wszystkie monitory?" + }, + "title": "Resetowanie systemu" + }, + "retention": { + "description": "Ustaw, jak dล‚ugo dane kontroli bฤ™dฤ… przechowywane przed ich automatycznym usuniฤ™ciem.", + "option": { + "days": { + "label": "Okres przechowywania (dni)", + "unlimited": "Bez ograniczeล„" + } + }, + "title": "Sprawdลบ retencjฤ™" + }, + "stats": { + "description": "Wyczyล›ฤ‡ caล‚ฤ… historiฤ™ monitorowania i statystyki dla Twojego zespoล‚u. Ta operacja jest nieodwracalna.", + "dialog": { + "description": "Tej operacji nie moลผna cofnฤ…ฤ‡", + "title": "Wyczyล›ciฤ‡ caล‚ฤ… historiฤ™ monitorowania?" + }, + "option": { + "clear": { + "label": "Wyczyล›ฤ‡ wszystkie statystyki. Tej operacji nie moลผna cofnฤ…ฤ‡." + } + }, + "title": "Monitoruj historiฤ™" + }, + "thresholds": { + "description": "Ustaw globalne progi CPU, pamiฤ™ci, dysku i temperatury dla monitorowania infrastruktury. Majฤ… one zastosowanie do wszystkich monitorรณw sprzฤ™tu, o ile nie zostanฤ… zastฤ…pione.", + "option": { + "cpu": { + "label": "Prรณg CPU (%)", + "placeholder": "1 - 100" + }, + "disk": { + "label": "Prรณg dysku (%)", + "placeholder": "1 - 100" + }, + "memory": { + "label": "Prรณg pamiฤ™ci (%)", + "placeholder": "1 - 100" + }, + "temperature": { + "label": "Prรณg temperatury (ยฐC)", + "placeholder": "1 - 150" + } + }, + "title": "Progi globalne" + }, + "timezone": { + "description": "Wybierz strefฤ™ czasowฤ… uลผywanฤ… do wyล›wietlania dat i godzin w caล‚ej aplikacji.", + "option": { + "timezone": { + "label": "Wyล›wietlanie strefy czasowej" + } + }, + "title": "Wyล›wietlana strefa czasowa" + }, + "ui": { + "description": "Przeล‚ฤ…czaj miฤ™dzy trybem jasnym i ciemnym, zmieniaj jฤ™zyk lub dostosuj typ wyล›wietlania wykresu.", + "option": { + "chartType": { + "heatmap": "Mapa ciepล‚a", + "histogram": "Histogram", + "label": "Typ wykresu" + }, + "language": { + "label": "Jฤ™zyk" + }, + "theme": { + "dark": "Ciemny", + "label": "Tryb motywu", + "light": "Jasny" + } + }, + "title": "Wyglฤ…d" + }, + "url": { + "description": "Wyล›wietl adres IP lub URL monitora na publicznej stronie Status. Jeล›li opcja jest wyล‚ฤ…czona, wyล›wietlana bฤ™dzie tylko nazwa monitora, aby chroniฤ‡ poufne informacje.", + "option": { + "showURL": { + "disabled": "Wyล‚ฤ…czone", + "enabled": "Wล‚ฤ…czone", + "label": "Wyล›wietl IP/URL na stronie statusu" + } + }, + "title": "Monitoruj IP/URL na stronie statusu" + }, + "validation": { + "errorMessage": "Proszฤ™ poprawiฤ‡ nastฤ™pujฤ…ce bล‚ฤ™dy walidacji:" + } + }, + "header": { + "description": "Skonfiguruj SMTP, integracje, retencjฤ™ oraz globalne zachowanie aplikacji.", + "title": "Ustawienia" + } + }, + "statusPages": { + "deleteSuccess": "Strona statusu zostaล‚a pomyล›lnie usuniฤ™ta", + "details": { + "empty": { + "addMonitor": "Dodaj monitor, aby rozpoczฤ…ฤ‡", + "title": "Nie ma tu jeszcze nic" + } + }, + "editorial": { + "allUp": "Wszystkie systemy dziaล‚ajฤ… prawidล‚owo.", + "reportTitle": "Raport serwisowy" + }, + "fallback": { + "actionButton": "Utwรณrz stronฤ™ statusu", + "description": "Opublikuj publicznฤ… stronฤ™, ktรณra pokazuje w czasie rzeczywistym historiฤ™ dostฤ™pnoล›ci i incydentรณw dla Twoich klientรณw i interesariuszy.", + "title": "Brak stron statusu" + }, + "footer": { + "poweredBy": "Powered by" + }, + "form": { + "access": { + "description": "Jeล›li Twoja strona statusu jest gotowa, moลผesz oznaczyฤ‡ jฤ… jako opublikowanฤ….", + "option": { + "published": { + "name": "Opublikowane i widoczne publicznie" + } + }, + "title": "Dostฤ™p" + }, + "appearance": { + "description": "Zdefiniuj domyล›lny wyglฤ…d i styl swojej publicznej strony statusu.", + "option": { + "color": { + "label": "Kolor marki" + }, + "logo": { + "label": "Logo" + } + }, + "title": "Wyglฤ…d" + }, + "basicInfo": { + "description": "Zdefiniuj nazwฤ™ firmy oraz subdomenฤ™, na ktรณrฤ… wskazuje Twoja strona statusu.", + "option": { + "customDomain": { + "helper": "Opcjonalne. Skieruj rekord DNS na tฤ™ instancjฤ™ Checkmate, aby wyล›wietlaฤ‡ stronฤ™ statusu pod wล‚asnฤ… domenฤ….", + "label": "Niestandardowa domena", + "placeholder": "status.przyklad.com" + }, + "name": { + "label": "Nazwa firmy", + "placeholder": "Acme Inc." + }, + "url": { + "label": "Adres Twojej strony statusowej", + "placeholder": "moja-strona-statusowa" + } + }, + "title": "Podstawowe informacje" + }, + "customCSS": { + "description": "Dodaj wล‚asny CSS, aby precyzyjnie dostosowaฤ‡ wyglฤ…d swojej publicznej strony statusu.", + "option": { + "customCSS": { + "label": "Niestandardowy CSS", + "placeholder": "footer { display: none; }" + } + }, + "title": "Niestandardowy CSS" + }, + "features": { + "description": "Skonfiguruj, jakie informacje sฤ… wyล›wietlane na Twojej stronie statusu.", + "option": { + "showAdminLoginLink": { + "label": "Pokaลผ link do logowania administratora" + }, + "showCharts": { + "label": "Pokaลผ wykresy czasu odpowiedzi" + }, + "showInfrastructure": { + "label": "Pokaลผ metryki infrastruktury" + }, + "showUptimePercentage": { + "label": "Pokaลผ procent czasu dostฤ™pnoล›ci" + } + }, + "title": "Funkcje" + }, + "monitors": { + "description": "Wybierz monitory do wyล›wietlenia na swojej stronie statusu.", + "noMonitors": "Nie wybrano monitorรณw", + "option": { + "monitors": { + "label": "Wybierz monitory", + "placeholder": "Wyszukaj i wybierz monitory..." + } + }, + "title": "Monitory" + }, + "theme": { + "description": "Wybierz styl wizualny dla swojej publicznej strony statusu.", + "label": "Motyw", + "options": { + "bold": { + "description": "Duลผa czcionka i wyraลบne odstฤ™py. Idealne dla stron nastawionych na budowanie marki.", + "name": "Pogrubiony" + }, + "editorial": { + "description": "Nagล‚รณwki szeryfowe i cienkie linie oddzielajฤ…ce. Wyrazisty i spokojny.", + "name": "Redakcyjny" + }, + "minimal": { + "description": "Prosty i kompaktowy, zawierajฤ…cy tylko najwaลผniejsze elementy.", + "name": "Minimalistyczny" + }, + "modern": { + "description": "SaaS'owy z pulsujฤ…cฤ… kropkฤ… statusu i delikatnie zanikajฤ…cymi kartami.", + "name": "Nowoczesny" + }, + "refined": { + "description": "Czysty, profesjonalny, zbliลผony do klasycznego wyglฤ…du Checkmate.", + "name": "Wyrafinowany" + } + }, + "title": "Motyw strony" + }, + "themeMode": { + "auto": "Automatycznie (zgodnie z urzฤ…dzeniem odwiedzajฤ…cego)", + "dark": "Tylko ciemny", + "label": "Tryb motywu", + "light": "Tylko jasny" + }, + "timezone": { + "description": "Wybierz strefฤ™ czasowฤ…, w ktรณrej bฤ™dzie wyล›wietlana Twoja strona statusu.", + "option": { + "timezone": { + "label": "Strefa czasowa", + "placeholder": "Wybierz strefฤ™ czasowฤ…..." + } + }, + "title": "Strefa czasowa" + } + }, + "header": { + "description": "Publikuj publiczne strony pokazujฤ…ce aktualny czas dziaล‚ania i historiฤ™ incydentรณw swoim klientom i interesariuszom.", + "title": "Strony statusu" + }, + "monitorsList": { + "chart": { + "avg": "ล›r. {{value}}ms", + "downTooltip": "Niedostฤ™pny", + "heatmapAria": "Heatmapa czasu odpowiedzi", + "max": "maks. {{value}}ms" + }, + "chartTypeHeatmap": "Heatmapa", + "chartTypeHistogram": "Histogram", + "infrastructure": { + "cpu": "CPU", + "disk": "Dysk", + "memory": "RAM", + "memoryText": "Pamiฤ™ฤ‡", + "title": "Infrastruktura", + "total": "ลฤ…cznie", + "usage": "Uลผycie", + "used": "Uลผywane" + }, + "noData": "Brak dostฤ™pnych danych", + "status": { + "breached": "Przekroczono prรณg", + "down": "Niedostฤ™pny", + "initializing": "Inicjowanie", + "maintenance": "Konserwacja", + "paused": "Wstrzymano", + "up": "Dziaล‚a poprawnie" + }, + "uptime": { + "responseTime": "Czas odpowiedzi", + "title": "Czas pracy" + } + }, + "preview": { + "title": "Podglฤ…d strony statusu" + }, + "statusBar": { + "allBreached": "Wszystkie systemy przekroczyล‚y progi", + "allDown": "Wszystkie systemy sฤ… niedostฤ™pne", + "allInitializing": "Wszystkie systemy sฤ… inicjalizowane", + "allMaintenance": "Wszystkie systemy sฤ… w trakcie konserwacji", + "allPaused": "Wszystkie systemy sฤ… wstrzymane", + "allUp": "Wszystkie systemy dziaล‚ajฤ… poprawnie", + "breached": "Niektรณre systemy przekroczyล‚y progi", + "breachedAndDown": "Niektรณre systemy przekroczyล‚y progi i sฤ… nieaktywne", + "degraded": "Niektรณre systemy majฤ… problemy", + "initializing": "Niektรณre systemy sฤ… inicjalizowane", + "maintenance": "Niektรณre systemy sฤ… w trakcie konserwacji", + "maintenanceAndDown": "Niektรณre systemy sฤ… niedostฤ™pne podczas konserwacji", + "monitoringSummary": "Monitorowanie {{count}} usล‚ugi ยท Aktualizacje co 10 sekund", + "monitoringSummary_other": "Monitorowanie {{count}} usล‚ug ยท Aktualizacje co 10 sekund", + "noMonitors": "Nie skonfigurowano ลผadnych monitorรณw", + "partiallyPaused": "Niektรณre systemy sฤ… wstrzymane", + "unknown": "Nie moลผna okreล›liฤ‡ stanu systemu" + }, + "table": { + "headers": { + "name": "Nazwa strony statusu", + "url": "Publiczny adres URL" + }, + "published": "Opublikowano", + "unpublished": "Nieopublikowane" + }, + "title": "Strony statusu" + }, + "tags": { + "fallback": { + "actionButton": "Utwรณrz tag", + "description": "Organizuj swoje monitory za pomocฤ… tagรณw. Po utworzeniu tagu i przypisaniu go do monitora, bฤ™dziesz mรณgล‚ filtrowaฤ‡ wedล‚ug tego tagu w caล‚ej aplikacji i zobaczyฤ‡ szczegรณล‚y tagu tutaj.", + "title": "Brak jeszcze tagรณw" + }, + "form": { + "color": { + "description": "Wybierz kolor dla swojego tagu", + "optionName": "Kolor tagu", + "placeholder": "np. #13715B", + "title": "Kolor etykiety" + }, + "name": { + "description": "Wybierz nazwฤ™ dla swojego tagu", + "optionName": "Nazwa tagu", + "placeholder": "np. Grupa 1", + "title": "Nazwa tagu" + } + }, + "header": { + "description": "Porzฤ…dkuj swoje monitory za pomocฤ… tagรณw", + "title": "Tagi" + }, + "table": { + "headers": { + "appearance": "Wyglฤ…d", + "color": "Kolor" + } + } + }, + "uptime": { + "fallback": { + "actionButton": "Utwรณrz monitor", + "description": "Monitoruj punkty koล„cowe HTTP, pingowania, kontenery i porty โ€” i otrzymuj powiadomienia natychmiast, gdy coล› przestanie dziaล‚aฤ‡.", + "title": "Brak monitorรณw dostฤ™pnoล›ci" + }, + "filters": { + "activeTags": "Aktywne tagi", + "search": { + "placeholder": "Wyszukaj monitory..." + } + }, + "header": { + "description": "Monitoruj punkty koล„cowe HTTP, ping, kontenery i porty โ€” i otrzymuj powiadomienia natychmiast, gdy coล› przestanie dziaล‚aฤ‡.", + "title": "Monitory dostฤ™pnoล›ci" + }, + "table": { + "headers": { + "responseTime": "Czas odpowiedzi", + "uptime": "Czas dziaล‚ania %" + } + } + } + } +} diff --git a/docker/dist-arm/server.Dockerfile b/docker/Dockerfile similarity index 64% rename from docker/dist-arm/server.Dockerfile rename to docker/Dockerfile index 5bdefd5bf..380210f53 100644 --- a/docker/dist-arm/server.Dockerfile +++ b/docker/Dockerfile @@ -4,7 +4,7 @@ # 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 +FROM --platform=$BUILDPLATFORM node:22-slim AS frontend-build WORKDIR /app/client @@ -19,7 +19,7 @@ RUN npm run build # --------------------- # Backend stage # --------------------- -FROM node:22-slim AS backend +FROM node:22-slim AS app # Install ping RUN apt-get update \ @@ -28,23 +28,18 @@ RUN apt-get update \ WORKDIR /app/server -COPY server/package.json ./ +COPY server/package*.json ./ -RUN npm install +RUN npm ci COPY server ./ -RUN chmod +x ./scripts/inject-vars.sh - RUN npm run build COPY --from=frontend-build /app/client/dist ./public -RUN chown -R node:node ./public \ - && chown -R node:node ./dist \ - && chown -R node:node ./scripts +USER node +EXPOSE 52345 52346 -EXPOSE 52345 - -CMD ["sh", "-c", "./scripts/inject-vars.sh && node ./dist/index.js"] +CMD ["node", "dist/index.js"] diff --git a/docker/coolify/build_images.bat b/docker/coolify/build_images.bat deleted file mode 100644 index b940039b6..000000000 --- a/docker/coolify/build_images.bat +++ /dev/null @@ -1,33 +0,0 @@ -@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_server -set dockerfiles[2]=.\docker\coolify\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 diff --git a/docker/coolify/build_images.sh b/docker/coolify/build_images.sh deleted file mode 100755 index 1d29ae5b1..000000000 --- a/docker/coolify/build_images.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/bin/bash -# Change directory to root Server directory for correct Docker Context -cd "$(dirname "$0")" -cd ../../ - -# Define an array of services and their Dockerfiles -declare -A services=( - ["bluewaveuptime/uptime_client"]="./docker/coolify/client.Dockerfile" - ["bluewaveuptime/uptime_database_mongo"]="./docker/coolify/mongoDB.Dockerfile" - ["bluewaveuptime/uptime_server"]="./docker/coolify/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" diff --git a/docker/coolify/client.Dockerfile b/docker/coolify/client.Dockerfile deleted file mode 100755 index 8c3853b99..000000000 --- a/docker/coolify/client.Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -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 ./server/docker/dist/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf -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;"] diff --git a/docker/coolify/mongoDB.Dockerfile b/docker/coolify/mongoDB.Dockerfile deleted file mode 100755 index 969a320c2..000000000 --- a/docker/coolify/mongoDB.Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM mongo -EXPOSE 27017 -CMD ["mongod"] diff --git a/docker/coolify/nginx/conf.d/default.conf b/docker/coolify/nginx/conf.d/default.conf deleted file mode 100755 index 0592dcd06..000000000 --- a/docker/coolify/nginx/conf.d/default.conf +++ /dev/null @@ -1,35 +0,0 @@ -server { - listen 80; - listen [::]:80; - - server_name checkmate-demo.bluewavelabs.ca; - server_tokens off; - - location /.well-known/acme-challenge/ { - root /var/www/certbot; - } - - location / { - root /usr/share/nginx/html; - index index.html index.htm; - try_files $uri $uri/ /index.html; - } - - location /api/ { - proxy_pass http://server:5000/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:5000/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; - } -} \ No newline at end of file diff --git a/docker/coolify/server.Dockerfile b/docker/coolify/server.Dockerfile deleted file mode 100755 index e786a0cbf..000000000 --- a/docker/coolify/server.Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM node:22-slim - -# Install ping -RUN apt-get update \ - && apt-get install -y iputils-ping \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app - -COPY ../../package*.json ./ - -RUN npm install - -COPY ../../ ./ - -RUN npm run build - -EXPOSE 5000 - -CMD ["node", "dist/index.js"] diff --git a/docker/dev/build_images.bat b/docker/dev/build_images.bat deleted file mode 100644 index ec3889418..000000000 --- a/docker/dev/build_images.bat +++ /dev/null @@ -1,33 +0,0 @@ -@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 \ No newline at end of file diff --git a/docker/dev/build_images.sh b/docker/dev/build_images.sh deleted file mode 100755 index eb43e31d8..000000000 --- a/docker/dev/build_images.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -# Change directory to root Server directory for correct Docker Context -cd "$(dirname "$0")" -cd ../.. - -# Define service names and their corresponding Dockerfiles in parallel arrays -services=("uptime_client" "uptime_mongo" "uptime_server") -dockerfiles=( - "./docker/dev/client.Dockerfile" - "./docker/dev/mongoDB.Dockerfile" - "./docker/dev/server.Dockerfile" -) - -# Loop through each service and build the corresponding image -for i in "${!services[@]}"; do - service="${services[$i]}" - dockerfile="${dockerfiles[$i]}" - - docker build -f "$dockerfile" -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" \ No newline at end of file diff --git a/docker/dev/client.Dockerfile b/docker/dev/client.Dockerfile deleted file mode 100755 index df3ac7439..000000000 --- a/docker/dev/client.Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -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;"] \ No newline at end of file diff --git a/docker/dev/docker-compose.custom-ca-example.yaml b/docker/dev/docker-compose.custom-ca-example.yaml index e1f185ae9..e086d6feb 100644 --- a/docker/dev/docker-compose.custom-ca-example.yaml +++ b/docker/dev/docker-compose.custom-ca-example.yaml @@ -1,29 +1,14 @@ # Dev/Test only: Not required in production # Example Docker Compose configuration for custom CA trust # This file demonstrates how to configure Checkmate to trust custom CAs -# -# Usage: docker-compose -f docker-compose.yaml -f docker-compose.custom-ca-example.yaml up +# +# Usage: docker compose -f docker-compose.yaml -f docker-compose.custom-ca-example.yaml up services: - server: - image: uptime_server:latest - restart: always - ports: - - "52345:52345" - env_file: - - server.env + checkmate: environment: # Mount your custom CA certificate NODE_EXTRA_CA_CERTS: /certs/custom-ca.pem volumes: # Mount the certs directory (read-only for security) - ./certs:/certs:ro - depends_on: - - mongodb - # Optional: Add healthcheck to ensure the server starts properly - healthcheck: - test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:52345/api/v1/health"] - interval: 30s - timeout: 10s - retries: 3 - start_period: 40s diff --git a/docker/dev/docker-compose.yaml b/docker/dev/docker-compose.yaml index 09c12fe18..1dba10fae 100755 --- a/docker/dev/docker-compose.yaml +++ b/docker/dev/docker-compose.yaml @@ -1,27 +1,20 @@ services: - client: - image: uptime_client:latest - restart: always - ports: - - "80:80" - environment: - UPTIME_APP_API_BASE_URL: "http://localhost:52345/api/v1" - UPTIME_APP_CLIENT_HOST: "http://localhost" - volumes: - - ./nginx/conf.d:/etc/nginx/conf.d/ - depends_on: - - server - server: - image: uptime_server:latest + checkmate: + build: + context: ../.. + dockerfile: docker/Dockerfile restart: always ports: - "52345:52345" env_file: - server.env depends_on: - - mongodb + mongodb: + condition: service_healthy worker: - image: uptime_server:latest + build: + context: ../.. + dockerfile: docker/Dockerfile restart: always ports: - "52346:52346" @@ -43,9 +36,10 @@ services: timeout: 3s retries: 3 depends_on: - - mongodb + mongodb: + condition: service_healthy mongodb: - image: uptime_mongo:latest + image: mongo:8.0 restart: always ports: - "27017:27017" diff --git a/docker/dev/mongoDB.Dockerfile b/docker/dev/mongoDB.Dockerfile deleted file mode 100755 index 969a320c2..000000000 --- a/docker/dev/mongoDB.Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM mongo -EXPOSE 27017 -CMD ["mongod"] diff --git a/docker/dev/nginx/conf.d/default.conf b/docker/dev/nginx/conf.d/default.conf deleted file mode 100755 index e2c2a7268..000000000 --- a/docker/dev/nginx/conf.d/default.conf +++ /dev/null @@ -1,35 +0,0 @@ -server { - listen 80 default_server; - listen [::]:80 default_server; - - server_name localhost; # Set server name to localhost - 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: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; - # proxy_set_header Connection ''; - # chunked_transfer_encoding off; - # proxy_buffering off; - # proxy_cache off; - # } - - # location /api-docs/ { - # proxy_pass http://server: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; - # } -} \ No newline at end of file diff --git a/docker/dev/server.Dockerfile b/docker/dev/server.Dockerfile deleted file mode 100755 index 8a9af51d1..000000000 --- a/docker/dev/server.Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM node:22-slim - -# Install ping -RUN apt-get update \ - && apt-get install -y iputils-ping \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app - -COPY ./server/package*.json ./ - -RUN npm install - -COPY ./server/ ./ - -RUN npm run build - -EXPOSE 52345 - -CMD ["node", "dist/index.js"] \ No newline at end of file diff --git a/docker/dist-arm/build_images.sh b/docker/dist-arm/build_images.sh deleted file mode 100755 index 2780e5359..000000000 --- a/docker/dist-arm/build_images.sh +++ /dev/null @@ -1,44 +0,0 @@ -#!/bin/bash - -set -e # Exit on error - -# Change directory to root Server directory for correct Docker Context -cd "$(dirname "$0")" -cd ../.. - -# Ensure buildx builder exists and is used -if ! docker buildx inspect mybuilder &>/dev/null; then - docker buildx create --name mybuilder --use -else - docker buildx use mybuilder -fi - -# Define service names and Dockerfiles -services=("mono_server_arm") -dockerfiles=( - "./docker/dist-arm/server.Dockerfile" -) - -# Static image name for GitHub Container Registry (GHCR) -image="ghcr.io/bluewave-labs/checkmate:backend-dist-multi-arch" -platforms="linux/amd64,linux/arm64" -repo_url="https://github.com/bluewave-labs/checkmate" - -# Loop through each service and build -for i in "${!services[@]}"; do - service="${services[$i]}" - dockerfile="${dockerfiles[$i]}" - - echo "๐Ÿš€ Building multi-arch image for $service..." - docker buildx build \ - --platform "$platforms" \ - -f "$dockerfile" \ - -t "$image" \ - --label "org.opencontainers.image.source=$repo_url" \ - --push \ - . - - echo "โœ… $image pushed for platforms: $platforms" -done - -echo "๐ŸŽ‰ All multi-arch images built and pushed successfully" diff --git a/docker/dist-arm/docker-compose.yaml b/docker/dist-arm/docker-compose.yaml deleted file mode 100644 index 49f81fe9a..000000000 --- a/docker/dist-arm/docker-compose.yaml +++ /dev/null @@ -1,28 +0,0 @@ -services: - server: - image: ghcr.io/bluewave-labs/checkmate-backend-mono-multiarch:latest - restart: always - pull_policy: always - ports: - - "52345:52345" - environment: - - UPTIME_APP_API_BASE_URL=http://localhost:52345/api/v1 - - UPTIME_APP_CLIENT_HOST=http://localhost - - DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db - - CLIENT_HOST=http://localhost - - JWT_SECRET=my_secret - depends_on: - - mongodb - mongodb: - image: mongo:6.0 - restart: always - command: ["mongod", "--quiet", "--bind_ip_all"] - volumes: - - ./mongo/data:/data/db - healthcheck: - test: ["CMD", "mongosh", "--eval", "db.runCommand({ ping: 1 })", "--quiet"] - interval: 5s - timeout: 30s - start_period: 0s - start_interval: 1s - retries: 30 diff --git a/docker/dist-mono/build_images.bat b/docker/dist-mono/build_images.bat deleted file mode 100644 index 3e1d39fe1..000000000 --- a/docker/dist-mono/build_images.bat +++ /dev/null @@ -1,30 +0,0 @@ -@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 diff --git a/docker/dist-mono/build_images.sh b/docker/dist-mono/build_images.sh deleted file mode 100755 index e3a0233d4..000000000 --- a/docker/dist-mono/build_images.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/bash - -# Change directory to root Server directory for correct Docker Context -cd "$(dirname "$0")" -cd ../.. - -# Define service names and their corresponding Dockerfiles in parallel arrays -services=("mono_mongo" "mono_server") -dockerfiles=( - "./docker/dist-mono/mongoDB.Dockerfile" - "./docker/dist-mono/server.Dockerfile" -) - -# Loop through each service and build the corresponding image -for i in "${!services[@]}"; do - service="${services[$i]}" - dockerfile="${dockerfiles[$i]}" - - docker build -f "$dockerfile" -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" \ No newline at end of file diff --git a/docker/dist-mono/docker-compose-local.yaml b/docker/dist-mono/docker-compose-local.yaml deleted file mode 100755 index a5616695b..000000000 --- a/docker/dist-mono/docker-compose-local.yaml +++ /dev/null @@ -1,27 +0,0 @@ -services: - server: - image: mono_server - restart: always - ports: - - "52345:52345" - environment: - - UPTIME_APP_API_BASE_URL=http://localhost:52345/api/v1 - - UPTIME_APP_CLIENT_HOST=http://localhost - - DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db - - CLIENT_HOST=http://localhost - - JWT_SECRET=my_secret - depends_on: - - mongodb - mongodb: - image: mono_mongo - restart: always - command: ["mongod", "--quiet", "--bind_ip_all"] - volumes: - - ./mongo/data:/data/db - healthcheck: - test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet"] - interval: 5s - timeout: 30s - start_period: 0s - start_interval: 1s - retries: 30 diff --git a/docker/dist-mono/docker-compose.yaml b/docker/dist-mono/docker-compose.yaml deleted file mode 100755 index 9553757b9..000000000 --- a/docker/dist-mono/docker-compose.yaml +++ /dev/null @@ -1,28 +0,0 @@ -services: - server: - image: ghcr.io/bluewave-labs/checkmate-backend-mono:latest - pull_policy: always - restart: always - ports: - - "52345:52345" - environment: - - UPTIME_APP_API_BASE_URL=http://localhost:52345/api/v1 - - UPTIME_APP_CLIENT_HOST=http://localhost - - DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db - - CLIENT_HOST=http://localhost - - JWT_SECRET=my_secret - depends_on: - - mongodb - mongodb: - image: ghcr.io/bluewave-labs/checkmate-mongo:latest - restart: always - command: ["mongod", "--quiet", "--bind_ip_all"] - volumes: - - ./mongo/data:/data/db - healthcheck: - test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet"] - interval: 5s - timeout: 30s - start_period: 0s - start_interval: 1s - retries: 30 diff --git a/docker/dist-mono/mongoDB.Dockerfile b/docker/dist-mono/mongoDB.Dockerfile deleted file mode 100755 index 969a320c2..000000000 --- a/docker/dist-mono/mongoDB.Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM mongo -EXPOSE 27017 -CMD ["mongod"] diff --git a/docker/dist-mono/server.Dockerfile b/docker/dist-mono/server.Dockerfile deleted file mode 100644 index 1e0eb303a..000000000 --- a/docker/dist-mono/server.Dockerfile +++ /dev/null @@ -1,37 +0,0 @@ -FROM node:22-slim AS frontend-build - -WORKDIR /app/client - -COPY client/package*.json ./ -RUN npm install - -COPY client ./ -RUN npm run build - -FROM node:22-slim AS app - -# Install ping -RUN apt-get update \ - && apt-get install -y iputils-ping \ - && rm -rf /var/lib/apt/lists/* - -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 - -RUN chmod +x ./scripts/inject-vars.sh \ - && chown -R node:node ./public \ - && chown -R node:node ./dist \ - && chown -R node:node ./scripts - - -EXPOSE 52345 - -CMD ["sh", "-c", "./scripts/inject-vars.sh && node ./dist/index.js"] diff --git a/docker/dist/build_images.bat b/docker/dist/build_images.bat deleted file mode 100644 index 92b5c4a9d..000000000 --- a/docker/dist/build_images.bat +++ /dev/null @@ -1,33 +0,0 @@ -@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 diff --git a/docker/dist/build_images.sh b/docker/dist/build_images.sh deleted file mode 100755 index 84fbb7d5a..000000000 --- a/docker/dist/build_images.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/bin/bash -# Change directory to root Server directory for correct Docker Context -cd "$(dirname "$0")" -cd ../../ - -# Define an array of services and their Dockerfiles -declare -A services=( - ["bluewaveuptime/uptime_client"]="./docker/dist/client.Dockerfile" - ["bluewaveuptime/uptime_database_mongo"]="./docker/dist/mongoDB.Dockerfile" - ["bluewaveuptime/uptime_server"]="./docker/dist/server.Dockerfile" -) - -for service in "${!services[@]}"; do - docker build \ - -f "${services[$service]}" \ - -t "$service" \ - . - - if [ $? -ne 0 ]; then - echo "Error building $service image. Exiting..." - exit 1 - fi -done - -echo "All images built successfully" diff --git a/docker/dist/client.Dockerfile b/docker/dist/client.Dockerfile deleted file mode 100755 index 9b42654b0..000000000 --- a/docker/dist/client.Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -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 ./docker/dist/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf -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;"] diff --git a/docker/dist/docker-compose.yaml b/docker/dist/docker-compose.yaml deleted file mode 100755 index 2e845daab..000000000 --- a/docker/dist/docker-compose.yaml +++ /dev/null @@ -1,38 +0,0 @@ -services: - client: - image: ghcr.io/bluewave-labs/checkmate-client:latest - restart: always - environment: - UPTIME_APP_API_BASE_URL: "http://localhost:52345/api/v1" - UPTIME_APP_CLIENT_HOST: "http://localhost" - ports: - - "80:80" - - "443:443" - depends_on: - - server - server: - image: ghcr.io/bluewave-labs/checkmate-backend:latest - restart: always - ports: - - "52345:52345" - depends_on: - - mongodb - environment: - - DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db - - CLIENT_HOST=http://localhost - - JWT_SECRET=my_secret - # volumes: - # - /var/run/docker.sock:/var/run/docker.sock:ro - mongodb: - image: ghcr.io/bluewave-labs/checkmate-mongo:latest - restart: always - volumes: - - ./mongo/data:/data/db - command: ["mongod", "--quiet", "--bind_ip_all"] - healthcheck: - test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet"] - interval: 5s - timeout: 30s - start_period: 0s - start_interval: 1s - retries: 30 diff --git a/docker/dist/mongoDB.Dockerfile b/docker/dist/mongoDB.Dockerfile deleted file mode 100755 index 969a320c2..000000000 --- a/docker/dist/mongoDB.Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM mongo -EXPOSE 27017 -CMD ["mongod"] diff --git a/docker/dist/nginx/conf.d/default.conf b/docker/dist/nginx/conf.d/default.conf deleted file mode 100755 index 30e51c71c..000000000 --- a/docker/dist/nginx/conf.d/default.conf +++ /dev/null @@ -1,35 +0,0 @@ -server { - listen 80; - listen [::]:80; - - server_name checkmate-demo.bluewavelabs.ca; - server_tokens off; - - location /.well-known/acme-challenge/ { - root /var/www/certbot; - } - - location / { - root /usr/share/nginx/html; - index index.html index.htm; - try_files $uri $uri/ /index.html; - } - - # location /api/ { - # proxy_pass http://server:5000/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:5000/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; - } -} \ No newline at end of file diff --git a/docker/dist/server.Dockerfile b/docker/dist/server.Dockerfile deleted file mode 100755 index 8a9af51d1..000000000 --- a/docker/dist/server.Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -FROM node:22-slim - -# Install ping -RUN apt-get update \ - && apt-get install -y iputils-ping \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app - -COPY ./server/package*.json ./ - -RUN npm install - -COPY ./server/ ./ - -RUN npm run build - -EXPOSE 52345 - -CMD ["node", "dist/index.js"] \ No newline at end of file diff --git a/docker/docker-compose.yaml b/docker/docker-compose.yaml new file mode 100644 index 000000000..e1a2d0e5a --- /dev/null +++ b/docker/docker-compose.yaml @@ -0,0 +1,45 @@ +services: + checkmate: + image: ghcr.io/bluewave-labs/checkmate:latest + pull_policy: always + restart: always + ports: + - "52345:52345" + environment: + - DB_CONNECTION_STRING=mongodb://mongodb:27017/uptime_db + - CLIENT_HOST=${CLIENT_HOST:-http://localhost:52345} + - JWT_SECRET=${JWT_SECRET:?set JWT_SECRET in your environment} + stop_grace_period: 60s + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "require('http').get('http://localhost:52346/livez',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))", + ] + interval: 15s + timeout: 3s + start_period: 60s + start_interval: 2s + retries: 3 + depends_on: + mongodb: + condition: service_healthy + + mongodb: + image: mongo:8.0 + restart: always + command: ["mongod", "--quiet", "--bind_ip_all"] + volumes: + - mongo-data:/data/db + healthcheck: + test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet"] + interval: 5s + timeout: 30s + start_period: 0s + start_interval: 1s + retries: 30 + +volumes: + mongo-data: diff --git a/docker/prod/build_images.bat b/docker/prod/build_images.bat deleted file mode 100644 index 79403db3f..000000000 --- a/docker/prod/build_images.bat +++ /dev/null @@ -1,33 +0,0 @@ -@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 diff --git a/docker/prod/build_images.sh b/docker/prod/build_images.sh deleted file mode 100755 index 49f8c7841..000000000 --- a/docker/prod/build_images.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/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/prod/client.Dockerfile" - ["uptime_database_mongo"]="./docker/prod/mongoDB.Dockerfile" - ["uptime_server"]="./docker/prod/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" \ No newline at end of file diff --git a/docker/prod/client.Dockerfile b/docker/prod/client.Dockerfile deleted file mode 100755 index b4c685e3e..000000000 --- a/docker/prod/client.Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -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;"] \ No newline at end of file diff --git a/docker/prod/docker-compose.yaml b/docker/prod/docker-compose.yaml deleted file mode 100755 index 02a9fd919..000000000 --- a/docker/prod/docker-compose.yaml +++ /dev/null @@ -1,53 +0,0 @@ -services: - client: - image: ghcr.io/bluewave-labs/checkmate:frontend-demo - restart: always - environment: - UPTIME_APP_API_BASE_URL: "https://demo.checkmate.so/api/v1" - UPTIME_APP_CLIENT_HOST: "https://demo.checkmate.so" - ports: - - "80:80" - - "443:443" - depends_on: - - server - volumes: - - ./nginx/conf.d:/etc/nginx/conf.d/:ro - - /etc/letsencrypt:/etc/nginx/ssl:ro - - server: - image: ghcr.io/bluewave-labs/checkmate:backend-demo - restart: always - ports: - - "52345:52345" - env_file: - - server.env - depends_on: - - mongodb - volumes: - - /var/run/docker.sock:/var/run/docker.sock:ro - mongodb: - image: ghcr.io/bluewave-labs/checkmate:mongo-demo - restart: always - command: ["mongod", "--quiet", "--bind_ip_all"] - volumes: - - ./mongo/data:/data/db - env_file: - - mongo.env - healthcheck: - test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet"] - interval: 5s - timeout: 30s - start_period: 0s - start_interval: 1s - retries: 30 - 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 diff --git a/docker/prod/mongo/init/init.js b/docker/prod/mongo/init/init.js deleted file mode 100644 index 34b90ced3..000000000 --- a/docker/prod/mongo/init/init.js +++ /dev/null @@ -1,41 +0,0 @@ -function initiateReplicaSet() { - try { - rs.initiate({ - _id: "rs0", - members: [{ _id: 0, host: "localhost:27017" }], - }); - } catch (e) { - print("Replica set already initiated or error occurred: " + e); - } -} - -function waitForPrimary() { - while (true) { - const isMaster = db.isMaster(); - if (isMaster.ismaster) { - print("This node is now PRIMARY."); - break; - } - print("Waiting to become PRIMARY..."); - sleep(2000); - } -} - -function createUser() { - db = db.getSiblingDB("uptime_db"); - - var username = process.env.USERNAME_ENV_VAR; - var password = process.env.PASSWORD_ENV_VAR; - - db.createUser({ - user: username, - pwd: password, - roles: [{ role: "readWrite", db: "uptime_db" }], - }); - - print("User '" + username + "' created successfully"); -} - -initiateReplicaSet(); -waitForPrimary(); -createUser(); diff --git a/docker/prod/mongoDB.Dockerfile b/docker/prod/mongoDB.Dockerfile deleted file mode 100755 index 969a320c2..000000000 --- a/docker/prod/mongoDB.Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM mongo -EXPOSE 27017 -CMD ["mongod"] diff --git a/docker/prod/nginx/conf.d/cerbot b/docker/prod/nginx/conf.d/cerbot deleted file mode 100755 index f26cfeb23..000000000 --- a/docker/prod/nginx/conf.d/cerbot +++ /dev/null @@ -1,15 +0,0 @@ -server { - listen 80; - listen [::]:80; - - server_name checkmate-demo.bluewavelabs.ca www.checkmate-demo.bluewavelabs.ca; - server_tokens off; - - location /.well-known/acme-challenge/ { - root /var/www/certbot; - } - - location / { - return 301 https://[domain-name]$request_uri; - } -} diff --git a/docker/prod/nginx/conf.d/default.conf b/docker/prod/nginx/conf.d/default.conf deleted file mode 100755 index 0e0e55a2b..000000000 --- a/docker/prod/nginx/conf.d/default.conf +++ /dev/null @@ -1,69 +0,0 @@ -server { - listen 80; - listen [::]:80; - - server_name checkmate-demo.bluewavelabs.ca; - server_tokens off; - - location /.well-known/acme-challenge/ { - root /var/www/certbot; - } - - location / { - root /usr/share/nginx/html; - index index.html index.htm; - try_files $uri $uri/ /index.html; - } - - location /api/ { - proxy_pass http://server: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: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; - } -} - -server { - listen 443 default_server ssl http2; - listen [::]:443 ssl http2; - - server_name checkmate-demo.bluewavelabs.ca; - - 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; - index index.html index.htm; - try_files $uri $uri/ /index.html; - } - - location /api/ { - proxy_pass http://server: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: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; - } -} diff --git a/docker/prod/server.Dockerfile b/docker/prod/server.Dockerfile deleted file mode 100755 index b093638f4..000000000 --- a/docker/prod/server.Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM node:22-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 - -RUN cp -r src/templates dist/templates - -EXPOSE 52345 - -CMD ["node", "dist/index.js"] \ No newline at end of file diff --git a/docker/staging/build_images.bat b/docker/staging/build_images.bat deleted file mode 100644 index c8be0378a..000000000 --- a/docker/staging/build_images.bat +++ /dev/null @@ -1,33 +0,0 @@ -@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 diff --git a/docker/staging/build_images.sh b/docker/staging/build_images.sh deleted file mode 100755 index a11aebf7c..000000000 --- a/docker/staging/build_images.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/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/staging/client.Dockerfile" - ["uptime_database_mongo"]="./docker/staging/mongoDB.Dockerfile" - ["uptime_server"]="./docker/staging/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" \ No newline at end of file diff --git a/docker/staging/cerbot-compose.yaml b/docker/staging/cerbot-compose.yaml deleted file mode 100755 index 9220d07a1..000000000 --- a/docker/staging/cerbot-compose.yaml +++ /dev/null @@ -1,19 +0,0 @@ -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 diff --git a/docker/staging/client.Dockerfile b/docker/staging/client.Dockerfile deleted file mode 100644 index df3ac7439..000000000 --- a/docker/staging/client.Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -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;"] \ No newline at end of file diff --git a/docker/staging/docker-compose.yaml b/docker/staging/docker-compose.yaml deleted file mode 100755 index f4ed5ce6f..000000000 --- a/docker/staging/docker-compose.yaml +++ /dev/null @@ -1,62 +0,0 @@ -services: - client: - image: ghcr.io/bluewave-labs/checkmate:frontend-staging - restart: always - environment: - UPTIME_APP_API_BASE_URL: "https://checkmate-test.bluewavelabs.ca/api/v1" - UPTIME_APP_CLIENT_HOST: "https://checkmate-test.bluewavelabs.ca" - ports: - - "80:80" - - "443:443" - depends_on: - - 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;'" - - server: - image: ghcr.io/bluewave-labs/checkmate:backend-staging - restart: always - ports: - - "52345:52345" - env_file: - - server.env - depends_on: - - mongodb - mongodb: - image: ghcr.io/bluewave-labs/checkmate:mongo-staging - restart: always - command: ["mongod", "--quiet", "--bind_ip_all"] - ports: - - "27017:27017" - volumes: - - ./mongo/data:/data/db - env_file: - - mongo.env - healthcheck: - test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')", "--quiet"] - interval: 5s - timeout: 30s - start_period: 0s - start_interval: 1s - retries: 30 - 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 diff --git a/docker/staging/mongo/init/01_init_replica_set.js b/docker/staging/mongo/init/01_init_replica_set.js deleted file mode 100644 index 878ce68a6..000000000 --- a/docker/staging/mongo/init/01_init_replica_set.js +++ /dev/null @@ -1,15 +0,0 @@ -try { - const status = rs.status(); - printjson(status); -} catch (e) { - if (e.codeName === "NotYetInitialized") { - print("Replica set not initialized. Initiating..."); - rs.initiate({ - _id: "rs0", - members: [{ _id: 0, host: "mongodb:27017" }], - }); - } else { - print("Unexpected error during rs.status():"); - printjson(e); - } -} diff --git a/docker/staging/mongo/init/02_create_users.js b/docker/staging/mongo/init/02_create_users.js deleted file mode 100755 index adb2bf078..000000000 --- a/docker/staging/mongo/init/02_create_users.js +++ /dev/null @@ -1,16 +0,0 @@ -var username = process.env.USERNAME_ENV_VAR; -var password = process.env.PASSWORD_ENV_VAR; - -db = db.getSiblingDB("uptime_db"); - -db.createUser({ - user: username, - pwd: password, - roles: [ - { - role: "readWrite", - db: "uptime_db", - }, - ], -}); -print("User uptime_user created successfully"); diff --git a/docker/staging/mongoDB.Dockerfile b/docker/staging/mongoDB.Dockerfile deleted file mode 100755 index 969a320c2..000000000 --- a/docker/staging/mongoDB.Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM mongo -EXPOSE 27017 -CMD ["mongod"] diff --git a/docker/staging/nginx/conf.d/certbot b/docker/staging/nginx/conf.d/certbot deleted file mode 100755 index eba24638c..000000000 --- a/docker/staging/nginx/conf.d/certbot +++ /dev/null @@ -1,15 +0,0 @@ -server { - listen 80; - listen [::]:80; - - server_name checkmate-test.bluewavelabs.ca www.checkmate-test.bluewavelabs.ca; - server_tokens off; - - location /.well-known/acme-challenge/ { - root /var/www/certbot; - } - - location / { - return 301 https://[domain-name]$request_uri; - } -} diff --git a/docker/staging/nginx/conf.d/default.conf b/docker/staging/nginx/conf.d/default.conf deleted file mode 100755 index c755ddef1..000000000 --- a/docker/staging/nginx/conf.d/default.conf +++ /dev/null @@ -1,77 +0,0 @@ -server { - listen 80; - listen [::]:80; - - server_name checkmate-test.bluewavelabs.ca; - server_tokens off; - - location /.well-known/acme-challenge/ { - root /var/www/certbot; - } - - location / { - root /usr/share/nginx/html; - index index.html index.htm; - try_files $uri $uri/ /index.html; - } - - location /api/ { - proxy_pass http://server: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; - proxy_set_header Connection ''; - chunked_transfer_encoding off; - proxy_buffering off; - proxy_cache off; - } - - location /api-docs/ { - proxy_pass http://server: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; - } -} - -server { - listen 443 default_server ssl http2; - listen [::]:443 ssl http2; - - server_name checkmate-test.bluewavelabs.ca; - - ssl_certificate /etc/nginx/ssl/live/checkmate-test.bluewavelabs.ca/fullchain.pem; - ssl_certificate_key /etc/nginx/ssl/live/checkmate-test.bluewavelabs.ca/privkey.pem; - - location / { - root /usr/share/nginx/html; - index index.html index.htm; - try_files $uri $uri/ /index.html; - } - - location /api/ { - proxy_pass http://server: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; - proxy_set_header Connection ''; - chunked_transfer_encoding off; - proxy_buffering off; - proxy_cache off; - } - - location /api-docs/ { - proxy_pass http://server: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; - } -} diff --git a/docker/staging/server.Dockerfile b/docker/staging/server.Dockerfile deleted file mode 100755 index b093638f4..000000000 --- a/docker/staging/server.Dockerfile +++ /dev/null @@ -1,24 +0,0 @@ -FROM node:22-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 - -RUN cp -r src/templates dist/templates - -EXPOSE 52345 - -CMD ["node", "dist/index.js"] \ No newline at end of file diff --git a/docker/unified/build_images.bat b/docker/unified/build_images.bat deleted file mode 100644 index a3d54e60d..000000000 --- a/docker/unified/build_images.bat +++ /dev/null @@ -1,33 +0,0 @@ -@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 diff --git a/docker/unified/build_images.sh b/docker/unified/build_images.sh deleted file mode 100755 index 21b866bbe..000000000 --- a/docker/unified/build_images.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/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" diff --git a/docker/unified/certbot-compose.yaml b/docker/unified/certbot-compose.yaml deleted file mode 100755 index 9220d07a1..000000000 --- a/docker/unified/certbot-compose.yaml +++ /dev/null @@ -1,19 +0,0 @@ -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 diff --git a/docker/unified/client.Dockerfile b/docker/unified/client.Dockerfile deleted file mode 100755 index b4c685e3e..000000000 --- a/docker/unified/client.Dockerfile +++ /dev/null @@ -1,20 +0,0 @@ -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;"] \ No newline at end of file diff --git a/docker/unified/docker-compose.yaml b/docker/unified/docker-compose.yaml deleted file mode 100755 index 199d0da21..000000000 --- a/docker/unified/docker-compose.yaml +++ /dev/null @@ -1,114 +0,0 @@ -# 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 diff --git a/docker/unified/mongoDB.Dockerfile b/docker/unified/mongoDB.Dockerfile deleted file mode 100755 index 969a320c2..000000000 --- a/docker/unified/mongoDB.Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -FROM mongo -EXPOSE 27017 -CMD ["mongod"] diff --git a/docker/unified/nginx/conf.d/prod.conf b/docker/unified/nginx/conf.d/prod.conf deleted file mode 100755 index 63a42a401..000000000 --- a/docker/unified/nginx/conf.d/prod.conf +++ /dev/null @@ -1,31 +0,0 @@ -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; - } -} \ No newline at end of file diff --git a/docker/unified/nginx/conf.d/staging.conf b/docker/unified/nginx/conf.d/staging.conf deleted file mode 100755 index 6b640a06b..000000000 --- a/docker/unified/nginx/conf.d/staging.conf +++ /dev/null @@ -1,31 +0,0 @@ -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; - } -} \ No newline at end of file diff --git a/docker/unified/server.Dockerfile b/docker/unified/server.Dockerfile deleted file mode 100755 index 86a8c1f48..000000000 --- a/docker/unified/server.Dockerfile +++ /dev/null @@ -1,22 +0,0 @@ -FROM node:22-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"] \ No newline at end of file diff --git a/docs/README.md b/docs/README.md index b8904d1f9..09698b115 100644 --- a/docs/README.md +++ b/docs/README.md @@ -19,8 +19,6 @@ If you need to monitor internal HTTPS endpoints with certificates from private C ### Example Configurations The `docker/dev/` directory contains example configurations: - `docker-compose.custom-ca-example.yaml` - Example Docker Compose with custom CA -- `server-custom-ca.Dockerfile` - Example Dockerfile for OS-level CA trust -- `export-smallstep-ca.sh` - Helper script for Smallstep CA export ## Contributing to Documentation diff --git a/docs/custom-ca-trust.md b/docs/custom-ca-trust.md index b3a19831f..6e94894c0 100644 --- a/docs/custom-ca-trust.md +++ b/docs/custom-ca-trust.md @@ -18,8 +18,8 @@ Add a volume mount for your CA certificate and set the environment variable: ```yaml services: - server: - image: uptime_server:latest + checkmate: + image: ghcr.io/bluewave-labs/checkmate:latest restart: always ports: - "52345:52345" @@ -30,7 +30,6 @@ services: volumes: - ./certs:/certs:ro depends_on: - - redis - mongodb ``` @@ -52,67 +51,53 @@ For more comprehensive trust configuration, you can create a derived Dockerfile ### Custom Dockerfile -Create `docker/dev/server-custom-ca.Dockerfile`: +Create `checkmate-custom-ca.Dockerfile`: ```dockerfile -FROM node:20-alpine +FROM ghcr.io/bluewave-labs/checkmate:latest -# Install ca-certificates for Alpine -RUN apk add --no-cache ca-certificates +USER root -WORKDIR /app +RUN apt-get update \ + && apt-get install -y ca-certificates \ + && rm -rf /var/lib/apt/lists/* -COPY ./server/package*.json ./ - -RUN npm install - -COPY ./server/ ./ - -# Copy your custom CA certificate +# Copy your custom CA certificate (must have a .crt extension) COPY ./certs/custom-ca.crt /usr/local/share/ca-certificates/ # Update CA certificates RUN update-ca-certificates -EXPOSE 52345 +USER node -CMD ["node", "src/index.js"] +# Node uses its bundled CA store by default; make it read the system store +ENV NODE_OPTIONS="--use-system-ca" ``` ### Docker Compose Override -Create `docker/dev/docker-compose.custom-ca.yaml`: +Create `docker-compose.custom-ca.yaml`: ```yaml services: - server: + checkmate: build: context: . - dockerfile: server-custom-ca.Dockerfile + dockerfile: checkmate-custom-ca.Dockerfile restart: always ports: - "52345:52345" env_file: - server.env depends_on: - - redis - mongodb ``` -Run with: `docker-compose -f docker-compose.yaml -f docker-compose.custom-ca.yaml up` +Run with: `docker compose -f docker-compose.yaml -f docker-compose.custom-ca.yaml up` -## Alpine Linux Considerations +## Base Image Considerations -Since Checkmate uses Alpine Linux as the base image, you need to install the `ca-certificates` package: - -```dockerfile -# Install ca-certificates for Alpine -RUN apk add --no-cache ca-certificates - -# Copy and update CA certificates -COPY ./certs/custom-ca.crt /usr/local/share/ca-certificates/ -RUN update-ca-certificates -``` +The Checkmate image is Debian-based (`node:22-slim`), which does not ship the `ca-certificates` package โ€” install it before running `update-ca-certificates` (see the Dockerfile above). Certificates dropped into `/usr/local/share/ca-certificates/` must have a `.crt` extension, and Node only consults the system store when started with `--use-system-ca`. For most setups the `NODE_EXTRA_CA_CERTS` compose approach is simpler. ## Smallstep CA Configuration diff --git a/docs/hosting/checkmate-so/README.md b/docs/hosting/checkmate-so/README.md new file mode 100644 index 000000000..0c06f6948 --- /dev/null +++ b/docs/hosting/checkmate-so/README.md @@ -0,0 +1,56 @@ +# checkmate.so hosting (demo + staging) + +Config for BlueWave's hosted instances at `demo.checkmate.so` and +`staging.checkmate.so`: one host, one nginx gateway terminating TLS, two +independent Checkmate stacks running the mono image +(`ghcr.io/bluewave-labs/checkmate`), plus a shared +[Capture](https://github.com/bluewave-labs/capture) agent. It replaces the old +`docker/unified/` setup (nginx client images + separate backend images + a +republished Mongo image) and doubles as a worked example of running the mono +image behind a TLS reverse proxy. + +## How deploys work + +`.github/workflows/build-image.yml` builds and pushes the image, then SSHes in +and runs `docker compose pull && docker compose up -d ` in +this directory: + +- merge to `develop` โ†’ image tagged `:develop` โ†’ `checkmate-staging` recreated +- merge to `master` โ†’ image tagged `:master` โ†’ `checkmate-demo` (prod) recreated + +The host keeps a git checkout of this repo (on `develop`); the deploy does +`git pull` first so compose changes land with the code. Note the deploy only +recreates the checkmate service โ€” changes to `gateway/nginx.conf` or the mongo +services need a manual `docker compose up -d` / gateway restart on the host. + +## Host-local files (not committed) + +Kept next to this compose file on the host: + +- `server-prod.env` / `server-staging.env` โ€” Checkmate server env + (`DB_CONNECTION_STRING`, `JWT_SECRET`, `CLIENT_HOST=https://demo.checkmate.so` + etc.). The old `UPTIME_APP_*` client vars are no-ops now and can be removed; + if a client override is ever needed, use the `CLIENT_CONFIG_*` vars (see the + Configuration section of the root README). +- `mongo-prod.env` / `mongo-staging.env` โ€” Mongo credentials. +- `mongo/data-prod` / `mongo/data-staging` โ€” Mongo data directories. + +TLS certs live in `/etc/letsencrypt` (see `certbot-compose.yaml`; certbot +answers ACME challenges on port 8080 behind the gateway). + +## One-time migration from docker/unified + +1. `cd ~/checkmate && git pull` (must be on a branch containing this directory). +2. Stop the old stack: `cd docker/unified && docker compose down`. +3. Move host-local files here: + `mv docker/unified/{server-prod.env,server-staging.env,mongo-prod.env,mongo-staging.env} docs/hosting/checkmate-so/` + `mv docker/unified/mongo docs/hosting/checkmate-so/mongo` +4. **Check the Mongo data version before first start.** The old + `checkmate:mongo-*` images were built `FROM mongo` (floating latest), so the + data files follow whatever version was current at their last image build. Run + `db.adminCommand({ getParameter: 1, featureCompatibilityVersion: 1 })` + against the old container first and make the compose's `mongo:` tag match + (checked 2026-07-09: both stacks report FCV 8.2, hence the `mongo:8.2` pin + here). A lower pin than the data's FCV will refuse to start; a higher one + should step up one release at a time. +5. `cd docs/hosting/checkmate-so && docker compose up -d`. diff --git a/docker/prod/certbot-compose.yaml b/docs/hosting/checkmate-so/certbot-compose.yaml similarity index 100% rename from docker/prod/certbot-compose.yaml rename to docs/hosting/checkmate-so/certbot-compose.yaml diff --git a/docs/hosting/checkmate-so/docker-compose.yaml b/docs/hosting/checkmate-so/docker-compose.yaml new file mode 100644 index 000000000..96849fecb --- /dev/null +++ b/docs/hosting/checkmate-so/docker-compose.yaml @@ -0,0 +1,122 @@ +# BlueWave's hosted demo + staging instances: one nginx gateway terminating TLS +# in front of two independent Checkmate stacks, both running the mono image +# (ghcr.io/bluewave-labs/checkmate). Doubles as a worked example of putting a +# TLS proxy in front of the image. +# +# Deployed by .github/workflows/build-image.yml (deploy-staging / deploy-demo +# jobs). Env files (server-prod.env, server-staging.env, mongo-prod.env, +# mongo-staging.env) live on the host next to this file and are not committed. + +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 + + # Prod: demo.checkmate.so ----- + checkmate-demo: + image: ghcr.io/bluewave-labs/checkmate:master + container_name: checkmate-demo + restart: always + env_file: + - server-prod.env + stop_grace_period: 60s + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "require('http').get('http://localhost:52346/livez',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))", + ] + interval: 15s + timeout: 3s + start_period: 60s + start_interval: 2s + retries: 3 + depends_on: + mongodb-prod: + condition: service_healthy + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + + mongodb-prod: + image: mongo:8.2 + 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: staging.checkmate.so ----- + checkmate-staging: + image: ghcr.io/bluewave-labs/checkmate:develop + container_name: checkmate-staging + restart: always + env_file: + - server-staging.env + stop_grace_period: 60s + healthcheck: + test: + [ + "CMD", + "node", + "-e", + "require('http').get('http://localhost:52346/livez',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))", + ] + interval: 15s + timeout: 3s + start_period: 60s + start_interval: 2s + retries: 3 + depends_on: + mongodb-staging: + condition: service_healthy + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + + mongodb-staging: + image: mongo:8.2 + 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 diff --git a/docker/unified/gateway/nginx.conf b/docs/hosting/checkmate-so/gateway/nginx.conf similarity index 59% rename from docker/unified/gateway/nginx.conf rename to docs/hosting/checkmate-so/gateway/nginx.conf index 47ad4695e..3523872c5 100644 --- a/docker/unified/gateway/nginx.conf +++ b/docs/hosting/checkmate-so/gateway/nginx.conf @@ -19,7 +19,7 @@ server { } -# --- PRODUCTION: demo.checkmate.so --- +# --- DEMO: demo.checkmate.so --- server { listen 443 ssl; server_name demo.checkmate.so; @@ -31,25 +31,24 @@ server { ssl_ciphers HIGH:!aNULL:!MD5; location / { - # Passes traffic to the prod client on port 80 - proxy_pass http://client-prod:80; + # The mono image serves both the SPA and the API + proxy_pass http://checkmate-demo:52345; } } # --- STAGING: staging.checkmate.so --- +server { + listen 443 ssl; + server_name 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_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; - 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; - } - } + location / { + # The mono image serves both the SPA and the API + proxy_pass http://checkmate-staging:52345; + } +} diff --git a/docs/translations/README.ar.md b/docs/translations/README.ar.md index 81a0fc8d8..8ff16e140 100644 --- a/docs/translations/README.ar.md +++ b/docs/translations/README.ar.md @@ -11,6 +11,7 @@ ๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol | ๐Ÿ‡ซ๐Ÿ‡ฎ Suomi | ๐Ÿ‡ซ๐Ÿ‡ท Franรงais | + ๐Ÿ‡ฎ๐Ÿ‡น Italiano | ๐Ÿ‡ฏ๐Ÿ‡ต ๆ—ฅๆœฌ่ชž | ๐Ÿ‡ง๐Ÿ‡ท Portuguรชs (Brasil) | ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน | @@ -86,7 +87,7 @@ ุงุทู‘ู„ุน ุนู„ู‰ ุชุนู„ูŠู…ุงุช ุงู„ุชุซุจูŠุช ููŠ [ุจูˆุงุจุฉ ูˆุซุงุฆู‚ Checkmate](https://checkmate.so/docs). -ุจุฏู„ุงู‹ ู…ู† ุฐู„ูƒุŒ ูŠู…ูƒู†ูƒ ุงุณุชุฎุฏุงู… [Coolify](https://coolify.io/)ุŒ ุฃูˆ [Elestio](https://elest.io/open-source/checkmate)ุŒ ุฃูˆ [K8s](./charts/helm/checkmate/INSTALLATION.md)ุŒ ุฃูˆ [Sive Host](https://sive.host) (ุฌู†ูˆุจ ุฃูุฑูŠู‚ูŠุง)ุŒ ุฃูˆ [Cloudzy](https://cloudzy.com/marketplace/checkmate)ุŒ ุฃูˆ [Pikapods](https://www.pikapods.com/) ู„ุชุดุบูŠู„ ู†ุณุฎุฉ ู…ู† Checkmate ุจุณุฑุนุฉ. ุฅู† ุฃุฑุฏุช ู…ุฑุงู‚ุจุฉ ุงู„ุจู†ูŠุฉ ุงู„ุชุญุชูŠุฉ ู„ุฎูˆุงุฏู…ูƒุŒ ุณุชุญุชุงุฌ ุฅู„ู‰ [ูˆูƒูŠู„ Capture](https://github.com/bluewave-labs/capture). ูŠุญุชูˆูŠ ู…ุณุชูˆุฏุน Capture ุฃูŠุถู‹ุง ุนู„ู‰ ุชุนู„ูŠู…ุงุช ุงู„ุชุซุจูŠุช. +ุจุฏู„ุงู‹ ู…ู† ุฐู„ูƒุŒ ูŠู…ูƒู†ูƒ ุงุณุชุฎุฏุงู… [Coolify](https://coolify.io/)ุŒ ุฃูˆ [Elestio](https://elest.io/open-source/checkmate)ุŒ ุฃูˆ [K8s](../../charts/helm/checkmate/INSTALLATION.md)ุŒ ุฃูˆ [Sive Host](https://sive.host) (ุฌู†ูˆุจ ุฃูุฑูŠู‚ูŠุง)ุŒ ุฃูˆ [Cloudzy](https://cloudzy.com/marketplace/checkmate)ุŒ ุฃูˆ [Pikapods](https://www.pikapods.com/) ู„ุชุดุบูŠู„ ู†ุณุฎุฉ ู…ู† Checkmate ุจุณุฑุนุฉ. ุฅู† ุฃุฑุฏุช ู…ุฑุงู‚ุจุฉ ุงู„ุจู†ูŠุฉ ุงู„ุชุญุชูŠุฉ ู„ุฎูˆุงุฏู…ูƒุŒ ุณุชุญุชุงุฌ ุฅู„ู‰ [ูˆูƒูŠู„ Capture](https://github.com/bluewave-labs/capture). ูŠุญุชูˆูŠ ู…ุณุชูˆุฏุน Capture ุฃูŠุถู‹ุง ุนู„ู‰ ุชุนู„ูŠู…ุงุช ุงู„ุชุซุจูŠุช. ### ุงุณุชุฎุฏุงู… ุดู‡ุงุฏุฉ CA ู…ุฎุตุตุฉ diff --git a/docs/translations/README.ca.md b/docs/translations/README.ca.md index 94e46d49d..29e7e4107 100644 --- a/docs/translations/README.ca.md +++ b/docs/translations/README.ca.md @@ -9,6 +9,7 @@ ๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol | ๐Ÿ‡ซ๐Ÿ‡ฎ Suomi | ๐Ÿ‡ซ๐Ÿ‡ท Franรงais | + ๐Ÿ‡ฎ๐Ÿ‡น Italiano | ๐Ÿ‡ฏ๐Ÿ‡ต ๆ—ฅๆœฌ่ชž | ๐Ÿ‡ง๐Ÿ‡ท Portuguรชs (Brasil) | ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน | @@ -84,7 +85,7 @@ Pots trobar les instruccions d'รบs [aquรญ](https://checkmate.so/docs). Consulta les instruccions d'instalยทlaciรณ al [portal de documentaciรณ de Checkmate](https://checkmate.so/docs). -Alternativament, tambรฉ pots utilitzar [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Sud-ร frica), [Cloudzy](https://cloudzy.com/marketplace/checkmate) o [Pikapods](https://www.pikapods.com/) per posar en marxa rร pidament una instร ncia de Checkmate. Si vols monitoritzar la infraestructura de servidors, necessitarร s l'[agent Capture](https://github.com/bluewave-labs/capture). El repositori de Capture tambรฉ contรฉ les instruccions d'instalยทlaciรณ. +Alternativament, tambรฉ pots utilitzar [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Sud-ร frica), [Cloudzy](https://cloudzy.com/marketplace/checkmate) o [Pikapods](https://www.pikapods.com/) per posar en marxa rร pidament una instร ncia de Checkmate. Si vols monitoritzar la infraestructura de servidors, necessitarร s l'[agent Capture](https://github.com/bluewave-labs/capture). El repositori de Capture tambรฉ contรฉ les instruccions d'instalยทlaciรณ. ### Utilitzar una CA personalitzada diff --git a/docs/translations/README.cs.md b/docs/translations/README.cs.md index 3faa00334..d662f0b70 100644 --- a/docs/translations/README.cs.md +++ b/docs/translations/README.cs.md @@ -9,6 +9,7 @@ ๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol | ๐Ÿ‡ซ๐Ÿ‡ฎ Suomi | ๐Ÿ‡ซ๐Ÿ‡ท Franรงais | + ๐Ÿ‡ฎ๐Ÿ‡น Italiano | ๐Ÿ‡ฏ๐Ÿ‡ต ๆ—ฅๆœฌ่ชž | ๐Ÿ‡ง๐Ÿ‡ท Portuguรชs (Brasil) | ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน | @@ -84,7 +85,7 @@ Pokyny k pouลพitรญ najdete [zde](https://checkmate.so/docs). Pokyny k instalaci najdete v [dokumentaฤnรญm portรกlu Checkmate](https://checkmate.so/docs). -Pล™รญpadnฤ› mลฏลพete pouลพรญt [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Jiลพnรญ Afrika), [Cloudzy](https://cloudzy.com/marketplace/checkmate) nebo [Pikapods](https://www.pikapods.com/) pro rychlรฉ spuลกtฤ›nรญ instance Checkmate. Pokud chcete monitorovat infrastrukturu serverลฏ, budete potล™ebovat [agenta Capture](https://github.com/bluewave-labs/capture). Repozitรกล™ Capture obsahuje takรฉ pokyny k instalaci. +Pล™รญpadnฤ› mลฏลพete pouลพรญt [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Jiลพnรญ Afrika), [Cloudzy](https://cloudzy.com/marketplace/checkmate) nebo [Pikapods](https://www.pikapods.com/) pro rychlรฉ spuลกtฤ›nรญ instance Checkmate. Pokud chcete monitorovat infrastrukturu serverลฏ, budete potล™ebovat [agenta Capture](https://github.com/bluewave-labs/capture). Repozitรกล™ Capture obsahuje takรฉ pokyny k instalaci. ### Pouลพitรญ vlastnรญ CA diff --git a/docs/translations/README.de.md b/docs/translations/README.de.md index 59f897368..faee4e44c 100644 --- a/docs/translations/README.de.md +++ b/docs/translations/README.de.md @@ -9,6 +9,7 @@ ๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol | ๐Ÿ‡ซ๐Ÿ‡ฎ Suomi | ๐Ÿ‡ซ๐Ÿ‡ท Franรงais | + ๐Ÿ‡ฎ๐Ÿ‡น Italiano | ๐Ÿ‡ฏ๐Ÿ‡ต ๆ—ฅๆœฌ่ชž | ๐Ÿ‡ง๐Ÿ‡ท Portuguรชs (Brasil) | ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน | @@ -81,7 +82,7 @@ Die Nutzungsanleitung findest du [hier](https://checkmate.so/docs). Die Installationsanleitung findest du im [Dokumentationsportal von Checkmate](https://checkmate.so/docs). -Alternativ kannst du [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Sรผdafrika), [Cloudzy](https://cloudzy.com/marketplace/checkmate) oder [Pikapods](https://www.pikapods.com/) nutzen, um schnell eine Checkmate-Instanz hochzuziehen. Fรผr das Monitoring deiner Server-Infrastruktur brauchst du den [Capture-Agenten](https://github.com/bluewave-labs/capture); dessen Repository enthรคlt ebenfalls eine Installationsanleitung. +Alternativ kannst du [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Sรผdafrika), [Cloudzy](https://cloudzy.com/marketplace/checkmate) oder [Pikapods](https://www.pikapods.com/) nutzen, um schnell eine Checkmate-Instanz hochzuziehen. Fรผr das Monitoring deiner Server-Infrastruktur brauchst du den [Capture-Agenten](https://github.com/bluewave-labs/capture); dessen Repository enthรคlt ebenfalls eine Installationsanleitung. ### Eigene CA verwenden diff --git a/docs/translations/README.es.md b/docs/translations/README.es.md index e72d67cc5..5dc04e269 100644 --- a/docs/translations/README.es.md +++ b/docs/translations/README.es.md @@ -9,6 +9,7 @@ ๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol | ๐Ÿ‡ซ๐Ÿ‡ฎ Suomi | ๐Ÿ‡ซ๐Ÿ‡ท Franรงais | + ๐Ÿ‡ฎ๐Ÿ‡น Italiano | ๐Ÿ‡ฏ๐Ÿ‡ต ๆ—ฅๆœฌ่ชž | ๐Ÿ‡ง๐Ÿ‡ท Portuguรชs (Brasil) | ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน | @@ -81,7 +82,7 @@ Las instrucciones de uso estรกn disponibles [aquรญ](https://checkmate.so/docs). Consulta las instrucciones de instalaciรณn en el [portal de documentaciรณn de Checkmate](https://checkmate.so/docs). -Tambiรฉn puedes utilizar [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Sudรกfrica), [Cloudzy](https://cloudzy.com/marketplace/checkmate) o [Pikapods](https://www.pikapods.com/) para levantar rรกpidamente una instancia de Checkmate. Si quieres monitorizar la infraestructura de tus servidores, necesitarรกs el [agente Capture](https://github.com/bluewave-labs/capture). El repositorio de Capture tambiรฉn incluye las instrucciones de instalaciรณn. +Tambiรฉn puedes utilizar [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Sudรกfrica), [Cloudzy](https://cloudzy.com/marketplace/checkmate) o [Pikapods](https://www.pikapods.com/) para levantar rรกpidamente una instancia de Checkmate. Si quieres monitorizar la infraestructura de tus servidores, necesitarรกs el [agente Capture](https://github.com/bluewave-labs/capture). El repositorio de Capture tambiรฉn incluye las instrucciones de instalaciรณn. ### Uso de una CA personalizada diff --git a/docs/translations/README.fi.md b/docs/translations/README.fi.md index 91b91058b..877130120 100644 --- a/docs/translations/README.fi.md +++ b/docs/translations/README.fi.md @@ -9,6 +9,7 @@ ๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol | ๐Ÿ‡ซ๐Ÿ‡ฎ Suomi | ๐Ÿ‡ซ๐Ÿ‡ท Franรงais | + ๐Ÿ‡ฎ๐Ÿ‡น Italiano | ๐Ÿ‡ฏ๐Ÿ‡ต ๆ—ฅๆœฌ่ชž | ๐Ÿ‡ง๐Ÿ‡ท Portuguรชs (Brasil) | ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน | @@ -84,7 +85,7 @@ Kรคyttรถohjeet lรถytyvรคt [tรครคltรค](https://checkmate.so/docs). Asennusohjeet lรถytyvรคt [Checkmaten dokumentaatioportaalista](https://checkmate.so/docs). -Vaihtoehtoisesti voit kรคyttรครค [Coolifya](https://coolify.io/), [Elestiota](https://elest.io/open-source/checkmate), [K8s:รครค](./charts/helm/checkmate/INSTALLATION.md), [Sive Hostia](https://sive.host) (Etelรค-Afrikka), [Cloudzya](https://cloudzy.com/marketplace/checkmate) tai [Pikapodsia](https://www.pikapods.com/) Checkmate-instanssin nopeaan kรคynnistรคmiseen. Jos haluat valvoa palvelininfrastruktuuriasi, tarvitset [Capture-agentin](https://github.com/bluewave-labs/capture). Capturen repositorio sisรคltรครค myรถs sen asennusohjeet. +Vaihtoehtoisesti voit kรคyttรครค [Coolifya](https://coolify.io/), [Elestiota](https://elest.io/open-source/checkmate), [K8s:รครค](../../charts/helm/checkmate/INSTALLATION.md), [Sive Hostia](https://sive.host) (Etelรค-Afrikka), [Cloudzya](https://cloudzy.com/marketplace/checkmate) tai [Pikapodsia](https://www.pikapods.com/) Checkmate-instanssin nopeaan kรคynnistรคmiseen. Jos haluat valvoa palvelininfrastruktuuriasi, tarvitset [Capture-agentin](https://github.com/bluewave-labs/capture). Capturen repositorio sisรคltรครค myรถs sen asennusohjeet. ### Mukautetun CA:n kรคyttรถ diff --git a/docs/translations/README.fr.md b/docs/translations/README.fr.md index fc4341a28..e78a6cf3c 100644 --- a/docs/translations/README.fr.md +++ b/docs/translations/README.fr.md @@ -9,6 +9,7 @@ ๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol | ๐Ÿ‡ซ๐Ÿ‡ฎ Suomi | ๐Ÿ‡ซ๐Ÿ‡ท Franรงais | + ๐Ÿ‡ฎ๐Ÿ‡น Italiano | ๐Ÿ‡ฏ๐Ÿ‡ต ๆ—ฅๆœฌ่ชž | ๐Ÿ‡ง๐Ÿ‡ท Portuguรชs (Brasil) | ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน | @@ -81,7 +82,7 @@ Les instructions d'utilisation sont disponibles [ici](https://checkmate.so/docs) Consultez les instructions d'installation dans le [portail de documentation Checkmate](https://checkmate.so/docs). -Vous pouvez รฉgalement utiliser [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Afrique du Sud), [Cloudzy](https://cloudzy.com/marketplace/checkmate) ou [Pikapods](https://www.pikapods.com/) pour dรฉmarrer rapidement une instance Checkmate. Pour surveiller votre infrastructure serveur, vous aurez besoin de l'[agent Capture](https://github.com/bluewave-labs/capture). Le dรฉpรดt Capture contient รฉgalement les instructions d'installation. +Vous pouvez รฉgalement utiliser [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Afrique du Sud), [Cloudzy](https://cloudzy.com/marketplace/checkmate) ou [Pikapods](https://www.pikapods.com/) pour dรฉmarrer rapidement une instance Checkmate. Pour surveiller votre infrastructure serveur, vous aurez besoin de l'[agent Capture](https://github.com/bluewave-labs/capture). Le dรฉpรดt Capture contient รฉgalement les instructions d'installation. ### Utiliser une autoritรฉ de certification personnalisรฉe diff --git a/docs/translations/README.it.md b/docs/translations/README.it.md new file mode 100644 index 000000000..1f03bd682 --- /dev/null +++ b/docs/translations/README.it.md @@ -0,0 +1,199 @@ +

bluewave-labs%2Fcheckmate | Trendshift

+ +

+ ๐Ÿ‡บ๐Ÿ‡ธ English | + ๐Ÿ‡ธ๐Ÿ‡ฆ ุงู„ุนุฑุจูŠุฉ | + ๐Ÿ‡ช๐Ÿ‡ธ Catalร  | + ๐Ÿ‡จ๐Ÿ‡ฟ ฤŒeลกtina | + ๐Ÿ‡ฉ๐Ÿ‡ช Deutsch | + ๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol | + ๐Ÿ‡ซ๐Ÿ‡ฎ Suomi | + ๐Ÿ‡ซ๐Ÿ‡ท Franรงais | + ๐Ÿ‡ฎ๐Ÿ‡น Italiano | + ๐Ÿ‡ฏ๐Ÿ‡ต ๆ—ฅๆœฌ่ชž | + ๐Ÿ‡ง๐Ÿ‡ท Portuguรชs (Brasil) | + ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน | + ๐Ÿ‡น๐Ÿ‡ญ เน„เธ—เธข | + ๐Ÿ‡น๐Ÿ‡ท Tรผrkรงe | + ๐Ÿ‡บ๐Ÿ‡ฆ ะฃะบั€ะฐั—ะฝััŒะบะฐ | + ๐Ÿ‡ป๐Ÿ‡ณ Tiแบฟng Viแป‡t | + ๐Ÿ‡จ๐Ÿ‡ณ ็ฎ€ไฝ“ไธญๆ–‡ | + ๐Ÿ‡น๐Ÿ‡ผ ็น้ซ”ไธญๆ–‡ +

+ +![](https://img.shields.io/github/license/bluewave-labs/checkmate) +![](https://img.shields.io/github/repo-size/bluewave-labs/checkmate) +![](https://img.shields.io/github/commit-activity/m/bluewave-labs/checkmate) +![](https://img.shields.io/github/last-commit/bluewave-labs/checkmate) +![](https://img.shields.io/github/languages/top/bluewave-labs/checkmate) +![](https://img.shields.io/github/issues/bluewave-labs/checkmate) +![](https://img.shields.io/github/issues-pr/bluewave-labs/checkmate) +[![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/bluewave-labs/checkmate) + +

Checkmate

+ +

Un'applicazione open source per il monitoraggio di uptime e infrastruttura

+ +[![Run on PikaPods](https://www.pikapods.com/static/run-button.svg)](https://www.pikapods.com/pods?run=checkmate) + +image + +
+ + +Questo repository contiene sia il frontend che il backend di Checkmate, uno strumento di monitoraggio open source e self-hosted per tenere sotto controllo l'hardware dei server, l'uptime, i tempi di risposta e gli incidenti in tempo reale, con visualizzazioni accattivanti. Checkmate verifica regolarmente se un server o un sito web รจ raggiungibile e funziona in modo ottimale, fornendo avvisi e report in tempo reale su disponibilitร , tempi di inattivitร  e tempi di risposta dei servizi monitorati. + +Checkmate dispone anche di un agente, chiamato [Capture](https://github.com/bluewave-labs/capture), per raccogliere dati dai server remoti. Anche se Capture non รจ necessario per far funzionare Checkmate, offre informazioni aggiuntive su CPU, RAM, disco e temperatura dei tuoi server. Capture puรฒ essere eseguito su Linux, Windows, Mac, Raspberry Pi o qualsiasi altro dispositivo in grado di eseguire Go. + +Checkmate รจ stato sottoposto a stress test con oltre 1000 monitor attivi senza problemi particolari nรฉ colli di bottiglia nelle prestazioni. + +## ๐Ÿ“š Indice + +- [๐Ÿ“ฆ Demo](#demo) +- [๐Ÿ”— Guida utente](#users-guide) +- [๐Ÿ› ๏ธ Installazione](#installation) +- [๐Ÿš€ Prestazioni](#performance) +- [๐Ÿ’š Domande e idee](#questions--ideas) +- [๐Ÿงฉ Funzionalitร ](#features) +- [๐Ÿ—๏ธ Screenshot](#screenshots) +- [๐Ÿ—๏ธ Tech stack](#tech-stack) +- [๐Ÿ”— Alcuni link](#a-few-links) +- [๐Ÿค Come contribuire](#contributing) + + + +## Demo + +Puoi vedere l'ultima build di [Checkmate](https://demo.checkmate.so/) in azione. + +Il nome utente รจ demouser@demo.com e la password รจ Demouser1! (una nota: aggiorniamo il server demo di tanto in tanto, quindi se non dovesse funzionare, scrivici sul canale Discussions). + + +## Guida utente + +Le istruzioni per l'uso sono disponibili [qui](https://checkmate.so/docs). + +## Prerequisiti +- [Docker](https://www.docker.com/) installato +- [Git](https://git-scm.com/) installato + + +## Installazione + +Consulta le istruzioni di installazione nel [portale della documentazione di Checkmate](https://checkmate.so/docs). + +In alternativa, puoi usare [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Sudafrica), [Cloudzy](https://cloudzy.com/marketplace/checkmate) o [Pikapods](https://www.pikapods.com/) per creare rapidamente un'istanza di Checkmate. Se desideri monitorare l'infrastruttura dei tuoi server, ti servirร  l'[agente Capture](https://github.com/bluewave-labs/capture). Anche il repository di Capture contiene le istruzioni di installazione. + +### Usare una CA personalizzata + +Se hai bisogno di monitorare endpoint HTTPS interni con certificati emessi da Autoritร  di Certificazione (CA) private (come Smallstep), consulta la nostra [Guida all'attendibilitร  delle CA personalizzate](../custom-ca-trust.md) per le opzioni di configurazione di Docker. + +Per ulteriore documentazione, consulta la [cartella docs](../). + + +## Prestazioni + +Grazie a numerose ottimizzazioni, Checkmate opera con un consumo di memoria estremamente ridotto, richiedendo risorse minime di memoria e CPU. Ecco l'utilizzo di memoria di un'istanza Node.js in esecuzione su un server che monitora 323 server ogni minuto: + +![image](https://github.com/user-attachments/assets/37e04a75-d83a-488f-b25c-025511b492c9) + +Puoi vedere il consumo di memoria di MongoDB e Redis sullo stesso server (398Mb e 15Mb) per lo stesso numero di server: + +![image](https://github.com/user-attachments/assets/3b469e85-e675-4040-a162-3f24c1afc751) + + +## Domande e idee + +Se hai domande, suggerimenti o commenti, hai diverse opzioni: + +- [Canale Discord](https://discord.gg/NAb6H3UTjK) (preferito) +- [GitHub Discussions](https://github.com/bluewave-labs/Checkmate/discussions) (controlliamo di tanto in tanto) + +Non esitare a fare domande o a condividere le tue idee: ci farebbe piacere sentirti! + + +## Funzionalitร  + +- Completamente open source, distribuibile sui tuoi server o su dispositivi domestici (es. Raspberry Pi 4 o 5) +- Diverse opzioni di monitoraggio: Uptime, Docker, Ping, SSL, Port, Game server +- Monitoraggio della velocitร  delle pagine +- Monitoraggio dell'infrastruttura (memoria, utilizzo del disco, prestazioni della CPU, rete, ecc.) - richiede l'agente [Capture](https://github.com/bluewave-labs/capture) + - Monitoraggio selettivo dei dischi con selezione del mountpoint +- Incidenti a colpo d'occhio +- Pagine di stato con 4 temi curati +- Notifiche via E-mail, Webhook, Discord, Slack, PagerDuty, Matrix, Microsoft Teams, Telegram, Pushover, Twilio (SMS) +- Manutenzione programmata +- Monitoraggio tramite query JSON +- Supporto multilingua per arabo, cinese (semplificato), cinese (tradizionale, Taiwan), ceco, inglese, finlandese, francese, tedesco, giapponese, portoghese (Brasile), russo, spagnolo, thailandese, turco, ucraino e vietnamita + + +## Ciclo di vita del monitor + +1. Un monitor esegue un controllo (HTTP / ping / port / hardware tramite l'agente Capture) +2. Il risultato viene memorizzato (successo/errore + tempo di risposta) +3. I risultati dei controlli recenti vengono valutati rispetto alla soglia di cambio stato configurata per il monitor +4. Se la soglia di cambio stato del monitor viene raggiunta e lo stato attuale รจ diverso da quello precedente, lo stato del monitor cambia (es. `initializing`, `up`, `down`, `breached`) +5. Al cambio di stato: viene creato o risolto un incidente, a seconda dello stato attuale del monitor +6. Le notifiche vengono attivate in base alla configurazione + + +## Screenshot + +

+image +

+

+ image +

+

+

image +

+

+image +

+ + + + +## Tech stack + +- [ReactJs](https://react.dev/) +- [MUI (framework React)](https://mui.com/) +- [Node.js](https://nodejs.org/en) +- [MongoDB](https://mongodb.com) +- [Recharts](https://recharts.org) +- Tanti altri componenti open source! + + +## Alcuni link + +- Se vuoi supportarci, valuta di lasciare una โญ e di cliccare su "watch". +- Hai una domanda o un suggerimento per la roadmap o le funzionalitร ? Dai un'occhiata al nostro [canale Discord](https://discord.gg/NAb6H3UTjK) o al forum [Discussions](https://github.com/bluewave-labs/checkmate/discussions). +- Vuoi ricevere una notifica quando esce una nuova release? Usa [Newreleases](https://newreleases.io/), un servizio gratuito per tenere traccia delle release. +- Guarda un [video di installazione e utilizzo](https://www.youtube.com/watch?v=GfFOc0xHIwY) di Checkmate + + +## Come contribuire + +Siamo [Alex](http://github.com/ajhollid) (team lead), [Gorkem](http://github.com/gorkem-bwl/), [Aryaman](https://github.com/Br0wnHammer), [Mert](https://github.com/mertssmnoglu) e [Karen](https://github.com/karenvicent), e aiutiamo privati e aziende a monitorare la propria infrastruttura e i propri server. + +Siamo orgogliosi di costruire legami solidi con i contributori a ogni livello. Pur essendo un progetto giovane, Checkmate ha giร  raccolto oltre 7000 stelle e attirato piรน di 90 contributori da tutto il mondo. + +Il nostro repository ha ricevuto una stella da dipendenti di **Google, Microsoft, Intel, Cisco, Tencent, Electronic Arts, ByteDance, JP Morgan Chase, Deloitte, Accenture, Foxconn, Broadcom, China Telecom, Barclays, Capgemini, Wipro, Cloudflare, Dassault Systรจmes e NEC**, quindi non trattenerti: unisciti a noi, contribuisci e impara con noi! + +Ecco come puoi contribuire: + +0. Metti una stella a questo repository :) +1. Consulta la [guida per i contributori](https://github.com/bluewave-labs/Checkmate/blob/develop/CONTRIBUTING.md). Chi รจ alle prime armi รจ incoraggiato a controllare il tag `good-first-issue`. +2. Leggi una descrizione dettagliata della struttura di [Checkmate](https://deepwiki.com/bluewave-labs/Checkmate) se vuoi approfondire l'architettura. +3. Apri una issue se pensi di aver trovato un bug. +4. Se sei un nuovo arrivato, dai un'occhiata alle good-first-issue. +5. Apri una pull request per aggiungere nuove funzionalitร , apportare miglioramenti dell'esperienza d'uso o correggere bug. +6. Dai un'occhiata a questa guida interattiva alla codebase di `Checkmate` su CodeCanvas [qui](https://www.code-canvas.com/?session=unauthenticatedGithub&repo=Checkmate&owner=bluewave-labs&branch=develop&OnboardingTutorial=true). Per perfezionare le simulazioni di flusso dati esistenti o crearne di nuove, segui il breve tutorial [qui](https://docs.code-canvas.com/updating-diagram). + + + + + +[![Star History Chart](https://api.star-history.com/svg?repos=bluewave-labs/checkmate&type=Date)](https://star-history.com/#bluewave-labs/Checkmate&Date) + diff --git a/docs/translations/README.ja.md b/docs/translations/README.ja.md index c5d86e4d0..bbb446a51 100644 --- a/docs/translations/README.ja.md +++ b/docs/translations/README.ja.md @@ -9,6 +9,7 @@ ๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol | ๐Ÿ‡ซ๐Ÿ‡ฎ Suomi | ๐Ÿ‡ซ๐Ÿ‡ท Franรงais | + ๐Ÿ‡ฎ๐Ÿ‡น Italiano | ๐Ÿ‡ฏ๐Ÿ‡ต ๆ—ฅๆœฌ่ชž | ๐Ÿ‡ง๐Ÿ‡ท Portuguรชs (Brasil) | ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน | @@ -81,7 +82,7 @@ Checkmate ใฏ 1000 ไปฅไธŠใฎใ‚ขใ‚ฏใƒ†ใ‚ฃใƒ–ใชใƒขใƒ‹ใ‚ฟใƒผใงใ‚นใƒˆใƒฌใ‚นใƒ†ใ‚น ใ‚คใƒณใ‚นใƒˆใƒผใƒซๆ‰‹้ †ใฏ [Checkmate ใƒ‰ใ‚ญใƒฅใƒกใƒณใƒˆใƒใƒผใ‚ฟใƒซ](https://checkmate.so/docs) ใ‚’ๅ‚็…งใ—ใฆใใ ใ•ใ„ใ€‚ -ไปฃใ‚ใ‚Šใซใ€[Coolify](https://coolify.io/)ใ€[Elestio](https://elest.io/open-source/checkmate)ใ€[K8s](./charts/helm/checkmate/INSTALLATION.md)ใ€[Sive Host](https://sive.host)๏ผˆๅ—ใ‚ขใƒ•ใƒชใ‚ซ๏ผ‰ใ€[Cloudzy](https://cloudzy.com/marketplace/checkmate)ใ€[Pikapods](https://www.pikapods.com/) ใ‚’ไฝฟใฃใฆ Checkmate ใ‚คใƒณใ‚นใ‚ฟใƒณใ‚นใ‚’ใ™ใฐใ‚„ใ็ซ‹ใกไธŠใ’ใ‚‹ใ“ใจใ‚‚ใงใใพใ™ใ€‚ใ‚ตใƒผใƒใƒผใ‚คใƒณใƒ•ใƒฉใ‚’็›ฃ่ฆ–ใ—ใŸใ„ๅ ดๅˆใฏใ€[Capture ใ‚จใƒผใ‚ธใ‚งใƒณใƒˆ](https://github.com/bluewave-labs/capture) ใŒๅฟ…่ฆใงใ™ใ€‚Capture ใƒชใƒใ‚ธใƒˆใƒชใซใ‚‚ใ‚คใƒณใ‚นใƒˆใƒผใƒซๆ‰‹้ †ใŒ่จ˜่ผ‰ใ•ใ‚Œใฆใ„ใพใ™ใ€‚ +ไปฃใ‚ใ‚Šใซใ€[Coolify](https://coolify.io/)ใ€[Elestio](https://elest.io/open-source/checkmate)ใ€[K8s](../../charts/helm/checkmate/INSTALLATION.md)ใ€[Sive Host](https://sive.host)๏ผˆๅ—ใ‚ขใƒ•ใƒชใ‚ซ๏ผ‰ใ€[Cloudzy](https://cloudzy.com/marketplace/checkmate)ใ€[Pikapods](https://www.pikapods.com/) ใ‚’ไฝฟใฃใฆ Checkmate ใ‚คใƒณใ‚นใ‚ฟใƒณใ‚นใ‚’ใ™ใฐใ‚„ใ็ซ‹ใกไธŠใ’ใ‚‹ใ“ใจใ‚‚ใงใใพใ™ใ€‚ใ‚ตใƒผใƒใƒผใ‚คใƒณใƒ•ใƒฉใ‚’็›ฃ่ฆ–ใ—ใŸใ„ๅ ดๅˆใฏใ€[Capture ใ‚จใƒผใ‚ธใ‚งใƒณใƒˆ](https://github.com/bluewave-labs/capture) ใŒๅฟ…่ฆใงใ™ใ€‚Capture ใƒชใƒใ‚ธใƒˆใƒชใซใ‚‚ใ‚คใƒณใ‚นใƒˆใƒผใƒซๆ‰‹้ †ใŒ่จ˜่ผ‰ใ•ใ‚Œใฆใ„ใพใ™ใ€‚ ### ใ‚ซใ‚นใ‚ฟใƒ  CA ใฎๅˆฉ็”จ diff --git a/docs/translations/README.pt-BR.md b/docs/translations/README.pt-BR.md index e96f7863e..ee35ca4f7 100644 --- a/docs/translations/README.pt-BR.md +++ b/docs/translations/README.pt-BR.md @@ -9,6 +9,7 @@ ๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol | ๐Ÿ‡ซ๐Ÿ‡ฎ Suomi | ๐Ÿ‡ซ๐Ÿ‡ท Franรงais | + ๐Ÿ‡ฎ๐Ÿ‡น Italiano | ๐Ÿ‡ฏ๐Ÿ‡ต ๆ—ฅๆœฌ่ชž | ๐Ÿ‡ง๐Ÿ‡ท Portuguรชs (Brasil) | ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน | @@ -81,7 +82,7 @@ As instruรงรตes de uso podem ser encontradas [aqui](https://checkmate.so/docs). Veja as instruรงรตes de instalaรงรฃo no [portal de documentaรงรฃo do Checkmate](https://checkmate.so/docs). -Alternativamente, vocรช pode usar [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (รfrica do Sul), [Cloudzy](https://cloudzy.com/marketplace/checkmate) ou [Pikapods](https://www.pikapods.com/) para subir rapidamente uma instรขncia do Checkmate. Se quiser monitorar sua infraestrutura de servidores, vocรช precisarรก do [agente Capture](https://github.com/bluewave-labs/capture). O repositรณrio do Capture tambรฉm contรฉm as instruรงรตes de instalaรงรฃo. +Alternativamente, vocรช pode usar [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (รfrica do Sul), [Cloudzy](https://cloudzy.com/marketplace/checkmate) ou [Pikapods](https://www.pikapods.com/) para subir rapidamente uma instรขncia do Checkmate. Se quiser monitorar sua infraestrutura de servidores, vocรช precisarรก do [agente Capture](https://github.com/bluewave-labs/capture). O repositรณrio do Capture tambรฉm contรฉm as instruรงรตes de instalaรงรฃo. ### Usando uma CA personalizada diff --git a/docs/translations/README.ru.md b/docs/translations/README.ru.md index 4f5d20725..d9b5dcb1c 100644 --- a/docs/translations/README.ru.md +++ b/docs/translations/README.ru.md @@ -9,6 +9,7 @@ ๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol | ๐Ÿ‡ซ๐Ÿ‡ฎ Suomi | ๐Ÿ‡ซ๐Ÿ‡ท Franรงais | + ๐Ÿ‡ฎ๐Ÿ‡น Italiano | ๐Ÿ‡ฏ๐Ÿ‡ต ๆ—ฅๆœฌ่ชž | ๐Ÿ‡ง๐Ÿ‡ท Portuguรชs (Brasil) | ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน | @@ -81,7 +82,7 @@ Checkmate ะฟั€ะพัˆั‘ะป ะฝะฐะณั€ัƒะทะพั‡ะฝะพะต ั‚ะตัั‚ะธั€ะพะฒะฐะฝะธะต ั 1000+ ะ˜ะฝัั‚ั€ัƒะบั†ะธะธ ะฟะพ ัƒัั‚ะฐะฝะพะฒะบะต ัะผะพั‚ั€ะธั‚ะต ะฝะฐ [ะฟะพั€ั‚ะฐะปะต ะดะพะบัƒะผะตะฝั‚ะฐั†ะธะธ Checkmate](https://checkmate.so/docs). -ะ’ ะบะฐั‡ะตัั‚ะฒะต ะฐะปัŒั‚ะตั€ะฝะฐั‚ะธะฒั‹ ะฒั‹ ะผะพะถะตั‚ะต ะธัะฟะพะปัŒะทะพะฒะฐั‚ัŒ [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (ะฎะะ ), [Cloudzy](https://cloudzy.com/marketplace/checkmate) ะธะปะธ [Pikapods](https://www.pikapods.com/), ั‡ั‚ะพะฑั‹ ะฑั‹ัั‚ั€ะพ ะฟะพะดะฝัั‚ัŒ ะธะฝัั‚ะฐะฝั Checkmate. ะ•ัะปะธ ะฒั‹ ั…ะพั‚ะธั‚ะต ะผะพะฝะธั‚ะพั€ะธั‚ัŒ ัะตั€ะฒะตั€ะฝัƒัŽ ะธะฝั„ั€ะฐัั‚ั€ัƒะบั‚ัƒั€ัƒ, ะฟะพั‚ั€ะตะฑัƒะตั‚ัั ะฐะณะตะฝั‚ [Capture](https://github.com/bluewave-labs/capture). ะ’ ั€ะตะฟะพะทะธั‚ะพั€ะธะธ Capture ั‚ะฐะบะถะต ะตัั‚ัŒ ะธะฝัั‚ั€ัƒะบั†ะธะธ ะฟะพ ัƒัั‚ะฐะฝะพะฒะบะต. +ะ’ ะบะฐั‡ะตัั‚ะฒะต ะฐะปัŒั‚ะตั€ะฝะฐั‚ะธะฒั‹ ะฒั‹ ะผะพะถะตั‚ะต ะธัะฟะพะปัŒะทะพะฒะฐั‚ัŒ [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (ะฎะะ ), [Cloudzy](https://cloudzy.com/marketplace/checkmate) ะธะปะธ [Pikapods](https://www.pikapods.com/), ั‡ั‚ะพะฑั‹ ะฑั‹ัั‚ั€ะพ ะฟะพะดะฝัั‚ัŒ ะธะฝัั‚ะฐะฝั Checkmate. ะ•ัะปะธ ะฒั‹ ั…ะพั‚ะธั‚ะต ะผะพะฝะธั‚ะพั€ะธั‚ัŒ ัะตั€ะฒะตั€ะฝัƒัŽ ะธะฝั„ั€ะฐัั‚ั€ัƒะบั‚ัƒั€ัƒ, ะฟะพั‚ั€ะตะฑัƒะตั‚ัั ะฐะณะตะฝั‚ [Capture](https://github.com/bluewave-labs/capture). ะ’ ั€ะตะฟะพะทะธั‚ะพั€ะธะธ Capture ั‚ะฐะบะถะต ะตัั‚ัŒ ะธะฝัั‚ั€ัƒะบั†ะธะธ ะฟะพ ัƒัั‚ะฐะฝะพะฒะบะต. ### ะ˜ัะฟะพะปัŒะทะพะฒะฐะฝะธะต ัะพะฑัั‚ะฒะตะฝะฝะพะณะพ ะฃะฆ diff --git a/docs/translations/README.th.md b/docs/translations/README.th.md index 0ea5de3fb..61b4953ca 100644 --- a/docs/translations/README.th.md +++ b/docs/translations/README.th.md @@ -9,6 +9,7 @@ ๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol | ๐Ÿ‡ซ๐Ÿ‡ฎ Suomi | ๐Ÿ‡ซ๐Ÿ‡ท Franรงais | + ๐Ÿ‡ฎ๐Ÿ‡น Italiano | ๐Ÿ‡ฏ๐Ÿ‡ต ๆ—ฅๆœฌ่ชž | ๐Ÿ‡ง๐Ÿ‡ท Portuguรชs (Brasil) | ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน | @@ -84,7 +85,7 @@ Checkmate เธœเนˆเธฒเธ™เธเธฒเธฃเธ—เธ”เธชเธญเธšเน‚เธซเธฅเธ”เธชเธณเธซเธฃเธฑเธšเธก เธ”เธนเธงเธดเธ˜เธตเธเธฒเธฃเธ•เธดเธ”เธ•เธฑเน‰เธ‡เนƒเธ™ [เธžเธญเธฃเนŒเธ—เธฑเธฅเน€เธญเธเธชเธฒเธฃเธ‚เธญเธ‡ Checkmate](https://checkmate.so/docs) -เธญเธตเธเธ—เธฒเธ‡เน€เธฅเธทเธญเธเธซเธ™เธถเนˆเธ‡ เธ„เธธเธ“เธชเธฒเธกเธฒเธฃเธ–เนƒเธŠเน‰ [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (เนเธญเธŸเธฃเธดเธเธฒเนƒเธ•เน‰), [Cloudzy](https://cloudzy.com/marketplace/checkmate) เธซเธฃเธทเธญ [Pikapods](https://www.pikapods.com/) เน€เธžเธทเนˆเธญเน€เธ›เธดเธ”เธญเธดเธ™เธชเนเธ•เธ™เธ‹เนŒ Checkmate เน„เธ”เน‰เธญเธขเนˆเธฒเธ‡เธฃเธงเธ”เน€เธฃเน‡เธง เธซเธฒเธเธ•เน‰เธญเธ‡เธเธฒเธฃเธกเธญเธ™เธดเน€เธ•เธญเธฃเนŒเน‚เธ„เธฃเธ‡เธชเธฃเน‰เธฒเธ‡เธžเธทเน‰เธ™เธเธฒเธ™เน€เธ‹เธดเธฃเนŒเธŸเน€เธงเธญเธฃเนŒ เธ„เธธเธ“เธˆเธฐเธ•เน‰เธญเธ‡เนƒเธŠเน‰ [เน€เธญเน€เธˆเธ™เธ•เนŒ Capture](https://github.com/bluewave-labs/capture) เธฃเธตเน‚เธž Capture เธขเธฑเธ‡เธกเธตเธงเธดเธ˜เธตเธเธฒเธฃเธ•เธดเธ”เธ•เธฑเน‰เธ‡เธ”เน‰เธงเธข +เธญเธตเธเธ—เธฒเธ‡เน€เธฅเธทเธญเธเธซเธ™เธถเนˆเธ‡ เธ„เธธเธ“เธชเธฒเธกเธฒเธฃเธ–เนƒเธŠเน‰ [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (เนเธญเธŸเธฃเธดเธเธฒเนƒเธ•เน‰), [Cloudzy](https://cloudzy.com/marketplace/checkmate) เธซเธฃเธทเธญ [Pikapods](https://www.pikapods.com/) เน€เธžเธทเนˆเธญเน€เธ›เธดเธ”เธญเธดเธ™เธชเนเธ•เธ™เธ‹เนŒ Checkmate เน„เธ”เน‰เธญเธขเนˆเธฒเธ‡เธฃเธงเธ”เน€เธฃเน‡เธง เธซเธฒเธเธ•เน‰เธญเธ‡เธเธฒเธฃเธกเธญเธ™เธดเน€เธ•เธญเธฃเนŒเน‚เธ„เธฃเธ‡เธชเธฃเน‰เธฒเธ‡เธžเธทเน‰เธ™เธเธฒเธ™เน€เธ‹เธดเธฃเนŒเธŸเน€เธงเธญเธฃเนŒ เธ„เธธเธ“เธˆเธฐเธ•เน‰เธญเธ‡เนƒเธŠเน‰ [เน€เธญเน€เธˆเธ™เธ•เนŒ Capture](https://github.com/bluewave-labs/capture) เธฃเธตเน‚เธž Capture เธขเธฑเธ‡เธกเธตเธงเธดเธ˜เธตเธเธฒเธฃเธ•เธดเธ”เธ•เธฑเน‰เธ‡เธ”เน‰เธงเธข ### เธเธฒเธฃเนƒเธŠเน‰ Custom CA diff --git a/docs/translations/README.tr.md b/docs/translations/README.tr.md index 48c56f3b0..7e41aec43 100644 --- a/docs/translations/README.tr.md +++ b/docs/translations/README.tr.md @@ -9,6 +9,7 @@ ๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol | ๐Ÿ‡ซ๐Ÿ‡ฎ Suomi | ๐Ÿ‡ซ๐Ÿ‡ท Franรงais | + ๐Ÿ‡ฎ๐Ÿ‡น Italiano | ๐Ÿ‡ฏ๐Ÿ‡ต ๆ—ฅๆœฌ่ชž | ๐Ÿ‡ง๐Ÿ‡ท Portuguรชs (Brasil) | ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน | @@ -81,7 +82,7 @@ Kullanฤฑm talimatlarฤฑnฤฑ [burada](https://checkmate.so/docs) bulabilirsiniz. Kurulum talimatlarฤฑ iรงin [Checkmate dokรผmantasyon portalฤฑna](https://checkmate.so/docs) bakฤฑn. -Alternatif olarak hฤฑzlฤฑca bir Checkmate รถrneฤŸi รงalฤฑลŸtฤฑrmak iรงin [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Gรผney Afrika), [Cloudzy](https://cloudzy.com/marketplace/checkmate) veya [Pikapods](https://www.pikapods.com/)'u kullanabilirsiniz. Sunucu altyapฤฑnฤฑzฤฑ izlemek isterseniz [Capture ajanฤฑna](https://github.com/bluewave-labs/capture) ihtiyacฤฑnฤฑz olacak. Capture deposu da kurulum talimatlarฤฑnฤฑ iรงerir. +Alternatif olarak hฤฑzlฤฑca bir Checkmate รถrneฤŸi รงalฤฑลŸtฤฑrmak iรงin [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Gรผney Afrika), [Cloudzy](https://cloudzy.com/marketplace/checkmate) veya [Pikapods](https://www.pikapods.com/)'u kullanabilirsiniz. Sunucu altyapฤฑnฤฑzฤฑ izlemek isterseniz [Capture ajanฤฑna](https://github.com/bluewave-labs/capture) ihtiyacฤฑnฤฑz olacak. Capture deposu da kurulum talimatlarฤฑnฤฑ iรงerir. ### ร–zel CA Kullanฤฑmฤฑ diff --git a/docs/translations/README.uk.md b/docs/translations/README.uk.md index 3f7b4630a..7322afac7 100644 --- a/docs/translations/README.uk.md +++ b/docs/translations/README.uk.md @@ -9,6 +9,7 @@ ๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol | ๐Ÿ‡ซ๐Ÿ‡ฎ Suomi | ๐Ÿ‡ซ๐Ÿ‡ท Franรงais | + ๐Ÿ‡ฎ๐Ÿ‡น Italiano | ๐Ÿ‡ฏ๐Ÿ‡ต ๆ—ฅๆœฌ่ชž | ๐Ÿ‡ง๐Ÿ‡ท Portuguรชs (Brasil) | ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน | @@ -84,7 +85,7 @@ Checkmate ะฟั€ะพะนัˆะพะฒ ะฝะฐะฒะฐะฝั‚ะฐะถัƒะฒะฐะปัŒะฝะต ั‚ะตัั‚ัƒะฒะฐะฝะฝั ะท ะ†ะฝัั‚ั€ัƒะบั†ั–ั— ะทั– ะฒัั‚ะฐะฝะพะฒะปะตะฝะฝั ะดะธะฒั–ั‚ัŒัั ัƒ [ะฟะพั€ั‚ะฐะปั– ะดะพะบัƒะผะตะฝั‚ะฐั†ั–ั— Checkmate](https://checkmate.so/docs). -ะะปัŒั‚ะตั€ะฝะฐั‚ะธะฒะฝะพ, ะดะปั ัˆะฒะธะดะบะพะณะพ ั€ะพะทะณะพั€ั‚ะฐะฝะฝั Checkmate ะผะพะถะฝะฐ ัะบะพั€ะธัั‚ะฐั‚ะธัั [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (ะŸั–ะฒะดะตะฝะฝะฐ ะั„ั€ะธะบะฐ), [Cloudzy](https://cloudzy.com/marketplace/checkmate) ะฐะฑะพ [Pikapods](https://www.pikapods.com/). ะฏะบั‰ะพ ั…ะพั‡ะตั‚ะต ะผะพะฝั–ั‚ะพั€ะธั‚ะธ ัะตั€ะฒะตั€ะฝัƒ ั–ะฝั„ั€ะฐัั‚ั€ัƒะบั‚ัƒั€ัƒ, ะทะฝะฐะดะพะฑะธั‚ัŒัั ะฐะณะตะฝั‚ [Capture](https://github.com/bluewave-labs/capture). ะฃ ั€ะตะฟะพะทะธั‚ะพั€ั–ั— Capture ั‚ะฐะบะพะถ ั” ั–ะฝัั‚ั€ัƒะบั†ั–ั— ะทั– ะฒัั‚ะฐะฝะพะฒะปะตะฝะฝั. +ะะปัŒั‚ะตั€ะฝะฐั‚ะธะฒะฝะพ, ะดะปั ัˆะฒะธะดะบะพะณะพ ั€ะพะทะณะพั€ั‚ะฐะฝะฝั Checkmate ะผะพะถะฝะฐ ัะบะพั€ะธัั‚ะฐั‚ะธัั [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (ะŸั–ะฒะดะตะฝะฝะฐ ะั„ั€ะธะบะฐ), [Cloudzy](https://cloudzy.com/marketplace/checkmate) ะฐะฑะพ [Pikapods](https://www.pikapods.com/). ะฏะบั‰ะพ ั…ะพั‡ะตั‚ะต ะผะพะฝั–ั‚ะพั€ะธั‚ะธ ัะตั€ะฒะตั€ะฝัƒ ั–ะฝั„ั€ะฐัั‚ั€ัƒะบั‚ัƒั€ัƒ, ะทะฝะฐะดะพะฑะธั‚ัŒัั ะฐะณะตะฝั‚ [Capture](https://github.com/bluewave-labs/capture). ะฃ ั€ะตะฟะพะทะธั‚ะพั€ั–ั— Capture ั‚ะฐะบะพะถ ั” ั–ะฝัั‚ั€ัƒะบั†ั–ั— ะทั– ะฒัั‚ะฐะฝะพะฒะปะตะฝะฝั. ### ะ’ะธะบะพั€ะธัั‚ะฐะฝะฝั ะฒะปะฐัะฝะพะณะพ ะฃะฆ diff --git a/docs/translations/README.vi.md b/docs/translations/README.vi.md index 13369f3d8..36bbac7ca 100644 --- a/docs/translations/README.vi.md +++ b/docs/translations/README.vi.md @@ -9,6 +9,7 @@ ๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol | ๐Ÿ‡ซ๐Ÿ‡ฎ Suomi | ๐Ÿ‡ซ๐Ÿ‡ท Franรงais | + ๐Ÿ‡ฎ๐Ÿ‡น Italiano | ๐Ÿ‡ฏ๐Ÿ‡ต ๆ—ฅๆœฌ่ชž | ๐Ÿ‡ง๐Ÿ‡ท Portuguรชs (Brasil) | ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน | @@ -84,7 +85,7 @@ Cรณ thแปƒ tรฌm thแบฅy hฦฐแป›ng dแบซn sแปญ dแปฅng [tแบกi ฤ‘รขy](https://checkmate Xem hฦฐแป›ng dแบซn cร i ฤ‘แบทt trong [cแป•ng tร i liแป‡u cแปงa Checkmate](https://checkmate.so/docs). -Hoแบทc bแบกn cรณ thแปƒ dรนng [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Nam Phi), [Cloudzy](https://cloudzy.com/marketplace/checkmate) hoแบทc [Pikapods](https://www.pikapods.com/) ฤ‘แปƒ dแปฑng nhanh mแป™t instance Checkmate. Nแบฟu muแป‘n giรกm sรกt hแบก tแบงng mรกy chแปง, bแบกn sแบฝ cแบงn [agent Capture](https://github.com/bluewave-labs/capture). Kho Capture cลฉng chแปฉa hฦฐแป›ng dแบซn cร i ฤ‘แบทt. +Hoแบทc bแบกn cรณ thแปƒ dรนng [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](../../charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (Nam Phi), [Cloudzy](https://cloudzy.com/marketplace/checkmate) hoแบทc [Pikapods](https://www.pikapods.com/) ฤ‘แปƒ dแปฑng nhanh mแป™t instance Checkmate. Nแบฟu muแป‘n giรกm sรกt hแบก tแบงng mรกy chแปง, bแบกn sแบฝ cแบงn [agent Capture](https://github.com/bluewave-labs/capture). Kho Capture cลฉng chแปฉa hฦฐแป›ng dแบซn cร i ฤ‘แบทt. ### Sแปญ dแปฅng CA tuแปณ chแป‰nh diff --git a/docs/translations/README.zh-CN.md b/docs/translations/README.zh-CN.md index 75ae7dc86..c3916509a 100644 --- a/docs/translations/README.zh-CN.md +++ b/docs/translations/README.zh-CN.md @@ -9,6 +9,7 @@ ๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol | ๐Ÿ‡ซ๐Ÿ‡ฎ Suomi | ๐Ÿ‡ซ๐Ÿ‡ท Franรงais | + ๐Ÿ‡ฎ๐Ÿ‡น Italiano | ๐Ÿ‡ฏ๐Ÿ‡ต ๆ—ฅๆœฌ่ชž | ๐Ÿ‡ง๐Ÿ‡ท Portuguรชs (Brasil) | ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน | @@ -81,7 +82,7 @@ Checkmate ๅทฒ็ปๅœจ 1000+ ไธชๆดป่ทƒ็›‘ๆŽง้กน็š„็Žฏๅขƒไธ‹ๅฎŒๆˆๅŽ‹ๅŠ›ๆต‹่ฏ•๏ผŒๆœช ่ฏทๅ‚้˜… [Checkmate ๆ–‡ๆกฃ้—จๆˆท](https://checkmate.so/docs)ไธญ็š„ๅฎ‰่ฃ…่ฏดๆ˜Žใ€‚ -ไฝ ไนŸๅฏไปฅไฝฟ็”จ [Coolify](https://coolify.io/)ใ€[Elestio](https://elest.io/open-source/checkmate)ใ€[K8s](./charts/helm/checkmate/INSTALLATION.md)ใ€[Sive Host](https://sive.host)๏ผˆๅ—้ž๏ผ‰ใ€[Cloudzy](https://cloudzy.com/marketplace/checkmate) ๆˆ– [Pikapods](https://www.pikapods.com/) ๅฟซ้€ŸๅฏๅŠจไธ€ไธช Checkmate ๅฎžไพ‹ใ€‚ๅฆ‚ๆžœๆƒณ็›‘ๆŽงๆœๅŠกๅ™จๅŸบ็ก€่ฎพๆ–ฝ๏ผŒไฝ ้œ€่ฆ [Capture ไปฃ็†](https://github.com/bluewave-labs/capture)๏ผŒCapture ไป“ๅบ“ไธญไนŸๆไพ›ไบ†ๅฎ‰่ฃ…่ฏดๆ˜Žใ€‚ +ไฝ ไนŸๅฏไปฅไฝฟ็”จ [Coolify](https://coolify.io/)ใ€[Elestio](https://elest.io/open-source/checkmate)ใ€[K8s](../../charts/helm/checkmate/INSTALLATION.md)ใ€[Sive Host](https://sive.host)๏ผˆๅ—้ž๏ผ‰ใ€[Cloudzy](https://cloudzy.com/marketplace/checkmate) ๆˆ– [Pikapods](https://www.pikapods.com/) ๅฟซ้€ŸๅฏๅŠจไธ€ไธช Checkmate ๅฎžไพ‹ใ€‚ๅฆ‚ๆžœๆƒณ็›‘ๆŽงๆœๅŠกๅ™จๅŸบ็ก€่ฎพๆ–ฝ๏ผŒไฝ ้œ€่ฆ [Capture ไปฃ็†](https://github.com/bluewave-labs/capture)๏ผŒCapture ไป“ๅบ“ไธญไนŸๆไพ›ไบ†ๅฎ‰่ฃ…่ฏดๆ˜Žใ€‚ ### ไฝฟ็”จ่‡ชๅฎšไน‰ CA diff --git a/docs/translations/README.zh-TW.md b/docs/translations/README.zh-TW.md index 8dfc605ec..cbf57b9fe 100644 --- a/docs/translations/README.zh-TW.md +++ b/docs/translations/README.zh-TW.md @@ -9,6 +9,7 @@ ๐Ÿ‡ช๐Ÿ‡ธ Espaรฑol | ๐Ÿ‡ซ๐Ÿ‡ฎ Suomi | ๐Ÿ‡ซ๐Ÿ‡ท Franรงais | + ๐Ÿ‡ฎ๐Ÿ‡น Italiano | ๐Ÿ‡ฏ๐Ÿ‡ต ๆ—ฅๆœฌ่ชž | ๐Ÿ‡ง๐Ÿ‡ท Portuguรชs (Brasil) | ๐Ÿ‡ท๐Ÿ‡บ ะ ัƒััะบะธะน | @@ -81,7 +82,7 @@ Checkmate ๅทฒๅœจ 1000+ ๅ€‹ๆดป่บ็›ฃๆŽง้ …็›ฎไธ‹้€ฒ่กŒ้Žๅฃ“ๅŠ›ๆธฌ่ฉฆ,ๆœช็™ผ็”Ÿ ๅฎ‰่ฃ่ชชๆ˜Ž่ซ‹ๅƒ้–ฑ [Checkmate ๆ–‡ไปถๅ…ฅๅฃ](https://checkmate.so/docs)ใ€‚ -ไฝ ไนŸๅฏไปฅไฝฟ็”จ [Coolify](https://coolify.io/)ใ€[Elestio](https://elest.io/open-source/checkmate)ใ€[K8s](./charts/helm/checkmate/INSTALLATION.md)ใ€[Sive Host](https://sive.host)(ๅ—้ž)ใ€[Cloudzy](https://cloudzy.com/marketplace/checkmate) ๆˆ– [Pikapods](https://www.pikapods.com/) ไพ†ๅฟซ้€Ÿๅ•Ÿๅ‹•ไธ€ๅ€‹ Checkmate ๅฏฆไพ‹ใ€‚่‹ฅ่ฆ็›ฃๆŽงไผบๆœๅ™จๅŸบ็คŽๆžถๆง‹,้œ€่ฆ [Capture ไปฃ็†็จ‹ๅผ](https://github.com/bluewave-labs/capture),Capture ๅ„ฒๅญ˜ๅบซไธญไนŸๆไพ›ไบ†ๅฎ‰่ฃ่ชชๆ˜Žใ€‚ +ไฝ ไนŸๅฏไปฅไฝฟ็”จ [Coolify](https://coolify.io/)ใ€[Elestio](https://elest.io/open-source/checkmate)ใ€[K8s](../../charts/helm/checkmate/INSTALLATION.md)ใ€[Sive Host](https://sive.host)(ๅ—้ž)ใ€[Cloudzy](https://cloudzy.com/marketplace/checkmate) ๆˆ– [Pikapods](https://www.pikapods.com/) ไพ†ๅฟซ้€Ÿๅ•Ÿๅ‹•ไธ€ๅ€‹ Checkmate ๅฏฆไพ‹ใ€‚่‹ฅ่ฆ็›ฃๆŽงไผบๆœๅ™จๅŸบ็คŽๆžถๆง‹,้œ€่ฆ [Capture ไปฃ็†็จ‹ๅผ](https://github.com/bluewave-labs/capture),Capture ๅ„ฒๅญ˜ๅบซไธญไนŸๆไพ›ไบ†ๅฎ‰่ฃ่ชชๆ˜Žใ€‚ ### ไฝฟ็”จ่‡ช่จ‚ CA diff --git a/server/.github/pull_request_template.md b/server/.github/pull_request_template.md deleted file mode 100755 index 4f4da5d80..000000000 --- a/server/.github/pull_request_template.md +++ /dev/null @@ -1,25 +0,0 @@ -**(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. - -## Write your issue number after "Fixes " - -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. -- [ ] I have included the issue # in the PR. -- [ ] I have added i18n support to visible strings (instead of `
Add
`, use): - -```Javascript -const { t } = useTranslation(); -
{t('add')}
-``` - -- [ ] The issue I am working on is assigned to me. -- [ ] I didn't use any hardcoded values (otherwise it will not scale, and will make it difficult to maintain consistency across the application). -- [ ] My PR is granular and targeted to one specific feature. diff --git a/server/.github/workflows/staging-deploy.yml b/server/.github/workflows/staging-deploy.yml deleted file mode 100755 index 8fadf12d4..000000000 --- a/server/.github/workflows/staging-deploy.yml +++ /dev/null @@ -1,68 +0,0 @@ -name: Staging deploy - -on: - push: - branches: ["develop"] - -jobs: - docker-build-and-push: - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Build Server Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate-backend:staging \ - -f ./docker/staging/server.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate-backend \ - . - - - name: Push Server Docker image - run: docker push ghcr.io/bluewave-labs/checkmate-backend:staging - - - name: Build Mongo Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate-mongo:staging \ - -f ./docker/staging/mongoDB.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate-backend \ - . - - - name: Push MongoDB Docker image - run: docker push ghcr.io/bluewave-labs/checkmate-mongo:staging - - - name: Build Redis Docker image - run: | - docker build \ - -t ghcr.io/bluewave-labs/checkmate-redis:staging \ - -f ./docker/staging/redis.Dockerfile \ - --label org.opencontainers.image.source=https://github.com/bluewave-labs/checkmate-backend \ - . - - - name: Push Redis Docker image - run: docker push ghcr.io/bluewave-labs/checkmate-redis:staging - - deploy-to-staging: - needs: docker-build-and-push - runs-on: ubuntu-latest - steps: - - name: SSH into server and restart container using Docker Compose - uses: appleboy/ssh-action@v1.0.0 - with: - host: ${{ secrets.STAGING_SERVER_HOST }} - username: ${{ secrets.STAGING_SERVER_USER }} - key: ${{ secrets.STAGING_SERVER_SSH_KEY }} - script: | - cd checkmate/server/docker/staging - docker compose down - docker compose pull - docker compose up -d diff --git a/server/package-lock.json b/server/package-lock.json index ce35150c4..b2f01cb2a 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -867,7 +867,6 @@ "integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.7", @@ -1500,7 +1499,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" }, @@ -1545,7 +1543,6 @@ } ], "license": "MIT", - "peer": true, "engines": { "node": ">=18" } @@ -1561,6 +1558,29 @@ "kuler": "^2.0.0" } }, + "node_modules/@emnapi/core": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.2.tgz", + "integrity": "sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.2.tgz", + "integrity": "sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", @@ -1568,6 +1588,7 @@ "dev": true, "license": "MIT", "optional": true, + "peer": true, "dependencies": { "tslib": "^2.4.0" } @@ -4239,7 +4260,6 @@ "integrity": "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", @@ -4400,7 +4420,6 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-24.9.1.tgz", "integrity": "sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==", "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -4638,7 +4657,6 @@ "integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.56.1", "@typescript-eslint/types": "8.56.1", @@ -5339,7 +5357,6 @@ "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -5747,7 +5764,6 @@ "integrity": "sha512-Z0oHEHAFDZkffN8Qc39zNZjQlMDkPJRyyyZieU1VH7u8c5S+qHZ2S8ixdKIAxEjfHO7FJxXmJWgteOghVanIsg==", "dev": true, "license": "Apache-2.0", - "peer": true, "peerDependencies": { "bare-abort-controller": "*" }, @@ -6073,7 +6089,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.42", "caniuse-lite": "^1.0.30001800", @@ -6959,7 +6974,6 @@ "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.9.tgz", "integrity": "sha512-uPR75+5Dk/WJ/YSPR1/YDHdwMM9c5FsaARljfKWgeCKLKOtJ0we21xy/RcCjn53fZnD/f6yYEIZ8pu18+GnbNQ==", "license": "MIT", - "peer": true, "dependencies": { "cssnano-preset-default": "^7.0.17", "lilconfig": "^3.1.3" @@ -7746,7 +7760,6 @@ "integrity": "sha512-t5aPOpmtJcZcz5UJyY2GbvpDlsK5E8JqRqoKtfiKE3cNh437KIqfJr3A3AKf5k64NPx6d0G3dno6XDY05PqPtw==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -8124,7 +8137,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-4.22.2.tgz", "integrity": "sha512-IuL+Elrou2ZvCFHs18/CIzy2Nzvo25nZ1/D2eIZlz7c+QUayAcYoiM2BthCjs+EBHVpjYjcuLDAiCWgeIX3X1Q==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -9841,7 +9853,6 @@ "integrity": "sha512-Yi1jqNC/Oq0N4hBgNH/YvBpP1P57QqundgytzYqy3yqAa7NZPNjSoi4SGbRAXDMdBzNE6xBCi5U7RgfrvMEUVQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@jest/core": "30.4.2", "@jest/types": "30.4.1", @@ -13146,7 +13157,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", @@ -15239,7 +15249,6 @@ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -15409,7 +15418,6 @@ "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@cspotcode/source-map-support": "^0.8.0", "@tsconfig/node10": "^1.0.7", @@ -15573,7 +15581,6 @@ "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -16362,7 +16369,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/server/package.json b/server/package.json index ad266794d..e67bcdb78 100755 --- a/server/package.json +++ b/server/package.json @@ -15,6 +15,7 @@ "worker:dev": "QUEUE_MODE=worker nodemon --exec tsx src/index.js", "clean": "rm -rf dist", "build": "npm run clean && npm run build:openapi && tsc -p tsconfig.build.json && tsc-alias -p tsconfig.build.json && cp -r src/templates dist/templates", + "typeCheck": "tsc --noEmit", "build:openapi": "tsc --noEmit && tsx scripts/buildOpenApi.ts", "lint": "eslint .", "lint-fix": "eslint --fix .", diff --git a/server/scripts/inject-vars.sh b/server/scripts/inject-vars.sh deleted file mode 100755 index 34365b0d3..000000000 --- a/server/scripts/inject-vars.sh +++ /dev/null @@ -1,7 +0,0 @@ -for i in $(env | grep UPTIME_APP_) -do - key=$(echo $i | cut -d '=' -f 1) - value=$(echo $i | cut -d '=' -f 2-) - echo $key=$value - find ./public/assets -type f \( -name '*.js' -o -name '*.css' \) -exec sed -i "s|${key}|${value}|g" '{}' + -done \ No newline at end of file diff --git a/server/src/api/controllers/authController.ts b/server/src/api/controllers/authController.ts index 45f806a8c..ab8d1fc61 100755 --- a/server/src/api/controllers/authController.ts +++ b/server/src/api/controllers/authController.ts @@ -1,4 +1,5 @@ -import { Request, Response, NextFunction } from "express"; +import { Request, Response, RequestHandler } from "express"; +import { catchAsync } from "@/utils/catchAsync.js"; import { AppError } from "@/utils/AppError.js"; import { requireTeamId, requireUserEmail, requireUserId, requireUserRoles } from "@/api/controllers/controllerUtils.js"; @@ -21,271 +22,207 @@ import { } from "@/api/validation/userValidation.js"; import { IUserService } from "@/domain/users/user.service.js"; -const SERVICE_NAME = "authController"; - export interface IAuthController { - registerUser(req: Request, res: Response, next: NextFunction): Promise; - createUser(req: Request, res: Response, next: NextFunction): Promise; - loginUser(req: Request, res: Response, next: NextFunction): Promise; - editUser(req: Request, res: Response, next: NextFunction): Promise; - checkSuperadminExists(req: Request, res: Response, next: NextFunction): Promise; - requestRecovery(req: Request, res: Response, next: NextFunction): Promise; - validateRecovery(req: Request, res: Response, next: NextFunction): Promise; - resetPassword(req: Request, res: Response, next: NextFunction): Promise; - deleteUser(req: Request, res: Response, next: NextFunction): Promise; - deleteUserById(req: Request, res: Response, next: NextFunction): Promise; - getAllUsers(req: Request, res: Response, next: NextFunction): Promise; - getUserById(req: Request, res: Response, next: NextFunction): Promise; - editUserById(req: Request, res: Response, next: NextFunction): Promise; - editUserPasswordById(req: Request, res: Response, next: NextFunction): Promise; + registerUser: RequestHandler; + createUser: RequestHandler; + loginUser: RequestHandler; + editUser: RequestHandler; + checkSuperadminExists: RequestHandler; + requestRecovery: RequestHandler; + validateRecovery: RequestHandler; + resetPassword: RequestHandler; + deleteUser: RequestHandler; + deleteUserById: RequestHandler; + getAllUsers: RequestHandler; + getUserById: RequestHandler; + editUserById: RequestHandler; + editUserPasswordById: RequestHandler; } class AuthController implements IAuthController { - static SERVICE_NAME = SERVICE_NAME; - private userService: IUserService; constructor(userService: IUserService) { this.userService = userService; } - get serviceName() { - return AuthController.SERVICE_NAME; - } - - registerUser = async (req: Request, res: Response, next: NextFunction) => { - try { - const newUser = req.body.user; - const newUserToken = req.body.token; - if (newUser?.email) { - const newUserEmail = requireUserEmail(newUser.email); - newUser.email = newUserEmail.toLowerCase(); - } - const validatedBody = registrationBodyValidation.parse(newUser); - - const { user, token } = await this.userService.registerUser(validatedBody, newUserToken, req?.file ?? null); - res.status(200).json({ - success: true, - msg: "User registered successfully", - data: { user, token }, - }); - } catch (error) { - next(error); + registerUser = catchAsync(async (req: Request, res: Response) => { + const newUser = req.body.user; + const newUserToken = req.body.token; + if (newUser?.email) { + const newUserEmail = requireUserEmail(newUser.email); + newUser.email = newUserEmail.toLowerCase(); } - }; + const validatedBody = registrationBodyValidation.parse(newUser); - createUser = async (req: Request, res: Response, next: NextFunction) => { - try { - const userData = req.body; - if (userData?.email) { - userData.email = userData.email.toLowerCase(); - } - const validatedBody = createUserBodyValidation.parse(userData); + const { user, token } = await this.userService.registerUser(validatedBody, newUserToken, req?.file ?? null); + res.status(200).json({ + success: true, + msg: "User registered successfully", + data: { user, token }, + }); + }); - const teamId = requireTeamId(req.user?.teamId); - const actorRoles = requireUserRoles(req.user?.role); - const newUser = await this.userService.createUser(validatedBody, teamId, actorRoles, req?.file ?? null); - res.status(201).json({ - success: true, - msg: "User created successfully", - data: newUser, - }); - } catch (error) { - next(error); + createUser = catchAsync(async (req: Request, res: Response) => { + const userData = req.body; + if (userData?.email) { + userData.email = userData.email.toLowerCase(); } - }; + const validatedBody = createUserBodyValidation.parse(userData); - loginUser = async (req: Request, res: Response, next: NextFunction) => { - try { - if (req.body?.email) { - req.body.email = req.body.email?.toLowerCase(); - } - loginValidation.parse(req.body); - const { user, token } = await this.userService.loginUser(req.body.email, req.body.password); + const teamId = requireTeamId(req.user?.teamId); + const actorRoles = requireUserRoles(req.user?.role); + const newUser = await this.userService.createUser(validatedBody, teamId, actorRoles, req?.file ?? null); + res.status(201).json({ + success: true, + msg: "User created successfully", + data: newUser, + }); + }); - return res.status(200).json({ - success: true, - msg: "User logged in successfully", - data: { - user, - token, - }, - }); - } catch (error) { - next(error); + loginUser = catchAsync(async (req: Request, res: Response) => { + if (req.body?.email) { + req.body.email = req.body.email?.toLowerCase(); } - }; + loginValidation.parse(req.body); + const { user, token } = await this.userService.loginUser(req.body.email, req.body.password); - editUser = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedBody = editUserBodyValidation.parse(req.body); - const userId = requireUserId(req.user?.id); - const userEmail = requireUserEmail(req.user?.email); - const updatedUser = await this.userService.editUser(validatedBody, req?.file ?? null, userId, userEmail); + return res.status(200).json({ + success: true, + msg: "User logged in successfully", + data: { + user, + token, + }, + }); + }); - res.status(200).json({ - success: true, - msg: "User updated successfully", - data: updatedUser, - }); - } catch (error) { - next(error); + editUser = catchAsync(async (req: Request, res: Response) => { + const validatedBody = editUserBodyValidation.parse(req.body); + const userId = requireUserId(req.user?.id); + const userEmail = requireUserEmail(req.user?.email); + const updatedUser = await this.userService.editUser(validatedBody, req?.file ?? null, userId, userEmail); + + res.status(200).json({ + success: true, + msg: "User updated successfully", + data: updatedUser, + }); + }); + + checkSuperadminExists = catchAsync(async (req: Request, res: Response) => { + const superAdminExists = await this.userService.checkSuperadminExists(); + return res.status(200).json({ + success: true, + msg: "Superadmin existence checked successfully", + data: superAdminExists, + }); + }); + + requestRecovery = catchAsync(async (req: Request, res: Response) => { + recoveryValidation.parse(req.body); + const email = req?.body?.email; + const msgId = await this.userService.requestRecovery(email); + return res.status(200).json({ + success: true, + msg: "Password recovery email sent successfully", + data: msgId, + }); + }); + + validateRecovery = catchAsync(async (req: Request, res: Response) => { + recoveryTokenBodyValidation.parse(req.body); + await this.userService.validateRecovery(req.body.recoveryToken); + return res.status(200).json({ + success: true, + msg: "Recovery token is valid", + }); + }); + + resetPassword = catchAsync(async (req: Request, res: Response) => { + newPasswordValidation.parse(req.body); + const { user, token } = await this.userService.resetPassword(req.body.password, req.body.recoveryToken); + return res.status(200).json({ + success: true, + msg: "Password has been reset successfully", + data: { user, token }, + }); + }); + + deleteUser = catchAsync(async (req: Request, res: Response) => { + const userId = requireUserId(req.user?.id); + const teamId = requireTeamId(req.user?.teamId); + const roles = requireUserRoles(req.user?.role); + + await this.userService.deleteUser({ + userId, + teamId, + roles, + }); + return res.status(200).json({ + success: true, + msg: "User deleted successfully", + }); + }); + + deleteUserById = catchAsync(async (req: Request, res: Response) => { + const validatedParams = getUserByIdParamValidation.parse(req.params); + const targetUserId = validatedParams.userId; + const actorId = requireUserId(req.user?.id); + const actorTeamId = requireTeamId(req.user?.teamId); + const actorRoles = requireUserRoles(req.user?.role); + await this.userService.deleteUserById({ actorId, actorTeamId, actorRoles, targetUserId }); + return res.status(200).json({ + success: true, + msg: "User removed successfully", + }); + }); + + getAllUsers = catchAsync(async (req: Request, res: Response) => { + const allUsers = await this.userService.getAllUsers(); + return res.status(200).json({ + success: true, + msg: "Users retrieved successfully", + data: allUsers, + }); + }); + + getUserById = catchAsync(async (req: Request, res: Response) => { + const validatedParams = getUserByIdParamValidation.parse(req.params); + const actorRoles = requireUserRoles(req.user?.role); + const user = await this.userService.getUserById(actorRoles, validatedParams.userId); + + return res.status(200).json({ success: true, msg: "ok", data: user }); + }); + + editUserById = catchAsync(async (req: Request, res: Response) => { + const actorRoles = requireUserRoles(req.user?.role); + const actorId = requireUserId(req.user?.id); + + if (!actorRoles.includes("superadmin")) { + throw new AppError({ message: "Unauthorized", status: 403 }); } - }; - checkSuperadminExists = async (req: Request, res: Response, next: NextFunction) => { - try { - const superAdminExists = await this.userService.checkSuperadminExists(); - return res.status(200).json({ - success: true, - msg: "Superadmin existence checked successfully", - data: superAdminExists, - }); - } catch (error) { - next(error); + const validatedParams = editUserByIdParamValidation.parse(req.params); + // If this is superadmin self edit, allow "superadmin" role + const validatedBody = + validatedParams.userId === actorId ? editSuperadminUserByIdBodyValidation.parse(req.body) : editUserByIdBodyValidation.parse(req.body); + + await this.userService.editUserById(validatedParams.userId, validatedBody); + return res.status(200).json({ success: true, msg: "ok" }); + }); + + editUserPasswordById = catchAsync(async (req: Request, res: Response) => { + const actorRoles = requireUserRoles(req.user?.role); + if (!actorRoles.includes("superadmin")) { + throw new AppError({ message: "Unauthorized", status: 403 }); } - }; - requestRecovery = async (req: Request, res: Response, next: NextFunction) => { - try { - recoveryValidation.parse(req.body); - const email = req?.body?.email; - const msgId = await this.userService.requestRecovery(email); - return res.status(200).json({ - success: true, - msg: "Password recovery email sent successfully", - data: msgId, - }); - } catch (error) { - next(error); - } - }; - - validateRecovery = async (req: Request, res: Response, next: NextFunction) => { - try { - recoveryTokenBodyValidation.parse(req.body); - await this.userService.validateRecovery(req.body.recoveryToken); - return res.status(200).json({ - success: true, - msg: "Recovery token is valid", - }); - } catch (error) { - next(error); - } - }; - - resetPassword = async (req: Request, res: Response, next: NextFunction) => { - try { - newPasswordValidation.parse(req.body); - const { user, token } = await this.userService.resetPassword(req.body.password, req.body.recoveryToken); - return res.status(200).json({ - success: true, - msg: "Password has been reset successfully", - data: { user, token }, - }); - } catch (error) { - next(error); - } - }; - - deleteUser = async (req: Request, res: Response, next: NextFunction) => { - try { - const userId = requireUserId(req.user?.id); - const teamId = requireTeamId(req.user?.teamId); - const roles = requireUserRoles(req.user?.role); - - await this.userService.deleteUser({ - userId, - teamId, - roles, - }); - return res.status(200).json({ - success: true, - msg: "User deleted successfully", - }); - } catch (error) { - next(error); - } - }; - - deleteUserById = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = getUserByIdParamValidation.parse(req.params); - const targetUserId = validatedParams.userId; - const actorId = requireUserId(req.user?.id); - const actorTeamId = requireTeamId(req.user?.teamId); - const actorRoles = requireUserRoles(req.user?.role); - await this.userService.deleteUserById({ actorId, actorTeamId, actorRoles, targetUserId }); - return res.status(200).json({ - success: true, - msg: "User removed successfully", - }); - } catch (error) { - next(error); - } - }; - - getAllUsers = async (req: Request, res: Response, next: NextFunction) => { - try { - const allUsers = await this.userService.getAllUsers(); - return res.status(200).json({ - success: true, - msg: "Users retrieved successfully", - data: allUsers, - }); - } catch (error) { - next(error); - } - }; - - getUserById = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = getUserByIdParamValidation.parse(req.params); - const actorRoles = requireUserRoles(req.user?.role); - const user = await this.userService.getUserById(actorRoles, validatedParams.userId); - - return res.status(200).json({ success: true, msg: "ok", data: user }); - } catch (error) { - next(error); - } - }; - - editUserById = async (req: Request, res: Response, next: NextFunction) => { - try { - const actorRoles = requireUserRoles(req.user?.role); - const actorId = requireUserId(req.user?.id); - - if (!actorRoles.includes("superadmin")) { - throw new AppError({ message: "Unauthorized", status: 403 }); - } - - const validatedParams = editUserByIdParamValidation.parse(req.params); - // If this is superadmin self edit, allow "superadmin" role - const validatedBody = - validatedParams.userId === actorId ? editSuperadminUserByIdBodyValidation.parse(req.body) : editUserByIdBodyValidation.parse(req.body); - - await this.userService.editUserById(validatedParams.userId, validatedBody); - return res.status(200).json({ success: true, msg: "ok" }); - } catch (error) { - next(error); - } - }; - - editUserPasswordById = async (req: Request, res: Response, next: NextFunction) => { - try { - const actorRoles = requireUserRoles(req.user?.role); - if (!actorRoles.includes("superadmin")) { - throw new AppError({ message: "Unauthorized", status: 403 }); - } - - const validatedParams = editUserByIdParamValidation.parse(req.params); - const validatedBody = editUserPasswordByIdBodyValidation.parse(req.body); - await this.userService.setPasswordByUserId(validatedParams.userId, validatedBody.password); - return res.status(200).json({ success: true, msg: "Password reset successfully" }); - } catch (error) { - next(error); - } - }; + const validatedParams = editUserByIdParamValidation.parse(req.params); + const validatedBody = editUserPasswordByIdBodyValidation.parse(req.body); + await this.userService.setPasswordByUserId(validatedParams.userId, validatedBody.password); + return res.status(200).json({ success: true, msg: "Password reset successfully" }); + }); } export default AuthController; diff --git a/server/src/api/controllers/checkController.ts b/server/src/api/controllers/checkController.ts index 23fbe2227..7b69c7047 100755 --- a/server/src/api/controllers/checkController.ts +++ b/server/src/api/controllers/checkController.ts @@ -1,4 +1,5 @@ -import { Request, Response, NextFunction } from "express"; +import { Request, Response, RequestHandler } from "express"; +import { catchAsync } from "@/utils/catchAsync.js"; import { getChecksParamValidation, @@ -10,132 +11,103 @@ import { import { ICheckService } from "@/domain/checks/check.service.js"; import { requireTeamId } from "@/api/controllers/controllerUtils.js"; -const SERVICE_NAME = "checkController"; - export interface ICheckController { - serviceName: string; - getChecksByMonitor: (req: Request, res: Response, next: NextFunction) => Promise; - getChecksByTeam: (req: Request, res: Response, next: NextFunction) => Promise; - getChecksSummaryByTeamId: (req: Request, res: Response, next: NextFunction) => Promise; - deleteChecks: (req: Request, res: Response, next: NextFunction) => Promise; - deleteChecksByTeamId: (req: Request, res: Response, next: NextFunction) => Promise; + getChecksByMonitor: RequestHandler; + getChecksByTeam: RequestHandler; + getChecksSummaryByTeamId: RequestHandler; + deleteChecks: RequestHandler; + deleteChecksByTeamId: RequestHandler; } class CheckController implements ICheckController { - static SERVICE_NAME = SERVICE_NAME; - private checkService: ICheckService; constructor(checkService: ICheckService) { this.checkService = checkService; } - get serviceName() { - return CheckController.SERVICE_NAME; - } + getChecksByMonitor = catchAsync(async (req: Request, res: Response) => { + const validatedParams = getChecksParamValidation.parse(req.params); + const validatedQuery = getChecksQueryValidation.parse(req.query); - getChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = getChecksParamValidation.parse(req.params); - const validatedQuery = getChecksQueryValidation.parse(req.query); + const teamId = requireTeamId(req.user?.teamId); - const teamId = requireTeamId(req.user?.teamId); + const result = await this.checkService.getChecksByMonitor({ + monitorId: validatedParams.monitorId, + teamId, + sortOrder: validatedQuery.sortOrder, + dateRange: validatedQuery.dateRange, + filter: validatedQuery.filter, + page: validatedQuery.page, + rowsPerPage: validatedQuery.rowsPerPage, + status: validatedQuery.status, + }); - const result = await this.checkService.getChecksByMonitor({ - monitorId: validatedParams.monitorId, - teamId, - sortOrder: validatedQuery.sortOrder, - dateRange: validatedQuery.dateRange, - filter: validatedQuery.filter, - page: validatedQuery.page, - rowsPerPage: validatedQuery.rowsPerPage, - status: validatedQuery.status, - }); + return res.status(200).json({ + success: true, + msg: "Checks retrieved successfully", + data: result, + }); + }); - return res.status(200).json({ - success: true, - msg: "Checks retrieved successfully", - data: result, - }); - } catch (error) { - next(error); - } - }; + getChecksByTeam = catchAsync(async (req: Request, res: Response) => { + const validatedQuery = getTeamChecksQueryValidation.parse(req.query); + const teamId = requireTeamId(req.user?.teamId); - getChecksByTeam = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedQuery = getTeamChecksQueryValidation.parse(req.query); - const teamId = requireTeamId(req.user?.teamId); + const checkData = await this.checkService.getChecksByTeam({ + teamId, + sortOrder: validatedQuery.sortOrder, + dateRange: validatedQuery.dateRange, + page: validatedQuery.page, + rowsPerPage: validatedQuery.rowsPerPage, + filter: validatedQuery.filter, + }); + return res.status(200).json({ + success: true, + msg: "Team checks retrieved successfully", + data: checkData, + }); + }); - const checkData = await this.checkService.getChecksByTeam({ - teamId, - sortOrder: validatedQuery.sortOrder, - dateRange: validatedQuery.dateRange, - page: validatedQuery.page, - rowsPerPage: validatedQuery.rowsPerPage, - filter: validatedQuery.filter, - }); - return res.status(200).json({ - success: true, - msg: "Team checks retrieved successfully", - data: checkData, - }); - } catch (error) { - next(error); - } - }; + getChecksSummaryByTeamId = catchAsync(async (req: Request, res: Response) => { + const validatedQuery = getChecksSummaryByTeamIdQueryValidation.parse(req.query); + const teamId = requireTeamId(req.user?.teamId); + const dateRange = validatedQuery.dateRange ?? "hour"; - getChecksSummaryByTeamId = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedQuery = getChecksSummaryByTeamIdQueryValidation.parse(req.query); - const teamId = requireTeamId(req.user?.teamId); - const dateRange = validatedQuery.dateRange ?? "hour"; + const summary = await this.checkService.getChecksSummaryByTeamId({ teamId, dateRange }); + return res.status(200).json({ + success: true, + msg: "Checks summary retrieved successfully", + data: summary, + }); + }); - const summary = await this.checkService.getChecksSummaryByTeamId({ teamId, dateRange }); - return res.status(200).json({ - success: true, - msg: "Checks summary retrieved successfully", - data: summary, - }); - } catch (error) { - next(error); - } - }; + deleteChecks = catchAsync(async (req: Request, res: Response) => { + const validatedParams = deleteChecksParamValidation.parse(req.params); + const teamId = requireTeamId(req.user?.teamId); - deleteChecks = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = deleteChecksParamValidation.parse(req.params); - const teamId = requireTeamId(req.user?.teamId); + const deletedCount = await this.checkService.deleteChecks({ + monitorId: validatedParams.monitorId, + teamId, + }); - const deletedCount = await this.checkService.deleteChecks({ - monitorId: validatedParams.monitorId, - teamId, - }); + return res.status(200).json({ + success: true, + msg: "Checks deleted successfully", + data: { deletedCount }, + }); + }); - return res.status(200).json({ - success: true, - msg: "Checks deleted successfully", - data: { deletedCount }, - }); - } catch (error) { - next(error); - } - }; + deleteChecksByTeamId = catchAsync(async (req: Request, res: Response) => { + const teamId = requireTeamId(req.user?.teamId); - deleteChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = requireTeamId(req.user?.teamId); + const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId }); - const deletedCount = await this.checkService.deleteChecksByTeamId({ teamId }); - - return res.status(200).json({ - success: true, - msg: "Checks deleted successfully", - data: { deletedCount }, - }); - } catch (error) { - next(error); - } - }; + return res.status(200).json({ + success: true, + msg: "Checks deleted successfully", + data: { deletedCount }, + }); + }); } export default CheckController; diff --git a/server/src/api/controllers/controllerUtils.ts b/server/src/api/controllers/controllerUtils.ts index 45bf3fa9f..1980ec0b4 100755 --- a/server/src/api/controllers/controllerUtils.ts +++ b/server/src/api/controllers/controllerUtils.ts @@ -1,5 +1,5 @@ import { AppError } from "@/utils/AppError.js"; -import { Monitor, MonitorTypes, type MonitorType } from "@/domain/monitors/monitor.types.js"; +import { Monitor, MonitorTypes, type MonitorType } from "@/domain/monitors/monitor.type.js"; import { UserRole } from "@/domain/users/user.type.js"; import sslChecker, { SSLDetails } from "ssl-checker"; type SSLCheckerType = typeof sslChecker; @@ -54,24 +54,6 @@ export const optionalNumber = (value: unknown, fieldName: string): number | unde throw new AppError({ message: `${fieldName} must be a number`, status: 400 }); }; -export const optionalBoolean = (value: unknown, fieldName: string): boolean | undefined => { - if (value === undefined) { - return undefined; - } - if (typeof value === "boolean") { - return value; - } - if (typeof value === "string") { - if (value === "true") { - return true; - } - if (value === "false") { - return false; - } - } - throw new AppError({ message: `${fieldName} must be a boolean`, status: 400 }); -}; - export const parseMonitorTypeFilter = (value: unknown): MonitorType | MonitorType[] | undefined => { const parseSingle = (input: unknown): MonitorType => { if (typeof input !== "string") { diff --git a/server/src/api/controllers/diagnosticController.ts b/server/src/api/controllers/diagnosticController.ts index efc383e1a..9d70304d4 100755 --- a/server/src/api/controllers/diagnosticController.ts +++ b/server/src/api/controllers/diagnosticController.ts @@ -1,37 +1,26 @@ import { IDiagnosticService } from "@/domain/diagnostics/diagnostic.service.js"; -import { Request, Response, NextFunction } from "express"; - -const SERVICE_NAME = "diagnosticController"; +import { Request, Response, RequestHandler } from "express"; +import { catchAsync } from "@/utils/catchAsync.js"; export interface IDiagnosticController { - getSystemStats: (req: Request, res: Response, next: NextFunction) => Promise; + getSystemStats: RequestHandler; } class DiagnosticController implements IDiagnosticController { - static SERVICE_NAME = SERVICE_NAME; - private diagnosticService: IDiagnosticService; constructor(diagnosticService: IDiagnosticService) { this.diagnosticService = diagnosticService; } - get serviceName() { - return DiagnosticController.SERVICE_NAME; - } - - getSystemStats = async (req: Request, res: Response, next: NextFunction) => { - try { - const diagnostics = await this.diagnosticService.getSystemStats(); - return res.status(200).json({ - success: true, - msg: "OK", - data: diagnostics, - }); - } catch (error) { - next(error); - } - }; + getSystemStats = catchAsync(async (req: Request, res: Response) => { + const diagnostics = await this.diagnosticService.getSystemStats(); + return res.status(200).json({ + success: true, + msg: "OK", + data: diagnostics, + }); + }); } export default DiagnosticController; diff --git a/server/src/api/controllers/geoCheckController.ts b/server/src/api/controllers/geoCheckController.ts index a50176188..24caef382 100644 --- a/server/src/api/controllers/geoCheckController.ts +++ b/server/src/api/controllers/geoCheckController.ts @@ -1,51 +1,40 @@ -import { Request, Response, NextFunction } from "express"; +import { Request, Response, RequestHandler } from "express"; +import { catchAsync } from "@/utils/catchAsync.js"; import { getChecksParamValidation, getChecksQueryValidation } from "@/api/validation/checkValidation.js"; import type { IGeoChecksService } from "@/domain/geo-checks/geo-check.service.js"; import { requireTeamId } from "./controllerUtils.js"; -const SERVICE_NAME = "geoCheckController"; - export interface IGeoCheckController { - getGeoChecksByMonitor: (req: Request, res: Response, next: NextFunction) => Promise; + getGeoChecksByMonitor: RequestHandler; } class GeoCheckController implements IGeoCheckController { - static SERVICE_NAME = SERVICE_NAME; - private geoChecksService: IGeoChecksService; constructor(geoChecksService: IGeoChecksService) { this.geoChecksService = geoChecksService; } - get serviceName() { - return GeoCheckController.SERVICE_NAME; - } + getGeoChecksByMonitor = catchAsync(async (req: Request, res: Response) => { + const validatedParams = getChecksParamValidation.parse(req.params); + const validatedQuery = getChecksQueryValidation.parse(req.query); - getGeoChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = getChecksParamValidation.parse(req.params); - const validatedQuery = getChecksQueryValidation.parse(req.query); + const teamId = requireTeamId(req.user?.teamId); - const teamId = requireTeamId(req.user?.teamId); + const result = await this.geoChecksService.getGeoChecksByMonitor({ + monitorId: validatedParams.monitorId, + teamId: teamId, + sortOrder: validatedQuery.sortOrder, + dateRange: validatedQuery.dateRange, + page: validatedQuery.page, + rowsPerPage: validatedQuery.rowsPerPage, + continent: validatedQuery.continent, + }); - const result = await this.geoChecksService.getGeoChecksByMonitor({ - monitorId: validatedParams.monitorId, - teamId: teamId, - sortOrder: validatedQuery.sortOrder, - dateRange: validatedQuery.dateRange, - page: validatedQuery.page, - rowsPerPage: validatedQuery.rowsPerPage, - continent: validatedQuery.continent, - }); - - return res.status(200).json({ - success: true, - msg: "Geo checks retrieved successfully", - data: result, - }); - } catch (error) { - next(error); - } - }; + return res.status(200).json({ + success: true, + msg: "Geo checks retrieved successfully", + data: result, + }); + }); } export default GeoCheckController; diff --git a/server/src/api/controllers/incidentController.ts b/server/src/api/controllers/incidentController.ts index 6358d2a89..f1998f16f 100644 --- a/server/src/api/controllers/incidentController.ts +++ b/server/src/api/controllers/incidentController.ts @@ -1,5 +1,6 @@ import { AppError } from "@/utils/AppError.js"; -import { Request, Response, NextFunction } from "express"; +import { Request, Response, RequestHandler } from "express"; +import { catchAsync } from "@/utils/catchAsync.js"; import { requireTeamId, requireUserId, requireUserEmail, extractString } from "./controllerUtils.js"; import { IIncidentService } from "@/domain/incidents/incident.service.js"; import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } from "@/api/validation/incidentValidation.js"; @@ -7,10 +8,10 @@ import { getIncidentsByTeamQueryValidation, getIncidentSummaryQueryValidation } const SERVICE_NAME = "IncidentController"; export interface IIncidentController { - getIncidentsByTeam: (req: Request, res: Response, next: NextFunction) => Promise; - getIncidentSummary: (req: Request, res: Response, next: NextFunction) => Promise; - getIncidentById: (req: Request, res: Response, next: NextFunction) => Promise; - resolveIncidentManually: (req: Request, res: Response, next: NextFunction) => Promise; + getIncidentsByTeam: RequestHandler; + getIncidentSummary: RequestHandler; + getIncidentById: RequestHandler; + resolveIncidentManually: RequestHandler; } class IncidentController implements IIncidentController { private incidentService: IIncidentService; @@ -18,90 +19,74 @@ class IncidentController implements IIncidentController { this.incidentService = incidentService; } - getIncidentsByTeam = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedQuery = getIncidentsByTeamQueryValidation.parse(req.query); + getIncidentsByTeam = catchAsync(async (req: Request, res: Response) => { + const validatedQuery = getIncidentsByTeamQueryValidation.parse(req.query); - const teamId = requireTeamId(req.user?.teamId); - const result = await this.incidentService.getIncidentsByTeam( - teamId, - validatedQuery.sortOrder, - validatedQuery.dateRange, - validatedQuery.page, - validatedQuery.rowsPerPage, - validatedQuery.status, - validatedQuery.monitorId, - validatedQuery.resolutionType - ); + const teamId = requireTeamId(req.user?.teamId); + const result = await this.incidentService.getIncidentsByTeam( + teamId, + validatedQuery.sortOrder, + validatedQuery.dateRange, + validatedQuery.page, + validatedQuery.rowsPerPage, + validatedQuery.status, + validatedQuery.monitorId, + validatedQuery.resolutionType + ); - return res.status(200).json({ - success: true, - msg: "Incidents retrieved successfully", - data: result, - }); - } catch (error) { - next(error); + return res.status(200).json({ + success: true, + msg: "Incidents retrieved successfully", + data: result, + }); + }); + + getIncidentSummary = catchAsync(async (req: Request, res: Response) => { + const teamId = requireTeamId(req.user?.teamId); + const validatedQuery = getIncidentSummaryQueryValidation.parse(req.query); + + const summary = await this.incidentService.getIncidentSummary(teamId, validatedQuery.limit); + return res.status(200).json({ + success: true, + msg: "Incident summary retrieved successfully", + data: summary, + }); + }); + + getIncidentById = catchAsync(async (req: Request, res: Response) => { + const teamId = requireTeamId(req.user?.teamId); + const incidentId = req.params.incidentId as string; + if (!incidentId) { + throw new AppError({ message: "Incident ID is required", service: SERVICE_NAME, status: 400 }); } - }; - getIncidentSummary = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = requireTeamId(req.user?.teamId); - const validatedQuery = getIncidentSummaryQueryValidation.parse(req.query); + const incident = await this.incidentService.getIncidentById(incidentId, teamId); - const summary = await this.incidentService.getIncidentSummary(teamId, validatedQuery.limit); - return res.status(200).json({ - success: true, - msg: "Incident summary retrieved successfully", - data: summary, - }); - } catch (error) { - next(error); + return res.status(200).json({ + success: true, + msg: "Incident retrieved successfully", + data: incident, + }); + }); + + resolveIncidentManually = catchAsync(async (req: Request, res: Response) => { + const teamId = requireTeamId(req.user?.teamId); + const userId = requireUserId(req.user?.id); + const userEmail = requireUserEmail(req.user?.email); + const incidentId = extractString(req.params?.incidentId); + if (!incidentId) { + throw new AppError({ message: "Incident ID is required", service: SERVICE_NAME, status: 400 }); } - }; - getIncidentById = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = requireTeamId(req.user?.teamId); - const incidentId = req.params.incidentId as string; - if (!incidentId) { - throw new AppError({ message: "Incident ID is required", service: SERVICE_NAME, status: 400 }); - } + const comment = extractString(req.body?.comment); + const resolvedIncident = await this.incidentService.resolveIncident(incidentId, userId, teamId, comment, userEmail); - const incident = await this.incidentService.getIncidentById(incidentId, teamId); - - return res.status(200).json({ - success: true, - msg: "Incident retrieved successfully", - data: incident, - }); - } catch (error) { - next(error); - } - }; - - resolveIncidentManually = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = requireTeamId(req.user?.teamId); - const userId = requireUserId(req.user?.id); - const userEmail = requireUserEmail(req.user?.email); - const incidentId = extractString(req.params?.incidentId); - if (!incidentId) { - throw new AppError({ message: "Incident ID is required", service: SERVICE_NAME, status: 400 }); - } - - const comment = extractString(req.body?.comment); - const resolvedIncident = await this.incidentService.resolveIncident(incidentId, userId, teamId, comment, userEmail); - - return res.status(200).json({ - success: true, - msg: "Incident resolved successfully", - data: resolvedIncident, - }); - } catch (error) { - next(error); - } - }; + return res.status(200).json({ + success: true, + msg: "Incident resolved successfully", + data: resolvedIncident, + }); + }); } export default IncidentController; diff --git a/server/src/api/controllers/inviteController.ts b/server/src/api/controllers/inviteController.ts index c77385f7b..511145b7b 100755 --- a/server/src/api/controllers/inviteController.ts +++ b/server/src/api/controllers/inviteController.ts @@ -1,82 +1,65 @@ -import { Request, Response, NextFunction } from "express"; +import { Request, Response, RequestHandler } from "express"; +import { catchAsync } from "@/utils/catchAsync.js"; import { inviteBodyValidation, inviteVerificationBodyValidation } from "@/api/validation/authValidation.js"; import { requireFirstName, requireTeamId, requireUserRoles } from "@/api/controllers/controllerUtils.js"; import { IInviteService } from "@/domain/invites/invite.service.js"; -const SERVICE_NAME = "inviteController"; export interface IInviteController { - getInviteToken: (req: Request, res: Response, next: NextFunction) => Promise; - sendInviteEmail: (req: Request, res: Response, next: NextFunction) => Promise; - verifyInviteToken: (req: Request, res: Response, next: NextFunction) => Promise; + getInviteToken: RequestHandler; + sendInviteEmail: RequestHandler; + verifyInviteToken: RequestHandler; } class InviteController implements IInviteController { - static SERVICE_NAME = SERVICE_NAME; private inviteService: IInviteService; constructor(inviteService: IInviteService) { this.inviteService = inviteService; } - get serviceName() { - return InviteController.SERVICE_NAME; - } + getInviteToken = catchAsync(async (req: Request, res: Response) => { + const teamId = requireTeamId(req.user?.teamId); + const userRoles = requireUserRoles(req.user?.role); + const invite = req.body; + invite.teamId = teamId; + inviteBodyValidation.parse(invite); + const inviteToken = await this.inviteService.getInviteToken({ invite, teamId, userRoles }); + return res.status(200).json({ + success: true, + msg: "Invite token generated successfully", + data: inviteToken, + }); + }); - getInviteToken = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = requireTeamId(req.user?.teamId); - const userRoles = requireUserRoles(req.user?.role); - const invite = req.body; - invite.teamId = teamId; - inviteBodyValidation.parse(invite); - const inviteToken = await this.inviteService.getInviteToken({ invite, teamId, userRoles }); - return res.status(200).json({ - success: true, - msg: "Invite token generated successfully", - data: inviteToken, - }); - } catch (error) { - next(error); - } - }; + sendInviteEmail = catchAsync(async (req: Request, res: Response) => { + const teamId = requireTeamId(req.user?.teamId); + const userRoles = requireUserRoles(req.user?.role); + const firstName = requireFirstName(req.user?.firstName); - sendInviteEmail = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = requireTeamId(req.user?.teamId); - const userRoles = requireUserRoles(req.user?.role); - const firstName = requireFirstName(req.user?.firstName); + const inviteRequest = req.body; + inviteRequest.teamId = teamId; + inviteBodyValidation.parse(inviteRequest); - const inviteRequest = req.body; - inviteRequest.teamId = teamId; - inviteBodyValidation.parse(inviteRequest); + const inviteToken = await this.inviteService.sendInviteEmail({ + invite: inviteRequest, + firstName, + userRoles, + }); + return res.status(200).json({ + success: true, + msg: "Invite issued successfully", + data: inviteToken, + }); + }); - const inviteToken = await this.inviteService.sendInviteEmail({ - invite: inviteRequest, - firstName, - userRoles, - }); - return res.status(200).json({ - success: true, - msg: "Invite issued successfully", - data: inviteToken, - }); - } catch (error) { - next(error); - } - }; - - verifyInviteToken = async (req: Request, res: Response, next: NextFunction) => { - try { - inviteVerificationBodyValidation.parse(req.body); - const invite = await this.inviteService.verifyInviteToken({ inviteToken: req?.body?.token }); - return res.status(200).json({ - success: true, - msg: "Invite verified successfully", - data: invite, - }); - } catch (error) { - next(error); - } - }; + verifyInviteToken = catchAsync(async (req: Request, res: Response) => { + inviteVerificationBodyValidation.parse(req.body); + const invite = await this.inviteService.verifyInviteToken({ inviteToken: req?.body?.token }); + return res.status(200).json({ + success: true, + msg: "Invite verified successfully", + data: invite, + }); + }); } export default InviteController; diff --git a/server/src/api/controllers/logController.ts b/server/src/api/controllers/logController.ts index 7d221c6c5..316d03788 100644 --- a/server/src/api/controllers/logController.ts +++ b/server/src/api/controllers/logController.ts @@ -1,33 +1,24 @@ import { ILogger } from "@/utils/logger.js"; -import { Request, Response, NextFunction } from "express"; - -const SERVICE_NAME = "LogController"; +import { Request, Response, RequestHandler } from "express"; +import { catchAsync } from "@/utils/catchAsync.js"; export interface ILogController { - getLogs: (req: Request, res: Response, next: NextFunction) => Promise; + getLogs: RequestHandler; } class LogController { - static SERVICE_NAME = SERVICE_NAME; private logger: ILogger; constructor(logger: ILogger) { this.logger = logger; } - get serviceName() { - return LogController.SERVICE_NAME; - } - getLogs = async (req: Request, res: Response, next: NextFunction) => { - try { - const logs = this.logger.getLogs(); - res.status(200).json({ - success: true, - msg: "Logs fetched successfully", - data: logs, - }); - } catch (error) { - next(error); - } - }; + getLogs = catchAsync(async (req: Request, res: Response) => { + const logs = this.logger.getLogs(); + res.status(200).json({ + success: true, + msg: "Logs fetched successfully", + data: logs, + }); + }); } export default LogController; diff --git a/server/src/api/controllers/maintenanceWindowController.ts b/server/src/api/controllers/maintenanceWindowController.ts index ddbf4deca..49fa605ec 100644 --- a/server/src/api/controllers/maintenanceWindowController.ts +++ b/server/src/api/controllers/maintenanceWindowController.ts @@ -1,4 +1,5 @@ -import { Request, Response, NextFunction } from "express"; +import { Request, Response, RequestHandler } from "express"; +import { catchAsync } from "@/utils/catchAsync.js"; import { createMaintenanceWindowBodyValidation, editMaintenanceWindowByIdParamValidation, @@ -12,12 +13,12 @@ import { requireTeamId } from "@/api/controllers/controllerUtils.js"; import { IMaintenanceWindowService } from "@/domain/maintenance-windows/maintenance-window.service.js"; export interface IMaintenanceWindowController { - createMaintenanceWindows: (req: Request, res: Response, next: NextFunction) => Promise; - getMaintenanceWindowById: (req: Request, res: Response, next: NextFunction) => Promise; - getMaintenanceWindowsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise; - getMaintenanceWindowsByMonitorId: (req: Request, res: Response, next: NextFunction) => Promise; - deleteMaintenanceWindow: (req: Request, res: Response, next: NextFunction) => Promise; - editMaintenanceWindow: (req: Request, res: Response, next: NextFunction) => Promise; + createMaintenanceWindows: RequestHandler; + getMaintenanceWindowById: RequestHandler; + getMaintenanceWindowsByTeamId: RequestHandler; + getMaintenanceWindowsByMonitorId: RequestHandler; + deleteMaintenanceWindow: RequestHandler; + editMaintenanceWindow: RequestHandler; } class MaintenanceWindowController implements IMaintenanceWindowController { private maintenanceWindowService: IMaintenanceWindowService; @@ -25,131 +26,107 @@ class MaintenanceWindowController implements IMaintenanceWindowController { this.maintenanceWindowService = maintenanceWindowService; } - createMaintenanceWindows = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedBody = createMaintenanceWindowBodyValidation.parse(req.body); - const teamId = requireTeamId(req?.user?.teamId); + createMaintenanceWindows = catchAsync(async (req: Request, res: Response) => { + const validatedBody = createMaintenanceWindowBodyValidation.parse(req.body); + const teamId = requireTeamId(req?.user?.teamId); - const monitorIDs = validatedBody.monitors; - const name = validatedBody.name; - const active = validatedBody.active ?? true; - const duration = validatedBody.duration; - const durationUnit = validatedBody.durationUnit; - const repeat = validatedBody.repeat; - const start = validatedBody.start; - const end = validatedBody.end; + const monitorIDs = validatedBody.monitors; + const name = validatedBody.name; + const active = validatedBody.active ?? true; + const duration = validatedBody.duration; + const durationUnit = validatedBody.durationUnit; + const repeat = validatedBody.repeat; + const start = validatedBody.start; + const end = validatedBody.end; - await this.maintenanceWindowService.createMaintenanceWindow({ teamId, monitorIDs, name, active, duration, durationUnit, repeat, start, end }); + await this.maintenanceWindowService.createMaintenanceWindow({ teamId, monitorIDs, name, active, duration, durationUnit, repeat, start, end }); - return res.status(200).json({ - success: true, - msg: "Maintenance window created successfully", - }); - } catch (error) { - next(error); - } - }; - getMaintenanceWindowById = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = getMaintenanceWindowByIdParamValidation.parse(req.params); + return res.status(200).json({ + success: true, + msg: "Maintenance window created successfully", + }); + }); + getMaintenanceWindowById = catchAsync(async (req: Request, res: Response) => { + const validatedParams = getMaintenanceWindowByIdParamValidation.parse(req.params); - const teamId = requireTeamId(req.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); - const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: validatedParams.id, teamId }); + const maintenanceWindow = await this.maintenanceWindowService.getMaintenanceWindowById({ id: validatedParams.id, teamId }); - return res.status(200).json({ - success: true, - msg: "Maintenance window fetched successfully", - data: maintenanceWindow, - }); - } catch (error) { - next(error); - } - }; + return res.status(200).json({ + success: true, + msg: "Maintenance window fetched successfully", + data: maintenanceWindow, + }); + }); - getMaintenanceWindowsByTeamId = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedQuery = getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query); + getMaintenanceWindowsByTeamId = catchAsync(async (req: Request, res: Response) => { + const validatedQuery = getMaintenanceWindowsByTeamIdQueryValidation.parse(req.query); - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req?.user?.teamId); - const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ - teamId, - active: validatedQuery.active, - page: validatedQuery.page, - rowsPerPage: validatedQuery.rowsPerPage, - field: validatedQuery.field, - order: validatedQuery.order, - }); + const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByTeamId({ + teamId, + active: validatedQuery.active, + page: validatedQuery.page, + rowsPerPage: validatedQuery.rowsPerPage, + field: validatedQuery.field, + order: validatedQuery.order, + }); - return res.status(200).json({ - success: true, - msg: "Maintenance windows fetched successfully", - data: maintenanceWindows, - }); - } catch (error) { - next(error); - } - }; + return res.status(200).json({ + success: true, + msg: "Maintenance windows fetched successfully", + data: maintenanceWindows, + }); + }); - getMaintenanceWindowsByMonitorId = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = getMaintenanceWindowsByMonitorIdParamValidation.parse(req.params); + getMaintenanceWindowsByMonitorId = catchAsync(async (req: Request, res: Response) => { + const validatedParams = getMaintenanceWindowsByMonitorIdParamValidation.parse(req.params); - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req?.user?.teamId); - const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ - monitorId: validatedParams.monitorId, - teamId, - }); + const maintenanceWindows = await this.maintenanceWindowService.getMaintenanceWindowsByMonitorId({ + monitorId: validatedParams.monitorId, + teamId, + }); - return res.status(200).json({ - success: true, - msg: "Maintenance windows fetched successfully", - data: maintenanceWindows, - }); - } catch (error) { - next(error); - } - }; - deleteMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = deleteMaintenanceWindowByIdParamValidation.parse(req.params); + return res.status(200).json({ + success: true, + msg: "Maintenance windows fetched successfully", + data: maintenanceWindows, + }); + }); + deleteMaintenanceWindow = catchAsync(async (req: Request, res: Response) => { + const validatedParams = deleteMaintenanceWindowByIdParamValidation.parse(req.params); - const teamId = requireTeamId(req?.user?.teamId); + const teamId = requireTeamId(req?.user?.teamId); - await this.maintenanceWindowService.deleteMaintenanceWindow({ id: validatedParams.id, teamId }); + await this.maintenanceWindowService.deleteMaintenanceWindow({ id: validatedParams.id, teamId }); - return res.status(200).json({ - success: true, - msg: "Maintenance window deleted successfully", - }); - } catch (error) { - next(error); - } - }; + return res.status(200).json({ + success: true, + msg: "Maintenance window deleted successfully", + }); + }); - editMaintenanceWindow = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = editMaintenanceWindowByIdParamValidation.parse(req.params); - const validatedBody = editMaintenanceByIdWindowBodyValidation.parse(req.body); + editMaintenanceWindow = catchAsync(async (req: Request, res: Response) => { + const validatedParams = editMaintenanceWindowByIdParamValidation.parse(req.params); + const validatedBody = editMaintenanceByIdWindowBodyValidation.parse(req.body); - const teamId = requireTeamId(req.user?.teamId); + const teamId = requireTeamId(req.user?.teamId); - const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ - id: validatedParams.id, - body: validatedBody, - teamId, - }); - return res.status(200).json({ - success: true, - msg: "Maintenance window edited successfully", - data: editedMaintenanceWindow, - }); - } catch (error) { - next(error); - } - }; + const editedMaintenanceWindow = await this.maintenanceWindowService.editMaintenanceWindow({ + id: validatedParams.id, + body: validatedBody, + teamId, + }); + return res.status(200).json({ + success: true, + msg: "Maintenance window edited successfully", + data: editedMaintenanceWindow, + }); + }); } export default MaintenanceWindowController; diff --git a/server/src/api/controllers/monitorController.ts b/server/src/api/controllers/monitorController.ts index a0cb073d2..664a78f45 100644 --- a/server/src/api/controllers/monitorController.ts +++ b/server/src/api/controllers/monitorController.ts @@ -1,4 +1,5 @@ -import { Request, Response, NextFunction } from "express"; +import { Request, Response, RequestHandler } from "express"; +import { catchAsync } from "@/utils/catchAsync.js"; import { updateNotificationsValidation } from "@/api/validation/notificationValidation.js"; import { getMonitorByIdParamValidation, @@ -22,33 +23,28 @@ import { AppError } from "@/utils/AppError.js"; import { IMonitorService } from "@/domain/monitors/monitor.service.js"; import { INotificationsService } from "@/domain/notifications/notification.service.js"; -const SERVICE_NAME = "monitorController"; - export interface IMonitorController { - getMonitorCertificate: (req: Request, res: Response, next: NextFunction) => Promise; - getUptimeDetailsById: (req: Request, res: Response, next: NextFunction) => Promise; - getHardwareDetailsById: (req: Request, res: Response, next: NextFunction) => Promise; - getPageSpeedDetailsById: (req: Request, res: Response, next: NextFunction) => Promise; - getGeoChecksByMonitorId: (req: Request, res: Response, next: NextFunction) => Promise; - getMonitorById: (req: Request, res: Response, next: NextFunction) => Promise; - createMonitor: (req: Request, res: Response, next: NextFunction) => Promise; - importMonitorsFromJSON: (req: Request, res: Response, next: NextFunction) => Promise; - deleteMonitor: (req: Request, res: Response, next: NextFunction) => Promise; - deleteAllMonitors: (req: Request, res: Response, next: NextFunction) => Promise; - editMonitor: (req: Request, res: Response, next: NextFunction) => Promise; - pauseMonitor: (req: Request, res: Response, next: NextFunction) => Promise; - bulkPauseMonitors: (req: Request, res: Response, next: NextFunction) => Promise; - addDemoMonitors: (req: Request, res: Response, next: NextFunction) => Promise; - getMonitorsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise; - getMonitorsWithChecksByTeamId: (req: Request, res: Response, next: NextFunction) => Promise; - exportMonitorsToJSON: (req: Request, res: Response, next: NextFunction) => Promise; - getAllGames: (req: Request, res: Response, next: NextFunction) => Promise; - getGroupsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise; - updateNotifications: (req: Request, res: Response, next: NextFunction) => Promise; + getMonitorCertificate: RequestHandler; + getUptimeDetailsById: RequestHandler; + getHardwareDetailsById: RequestHandler; + getPageSpeedDetailsById: RequestHandler; + getGeoChecksByMonitorId: RequestHandler; + getMonitorById: RequestHandler; + createMonitor: RequestHandler; + importMonitorsFromJSON: RequestHandler; + deleteMonitor: RequestHandler; + deleteAllMonitors: RequestHandler; + editMonitor: RequestHandler; + pauseMonitor: RequestHandler; + bulkPauseMonitors: RequestHandler; + addDemoMonitors: RequestHandler; + getMonitorsByTeamId: RequestHandler; + getMonitorsWithChecksByTeamId: RequestHandler; + exportMonitorsToJSON: RequestHandler; + getAllGames: RequestHandler; + updateNotifications: RequestHandler; } class MonitorController implements IMonitorController { - static SERVICE_NAME = SERVICE_NAME; - private monitorService: IMonitorService; private notificationsService: INotificationsService; @@ -57,434 +53,340 @@ class MonitorController implements IMonitorController { this.notificationsService = notificationsService; } - get serviceName() { - return MonitorController.SERVICE_NAME; - } + getMonitorCertificate = catchAsync(async (req: Request, res: Response) => { + const validatedParams = getCertificateParamValidation.parse(req.params); + const teamId = requireTeamId(req.user?.teamId); + const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: validatedParams.monitorId }); + const certificate = await fetchMonitorCertificate(sslChecker, monitor); - getMonitorCertificate = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = getCertificateParamValidation.parse(req.params); - const teamId = requireTeamId(req.user?.teamId); - const monitor = await this.monitorService.getMonitorById({ teamId, monitorId: validatedParams.monitorId }); - const certificate = await fetchMonitorCertificate(sslChecker, monitor); + return res.status(200).json({ + success: true, + msg: "SSL certificate retrieved successfully", + data: { + certificateDate: new Date(certificate.validTo), + }, + }); + }); - return res.status(200).json({ - success: true, - msg: "SSL certificate retrieved successfully", - data: { - certificateDate: new Date(certificate.validTo), - }, - }); - } catch (error) { - next(error); + getUptimeDetailsById = catchAsync(async (req: Request, res: Response) => { + const validatedParams = getUptimeDetailsByIdParamValidation.parse(req.params); + const validatedQuery = getUptimeDetailsByIdQueryValidation.parse(req.query); + + const monitorId = validatedParams.monitorId; + const dateRange = validatedQuery.dateRange; + + const teamId = requireTeamId(req.user?.teamId); + + const data = await this.monitorService.getUptimeDetailsById({ + teamId, + monitorId, + dateRange, + }); + return res.status(200).json({ + success: true, + msg: "Uptime details retrieved successfully", + data: data, + }); + }); + + getHardwareDetailsById = catchAsync(async (req: Request, res: Response) => { + const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params); + const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query); + + const monitorId = validatedParams.monitorId; + const dateRange = validatedQuery.dateRange || "recent"; + const teamId = requireTeamId(req.user?.teamId); + + const data = await this.monitorService.getHardwareDetailsById({ + teamId, + monitorId, + dateRange, + }); + + return res.status(200).json({ + success: true, + msg: "Hardware details retrieved successfully", + data: data, + }); + }); + getPageSpeedDetailsById = catchAsync(async (req: Request, res: Response) => { + const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params); + const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query); + + const monitorId = validatedParams.monitorId; + const dateRange = validatedQuery.dateRange || "recent"; + const teamId = requireTeamId(req.user?.teamId); + + const data = await this.monitorService.getPageSpeedDetailsById({ + teamId, + monitorId, + dateRange, + }); + + return res.status(200).json({ + success: true, + msg: "Page speed details retrieved successfully", + data, + }); + }); + + getGeoChecksByMonitorId = catchAsync(async (req: Request, res: Response) => { + const validatedParams = getMonitorByIdParamValidation.parse(req.params); + const validatedQuery = getMonitorByIdQueryValidation.parse(req.query); + + const monitorId = validatedParams.monitorId; + const dateRange = validatedQuery.dateRange || "recent"; + const continentParam = validatedQuery.continent; + const continents = continentParam ? (Array.isArray(continentParam) ? continentParam : [continentParam]) : undefined; + const teamId = requireTeamId(req.user?.teamId); + + const data = await this.monitorService.getGeoChecksByMonitorId({ + teamId, + monitorId, + dateRange, + continents, + }); + + return res.status(200).json({ + success: true, + msg: "Geo checks retrieved successfully", + data, + }); + }); + + getMonitorById = catchAsync(async (req: Request, res: Response) => { + const validatedParams = getMonitorByIdParamValidation.parse(req.params); + + const teamId = requireTeamId(req.user?.teamId); + const monitorId = validatedParams.monitorId; + + const monitor = await this.monitorService.getMonitorById({ teamId, monitorId }); + + return res.status(200).json({ + success: true, + msg: "Monitor retrieved successfully", + data: monitor, + }); + }); + + createMonitor = catchAsync(async (req: Request, res: Response) => { + const validatedBody = createMonitorBodyValidation.parse(req.body); + + const userId = requireUserId(req.user?.id); + const teamId = requireTeamId(req.user?.teamId); + + const monitor = await this.monitorService.createMonitor(teamId, userId, validatedBody); + + return res.status(200).json({ + success: true, + msg: "Monitor created successfully", + data: monitor, + }); + }); + + importMonitorsFromJSON = catchAsync(async (req: Request, res: Response) => { + const teamId = requireTeamId(req.user?.teamId); + const userId = requireUserId(req.user?.id); + const validatedBody = importMonitorsBodyValidation.parse(req.body); + const monitors = validatedBody.monitors; + + const result = await this.monitorService.importMonitorsFromJSON({ teamId, userId, monitors }); + + return res.status(200).json({ + success: true, + msg: `Successfully imported ${result.imported} monitor(s)`, + data: result, + }); + }); + + deleteMonitor = catchAsync(async (req: Request, res: Response) => { + const validatedParams = getMonitorByIdParamValidation.parse(req.params); + const teamId = requireTeamId(req.user?.teamId); + const monitorId = validatedParams.monitorId; + + const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId }); + + return res.status(200).json({ + success: true, + msg: "Monitor deleted successfully", + data: deletedMonitor, + }); + }); + + deleteAllMonitors = catchAsync(async (req: Request, res: Response) => { + const teamId = requireTeamId(req.user?.teamId); + const deletedCount = await this.monitorService.deleteAllMonitors({ teamId }); + + return res.status(200).json({ + success: true, + msg: `Deleted ${deletedCount} monitors`, + }); + }); + + editMonitor = catchAsync(async (req: Request, res: Response) => { + const validatedParams = getMonitorByIdParamValidation.parse(req.params); + const validatedBody = editMonitorBodyValidation.parse(req.body); + const monitorId = validatedParams.monitorId; + const teamId = requireTeamId(req.user?.teamId); + + const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: validatedBody }); + + return res.status(200).json({ + success: true, + msg: "Monitor edited successfully", + data: editedMonitor, + }); + }); + + pauseMonitor = catchAsync(async (req: Request, res: Response) => { + const validatedParams = pauseMonitorParamValidation.parse(req.params); + + const monitorId = validatedParams.monitorId; + const teamId = requireTeamId(req.user?.teamId); + + const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId }); + + return res.status(200).json({ + success: true, + msg: monitor.isActive ? "Monitor resumed successfully" : "Monitor paused successfully", + data: monitor, + }); + }); + + bulkPauseMonitors = catchAsync(async (req: Request, res: Response) => { + const validatedBody = bulkPauseMonitorBodyValidation.parse(req.body); + + const { monitorIds, pause } = validatedBody; + const teamId = requireTeamId(req.user?.teamId); + + const { monitors, failedCount } = await this.monitorService.bulkPauseMonitors({ teamId, monitorIds, pause }); + + const action = pause ? "paused" : "resumed"; + const monitorStr = monitors.length === 1 ? "monitor" : "monitors"; + + let msg = `${monitors.length} ${monitorStr} ${action} successfully`; + if (failedCount > 0) { + msg = `${monitors.length} ${monitorStr} ${action} in database, but ${failedCount} failed to sync with the job queue. Please check logs.`; } - }; - getUptimeDetailsById = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = getUptimeDetailsByIdParamValidation.parse(req.params); - const validatedQuery = getUptimeDetailsByIdQueryValidation.parse(req.query); + return res.status(200).json({ + success: true, + msg, + data: { monitors, failedCount }, + }); + }); - const monitorId = validatedParams.monitorId; - const dateRange = validatedQuery.dateRange; + addDemoMonitors = catchAsync(async (req: Request, res: Response) => { + const id = requireUserId(req.user?.id); + const teamId = requireTeamId(req.user?.teamId); + const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId }); - const teamId = requireTeamId(req.user?.teamId); + return res.status(200).json({ + success: true, + msg: "Demo monitors added successfully", + data: demoMonitors?.length ?? 0, + }); + }); - const data = await this.monitorService.getUptimeDetailsById({ - teamId, - monitorId, - dateRange, + getMonitorsByTeamId = catchAsync(async (req: Request, res: Response) => { + const validatedQuery = getMonitorsByTeamIdQueryValidation.parse(req.query); + + const teamId = requireTeamId(req.user?.teamId); + const type = validatedQuery.type; + const tags = validatedQuery.tags; + const filter = validatedQuery.filter; + + const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, tags, filter }); + + return res.status(200).json({ + success: true, + msg: "Monitors retrieved successfully", + data: monitors, + }); + }); + + getMonitorsWithChecksByTeamId = catchAsync(async (req: Request, res: Response) => { + const validatedQuery = getMonitorsWithChecksQueryValidation.parse(req.query); + + const limit = validatedQuery.limit; + const page = validatedQuery.page; + const rowsPerPage = validatedQuery.rowsPerPage; + const filter = validatedQuery.filter; + const field = validatedQuery.field; + const order = validatedQuery.order; + const type = validatedQuery.type; + const tags = validatedQuery.tags; + const teamId = requireTeamId(req.user?.teamId); + + const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({ + teamId, + limit, + type, + tags, + page, + rowsPerPage, + filter, + field, + order, + }); + + return res.status(200).json({ + msg: "Monitors retrieved successfully", + data: monitors, + }); + }); + + exportMonitorsToJSON = catchAsync(async (req: Request, res: Response) => { + const teamId = requireTeamId(req.user?.teamId); + const json = await this.monitorService.exportMonitorsToJSON({ teamId }); + + return res.status(200).json({ + success: true, + msg: "Monitors exported successfully", + data: json, + }); + }); + + getAllGames = catchAsync(async (req: Request, res: Response) => { + const games = this.monitorService.getAllGames(); + return res.status(200).json({ + success: true, + msg: "Supported games retrieved successfully", + data: games, + }); + }); + + updateNotifications = catchAsync(async (req: Request, res: Response) => { + updateNotificationsValidation.parse(req.body); + + const teamId = requireTeamId(req.user?.teamId); + const { monitorIds, notificationIds, action } = req.body; + + // Verify all requested notification IDs actually belong to this team + const teamNotifications = await this.notificationsService.findNotificationsByTeamId(teamId); + const validNotificationIds = teamNotifications.map((n) => n.id); + + const invalidIds = notificationIds.filter((id: string) => !validNotificationIds.includes(id)); + if (invalidIds.length > 0) { + throw new AppError({ + message: `The following notification IDs are invalid or do not belong to your team: ${invalidIds.join(", ")}`, + status: 403, }); - return res.status(200).json({ - success: true, - msg: "Uptime details retrieved successfully", - data: data, - }); - } catch (error) { - next(error); } - }; - getHardwareDetailsById = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params); - const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query); + const modifiedCount = await this.monitorService.updateNotifications({ + teamId, + monitorIds, + notificationIds, + action, + }); - const monitorId = validatedParams.monitorId; - const dateRange = validatedQuery.dateRange || "recent"; - const teamId = requireTeamId(req.user?.teamId); - - const data = await this.monitorService.getHardwareDetailsById({ - teamId, - monitorId, - dateRange, - }); - - return res.status(200).json({ - success: true, - msg: "Hardware details retrieved successfully", - data: data, - }); - } catch (error) { - next(error); - } - }; - getPageSpeedDetailsById = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = getHardwareDetailsByIdParamValidation.parse(req.params); - const validatedQuery = getHardwareDetailsByIdQueryValidation.parse(req.query); - - const monitorId = validatedParams.monitorId; - const dateRange = validatedQuery.dateRange || "recent"; - const teamId = requireTeamId(req.user?.teamId); - - const data = await this.monitorService.getPageSpeedDetailsById({ - teamId, - monitorId, - dateRange, - }); - - return res.status(200).json({ - success: true, - msg: "Page speed details retrieved successfully", - data, - }); - } catch (error) { - next(error); - } - }; - - getGeoChecksByMonitorId = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = getMonitorByIdParamValidation.parse(req.params); - const validatedQuery = getMonitorByIdQueryValidation.parse(req.query); - - const monitorId = validatedParams.monitorId; - const dateRange = validatedQuery.dateRange || "recent"; - const continentParam = validatedQuery.continent; - const continents = continentParam ? (Array.isArray(continentParam) ? continentParam : [continentParam]) : undefined; - const teamId = requireTeamId(req.user?.teamId); - - const data = await this.monitorService.getGeoChecksByMonitorId({ - teamId, - monitorId, - dateRange, - continents, - }); - - return res.status(200).json({ - success: true, - msg: "Geo checks retrieved successfully", - data, - }); - } catch (error) { - next(error); - } - }; - - getMonitorById = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = getMonitorByIdParamValidation.parse(req.params); - - const teamId = requireTeamId(req.user?.teamId); - const monitorId = validatedParams.monitorId; - - const monitor = await this.monitorService.getMonitorById({ teamId, monitorId }); - - return res.status(200).json({ - success: true, - msg: "Monitor retrieved successfully", - data: monitor, - }); - } catch (error) { - next(error); - } - }; - - createMonitor = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedBody = createMonitorBodyValidation.parse(req.body); - - const userId = requireUserId(req.user?.id); - const teamId = requireTeamId(req.user?.teamId); - - const monitor = await this.monitorService.createMonitor(teamId, userId, validatedBody); - - return res.status(200).json({ - success: true, - msg: "Monitor created successfully", - data: monitor, - }); - } catch (error) { - next(error); - } - }; - - importMonitorsFromJSON = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = requireTeamId(req.user?.teamId); - const userId = requireUserId(req.user?.id); - const validatedBody = importMonitorsBodyValidation.parse(req.body); - const monitors = validatedBody.monitors; - - const result = await this.monitorService.importMonitorsFromJSON({ teamId, userId, monitors }); - - return res.status(200).json({ - success: true, - msg: `Successfully imported ${result.imported} monitor(s)`, - data: result, - }); - } catch (error) { - next(error); - } - }; - - deleteMonitor = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = getMonitorByIdParamValidation.parse(req.params); - const teamId = requireTeamId(req.user?.teamId); - const monitorId = validatedParams.monitorId; - - const deletedMonitor = await this.monitorService.deleteMonitor({ teamId, monitorId }); - - return res.status(200).json({ - success: true, - msg: "Monitor deleted successfully", - data: deletedMonitor, - }); - } catch (error) { - next(error); - } - }; - - deleteAllMonitors = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = requireTeamId(req.user?.teamId); - const deletedCount = await this.monitorService.deleteAllMonitors({ teamId }); - - return res.status(200).json({ - success: true, - msg: `Deleted ${deletedCount} monitors`, - }); - } catch (error) { - next(error); - } - }; - - editMonitor = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = getMonitorByIdParamValidation.parse(req.params); - const validatedBody = editMonitorBodyValidation.parse(req.body); - const monitorId = validatedParams.monitorId; - const teamId = requireTeamId(req.user?.teamId); - - const editedMonitor = await this.monitorService.editMonitor({ teamId, monitorId, body: validatedBody }); - - return res.status(200).json({ - success: true, - msg: "Monitor edited successfully", - data: editedMonitor, - }); - } catch (error) { - next(error); - } - }; - - pauseMonitor = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = pauseMonitorParamValidation.parse(req.params); - - const monitorId = validatedParams.monitorId; - const teamId = requireTeamId(req.user?.teamId); - - const monitor = await this.monitorService.pauseMonitor({ teamId, monitorId }); - - return res.status(200).json({ - success: true, - msg: monitor.isActive ? "Monitor resumed successfully" : "Monitor paused successfully", - data: monitor, - }); - } catch (error) { - next(error); - } - }; - - bulkPauseMonitors = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedBody = bulkPauseMonitorBodyValidation.parse(req.body); - - const { monitorIds, pause } = validatedBody; - const teamId = requireTeamId(req.user?.teamId); - - const { monitors, failedCount } = await this.monitorService.bulkPauseMonitors({ teamId, monitorIds, pause }); - - const action = pause ? "paused" : "resumed"; - const monitorStr = monitors.length === 1 ? "monitor" : "monitors"; - - let msg = `${monitors.length} ${monitorStr} ${action} successfully`; - if (failedCount > 0) { - msg = `${monitors.length} ${monitorStr} ${action} in database, but ${failedCount} failed to sync with the job queue. Please check logs.`; - } - - return res.status(200).json({ - success: true, - msg, - data: { monitors, failedCount }, - }); - } catch (error) { - next(error); - } - }; - - addDemoMonitors = async (req: Request, res: Response, next: NextFunction) => { - try { - const id = requireUserId(req.user?.id); - const teamId = requireTeamId(req.user?.teamId); - const demoMonitors = await this.monitorService.addDemoMonitors({ userId: id, teamId }); - - return res.status(200).json({ - success: true, - msg: "Demo monitors added successfully", - data: demoMonitors?.length ?? 0, - }); - } catch (error) { - next(error); - } - }; - - getMonitorsByTeamId = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedQuery = getMonitorsByTeamIdQueryValidation.parse(req.query); - - const teamId = requireTeamId(req.user?.teamId); - const type = validatedQuery.type; - const tags = validatedQuery.tags; - const filter = validatedQuery.filter; - - const monitors = await this.monitorService.getMonitorsByTeamId({ teamId, type, tags, filter }); - - return res.status(200).json({ - success: true, - msg: "Monitors retrieved successfully", - data: monitors, - }); - } catch (error) { - next(error); - } - }; - - getMonitorsWithChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedQuery = getMonitorsWithChecksQueryValidation.parse(req.query); - - const limit = validatedQuery.limit; - const page = validatedQuery.page; - const rowsPerPage = validatedQuery.rowsPerPage; - const filter = validatedQuery.filter; - const field = validatedQuery.field; - const order = validatedQuery.order; - const type = validatedQuery.type; - const tags = validatedQuery.tags; - const teamId = requireTeamId(req.user?.teamId); - - const monitors = await this.monitorService.getMonitorsWithChecksByTeamId({ - teamId, - limit, - type, - tags, - page, - rowsPerPage, - filter, - field, - order, - }); - - return res.status(200).json({ - msg: "Monitors retrieved successfully", - data: monitors, - }); - } catch (error) { - next(error); - } - }; - - exportMonitorsToJSON = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = requireTeamId(req.user?.teamId); - const json = await this.monitorService.exportMonitorsToJSON({ teamId }); - - return res.status(200).json({ - success: true, - msg: "Monitors exported successfully", - data: json, - }); - } catch (error) { - next(error); - } - }; - - getAllGames = async (req: Request, res: Response, next: NextFunction) => { - try { - const games = this.monitorService.getAllGames(); - return res.status(200).json({ - success: true, - msg: "Supported games retrieved successfully", - data: games, - }); - } catch (error) { - next(error); - } - }; - - getGroupsByTeamId = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = requireTeamId(req.user?.teamId); - const groups = await this.monitorService.getGroupsByTeamId({ teamId }); - - return res.status(200).json({ - msg: "Groups retrieved successfully", - data: groups, - }); - } catch (error) { - next(error); - } - }; - - updateNotifications = async (req: Request, res: Response, next: NextFunction) => { - try { - updateNotificationsValidation.parse(req.body); - - const teamId = requireTeamId(req.user?.teamId); - const { monitorIds, notificationIds, action } = req.body; - - // Verify all requested notification IDs actually belong to this team - const teamNotifications = await this.notificationsService.findNotificationsByTeamId(teamId); - const validNotificationIds = teamNotifications.map((n) => n.id); - - const invalidIds = notificationIds.filter((id: string) => !validNotificationIds.includes(id)); - if (invalidIds.length > 0) { - throw new AppError({ - message: `The following notification IDs are invalid or do not belong to your team: ${invalidIds.join(", ")}`, - status: 403, - }); - } - - const modifiedCount = await this.monitorService.updateNotifications({ - teamId, - monitorIds, - notificationIds, - action, - }); - - return res.status(200).json({ - success: true, - msg: `Notifications updated successfully on ${modifiedCount} monitor(s)`, - data: { modifiedCount }, - }); - } catch (error) { - next(error); - } - }; + return res.status(200).json({ + success: true, + msg: `Notifications updated successfully on ${modifiedCount} monitor(s)`, + data: { modifiedCount }, + }); + }); } export default MonitorController; diff --git a/server/src/api/controllers/notificationController.ts b/server/src/api/controllers/notificationController.ts index 3aecfcad8..53dd7645c 100644 --- a/server/src/api/controllers/notificationController.ts +++ b/server/src/api/controllers/notificationController.ts @@ -1,4 +1,5 @@ -import { Request, Response, NextFunction } from "express"; +import { Request, Response, RequestHandler } from "express"; +import { catchAsync } from "@/utils/catchAsync.js"; import { createNotificationBodyValidation, @@ -16,13 +17,13 @@ import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interf const SERVICE_NAME = "NotificationController"; export interface INotificationController { - testNotification: (req: Request, res: Response, next: NextFunction) => Promise; - createNotification: (req: Request, res: Response, next: NextFunction) => Promise; - getNotificationsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise; - deleteNotification: (req: Request, res: Response, next: NextFunction) => Promise; - getNotificationById: (req: Request, res: Response, next: NextFunction) => Promise; - editNotification: (req: Request, res: Response, next: NextFunction) => Promise; - testAllNotifications: (req: Request, res: Response, next: NextFunction) => Promise; + testNotification: RequestHandler; + createNotification: RequestHandler; + getNotificationsByTeamId: RequestHandler; + deleteNotification: RequestHandler; + getNotificationById: RequestHandler; + editNotification: RequestHandler; + testAllNotifications: RequestHandler; } class NotificationController implements INotificationController { private notificationsService: INotificationsService; @@ -32,132 +33,104 @@ class NotificationController implements INotificationController { this.monitorsRepository = monitorsRepository; } - testNotification = async (req: Request, res: Response, next: NextFunction) => { - try { - const notification = testNotificationBodyValidation.parse(req.body); - const success = await this.notificationsService.sendTestNotification(notification); + testNotification = catchAsync(async (req: Request, res: Response) => { + const notification = testNotificationBodyValidation.parse(req.body); + const success = await this.notificationsService.sendTestNotification(notification); - return res.status(200).json({ - success, - msg: success ? "Notification sent successfully" : "Notification could not be sent โ€” check the destination details.", - details: { service: SERVICE_NAME }, - }); - } catch (error) { - next(error); + return res.status(200).json({ + success, + msg: success ? "Notification sent successfully" : "Notification could not be sent โ€” check the destination details.", + details: { service: SERVICE_NAME }, + }); + }); + + createNotification = catchAsync(async (req: Request, res: Response) => { + const validatedBody = createNotificationBodyValidation.parse(req.body); + + const teamId = requireTeamId(req.user?.teamId); + const userId = requireUserId(req.user?.id); + + const notification = await this.notificationsService.createNotification(validatedBody, userId, teamId); + return res.status(200).json({ + success: true, + msg: "Notification created successfully", + data: notification, + }); + }); + + getNotificationsByTeamId = catchAsync(async (req: Request, res: Response) => { + const teamId = requireTeamId(req.user?.teamId); + const notifications = await this.notificationsService.findNotificationsByTeamId(teamId); + + return res.status(200).json({ + success: true, + msg: "Notifications fetched successfully", + data: notifications, + }); + }); + + deleteNotification = catchAsync(async (req: Request, res: Response) => { + const teamId = requireTeamId(req.user?.teamId); + const validatedParams = deleteNotificationParamValidation.parse(req.params); + + await this.notificationsService.deleteById(validatedParams.id, teamId); + return res.status(200).json({ + success: true, + msg: "Notification deleted successfully", + }); + }); + + getNotificationById = catchAsync(async (req: Request, res: Response) => { + const teamId = requireTeamId(req.user?.teamId); + const validatedParams = getNotificationByIdParamValidation.parse(req.params); + + const notification = await this.notificationsService.findById(validatedParams.id, teamId); + + return res.status(200).json({ + success: true, + msg: "Notification fetched successfully", + data: notification, + }); + }); + + editNotification = catchAsync(async (req: Request, res: Response) => { + const validatedBody = createNotificationBodyValidation.parse(req.body); + const validatedParams = editNotificationParamValidation.parse(req.params); + + const teamId = requireTeamId(req.user?.teamId); + const notificationId = validatedParams.id; + + const editedNotification = await this.notificationsService.updateById(notificationId, teamId, validatedBody); + return res.status(200).json({ + success: true, + msg: "Notification updated successfully", + data: editedNotification, + }); + }); + + testAllNotifications = catchAsync(async (req: Request, res: Response) => { + const validatedBody = testAllNotificationsBodyValidation.parse(req.body); + + const teamId = requireTeamId(req.user?.teamId); + + const monitor = await this.monitorsRepository.findById(validatedBody.monitorId, teamId); + const notifications = monitor.notifications || []; + + if (notifications.length === 0) { + throw new AppError({ message: "No notifications", status: 400 }); } - }; - createNotification = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedBody = createNotificationBodyValidation.parse(req.body); + const result = await this.notificationsService.testAllNotifications(notifications); - const teamId = requireTeamId(req.user?.teamId); - const userId = requireUserId(req.user?.id); - - const notification = await this.notificationsService.createNotification(validatedBody, userId, teamId); - return res.status(200).json({ - success: true, - msg: "Notification created successfully", - data: notification, - }); - } catch (error) { - next(error); + if (!result) { + throw new AppError({ message: "Failed to send all notifications", status: 500 }); } - }; - getNotificationsByTeamId = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = requireTeamId(req.user?.teamId); - const notifications = await this.notificationsService.findNotificationsByTeamId(teamId); - - return res.status(200).json({ - success: true, - msg: "Notifications fetched successfully", - data: notifications, - }); - } catch (error) { - next(error); - } - }; - - deleteNotification = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = requireTeamId(req.user?.teamId); - const validatedParams = deleteNotificationParamValidation.parse(req.params); - - await this.notificationsService.deleteById(validatedParams.id, teamId); - return res.status(200).json({ - success: true, - msg: "Notification deleted successfully", - }); - } catch (error) { - next(error); - } - }; - - getNotificationById = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = requireTeamId(req.user?.teamId); - const validatedParams = getNotificationByIdParamValidation.parse(req.params); - - const notification = await this.notificationsService.findById(validatedParams.id, teamId); - - return res.status(200).json({ - success: true, - msg: "Notification fetched successfully", - data: notification, - }); - } catch (error) { - next(error); - } - }; - - editNotification = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedBody = createNotificationBodyValidation.parse(req.body); - const validatedParams = editNotificationParamValidation.parse(req.params); - - const teamId = requireTeamId(req.user?.teamId); - const notificationId = validatedParams.id; - - const editedNotification = await this.notificationsService.updateById(notificationId, teamId, validatedBody); - return res.status(200).json({ - success: true, - msg: "Notification updated successfully", - data: editedNotification, - }); - } catch (error) { - next(error); - } - }; - - testAllNotifications = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedBody = testAllNotificationsBodyValidation.parse(req.body); - - const teamId = requireTeamId(req.user?.teamId); - - const monitor = await this.monitorsRepository.findById(validatedBody.monitorId, teamId); - const notifications = monitor.notifications || []; - - if (notifications.length === 0) { - throw new AppError({ message: "No notifications", status: 400 }); - } - - const result = await this.notificationsService.testAllNotifications(notifications); - - if (!result) { - throw new AppError({ message: "Failed to send all notifications", status: 500 }); - } - - return res.status(200).json({ - success: true, - msg: "All notifications sent successfully", - }); - } catch (error) { - next(error); - } - }; + return res.status(200).json({ + success: true, + msg: "All notifications sent successfully", + }); + }); } export default NotificationController; diff --git a/server/src/api/controllers/queueController.ts b/server/src/api/controllers/queueController.ts index 0a21761ab..03204167c 100755 --- a/server/src/api/controllers/queueController.ts +++ b/server/src/api/controllers/queueController.ts @@ -1,77 +1,55 @@ import { IJobScheduler } from "@/worker/worker.interface.js"; import { getQueueJobsQueryValidation } from "@/api/validation/queueValidation.js"; -import { Request, Response, NextFunction } from "express"; - -const SERVICE_NAME = "JobQueueController"; +import { Request, Response, RequestHandler } from "express"; +import { catchAsync } from "@/utils/catchAsync.js"; export interface IJobQueueController { - getMetrics(req: Request, res: Response, next: NextFunction): Promise; - getJobs(req: Request, res: Response, next: NextFunction): Promise; - getAllMetrics(req: Request, res: Response, next: NextFunction): Promise; - flushQueue(req: Request, res: Response, next: NextFunction): Promise; + getMetrics: RequestHandler; + getJobs: RequestHandler; + getAllMetrics: RequestHandler; + flushQueue: RequestHandler; } class JobQueueController implements IJobQueueController { - static SERVICE_NAME = SERVICE_NAME; constructor(private scheduler: IJobScheduler) {} - get serviceName() { - return JobQueueController.SERVICE_NAME; - } + getMetrics = catchAsync(async (req: Request, res: Response) => { + const metrics = await this.scheduler.getMetrics(); + res.status(200).json({ + success: true, + msg: "Queue metrics fetched successfully", + data: metrics, + }); + }); - getMetrics = async (req: Request, res: Response, next: NextFunction) => { - try { - const metrics = await this.scheduler.getMetrics(); - res.status(200).json({ - success: true, - msg: "Queue metrics fetched successfully", - data: metrics, - }); - } catch (error) { - next(error); - } - }; + getJobs = catchAsync(async (req: Request, res: Response) => { + const pagination = getQueueJobsQueryValidation.parse(req.query); + const { jobs, count } = await this.scheduler.getJobs(pagination); + return res.status(200).json({ + success: true, + msg: "Queue jobs fetched successfully", + data: { jobs, count }, + }); + }); - getJobs = async (req: Request, res: Response, next: NextFunction) => { - try { - const pagination = getQueueJobsQueryValidation.parse(req.query); - const { jobs, count } = await this.scheduler.getJobs(pagination); - return res.status(200).json({ - success: true, - msg: "Queue jobs fetched successfully", - data: { jobs, count }, - }); - } catch (error) { - next(error); - } - }; + getAllMetrics = catchAsync(async (req: Request, res: Response) => { + const pagination = getQueueJobsQueryValidation.parse(req.query); + const { jobs, count } = await this.scheduler.getJobs(pagination); + const metrics = await this.scheduler.getMetrics(); + return res.status(200).json({ + success: true, + msg: "Queue metrics fetched successfully", + data: { jobs, count, metrics }, + }); + }); - getAllMetrics = async (req: Request, res: Response, next: NextFunction) => { - try { - const pagination = getQueueJobsQueryValidation.parse(req.query); - const { jobs, count } = await this.scheduler.getJobs(pagination); - const metrics = await this.scheduler.getMetrics(); - return res.status(200).json({ - success: true, - msg: "Queue metrics fetched successfully", - data: { jobs, count, metrics }, - }); - } catch (error) { - next(error); - } - }; - - flushQueue = async (req: Request, res: Response, next: NextFunction) => { - try { - const result = await this.scheduler.flushQueues(); - return res.status(200).json({ - success: true, - msg: "Queue flushed successfully", - data: result, - }); - } catch (error) { - next(error); - } - }; + flushQueue = catchAsync(async (req: Request, res: Response) => { + const result = await this.scheduler.flushQueues(); + return res.status(200).json({ + success: true, + msg: "Queue flushed successfully", + data: result, + }); + }); } export default JobQueueController; diff --git a/server/src/api/controllers/settingsController.ts b/server/src/api/controllers/settingsController.ts index 4acfdf552..3cb032e93 100644 --- a/server/src/api/controllers/settingsController.ts +++ b/server/src/api/controllers/settingsController.ts @@ -1,4 +1,5 @@ -import { Request, Response, NextFunction } from "express"; +import { Request, Response, RequestHandler } from "express"; +import { catchAsync } from "@/utils/catchAsync.js"; import { updateAppSettingsBodyValidation } from "@/api/validation/settingsValidation.js"; import { sendTestEmailBodyValidation } from "@/api/validation/notificationValidation.js"; import { AppError } from "@/utils/AppError.js"; @@ -6,17 +7,13 @@ import { ISettingsService } from "@/domain/app-settings/app-settings.service.js" import { IEmailService } from "@/service/emailService.js"; import { Settings } from "@/domain/app-settings/app-settings.type.js"; -const SERVICE_NAME = "SettingsController"; - export interface ISettingsController { - serviceName: string; - getAppSettings(req: Request, res: Response, next: NextFunction): Promise; - updateAppSettings(req: Request, res: Response, next: NextFunction): Promise; - sendTestEmail(req: Request, res: Response, next: NextFunction): Promise; + getAppSettings: RequestHandler; + updateAppSettings: RequestHandler; + sendTestEmail: RequestHandler; } class SettingsController implements ISettingsController { - static SERVICE_NAME = SERVICE_NAME; private settingsService: ISettingsService; private emailService: IEmailService; constructor(settingsService: ISettingsService, emailService: IEmailService) { @@ -24,10 +21,6 @@ class SettingsController implements ISettingsController { this.emailService = emailService; } - get serviceName() { - return SettingsController.SERVICE_NAME; - } - buildAppSettings = (dbSettings: Settings) => { const sanitizedSettings: Record = { ...dbSettings }; delete sanitizedSettings.version; @@ -50,94 +43,82 @@ class SettingsController implements ISettingsController { return returnSettings; }; - getAppSettings = async (req: Request, res: Response, next: NextFunction) => { - try { - const dbSettings = await this.settingsService.getDBSettings(); + getAppSettings = catchAsync(async (req: Request, res: Response) => { + const dbSettings = await this.settingsService.getDBSettings(); - const returnSettings = this.buildAppSettings(dbSettings); - return res.status(200).json({ - success: true, - msg: "App settings fetched successfully", - data: returnSettings, - }); - } catch (error) { - next(error); + const returnSettings = this.buildAppSettings(dbSettings); + return res.status(200).json({ + success: true, + msg: "App settings fetched successfully", + data: returnSettings, + }); + }); + + updateAppSettings = catchAsync(async (req: Request, res: Response) => { + const validatedBody = updateAppSettingsBodyValidation.parse(req.body); + + const updatedSettings = await this.settingsService.updateDbSettings(validatedBody); + const returnSettings = this.buildAppSettings(updatedSettings); + return res.status(200).json({ + success: true, + msg: "App settings updated successfully", + data: returnSettings, + }); + }); + + sendTestEmail = catchAsync(async (req: Request, res: Response) => { + sendTestEmailBodyValidation.parse(req.body); + + const { + to, + systemEmailHost, + systemEmailPort, + systemEmailAddress, + systemEmailDisplayName, + systemEmailPassword, + systemEmailUser, + systemEmailConnectionHost, + systemEmailSecure, + systemEmailPool, + systemEmailIgnoreTLS, + systemEmailRequireTLS, + systemEmailRejectUnauthorized, + systemEmailTLSServername, + } = req.body; + + const subject = "This is a test email from Checkmate"; + const context = { testName: "Monitoring System" }; + + const html = await this.emailService.buildEmail("testEmailTemplate", context); + if (!html) { + throw new AppError({ message: "Failed to build email template.", status: 500 }); } - }; + const messageId = await this.emailService.sendEmail(to, subject, html, { + systemEmailHost, + systemEmailPort, + systemEmailUser, + systemEmailAddress, + systemEmailDisplayName, + systemEmailPassword, + systemEmailConnectionHost, + systemEmailSecure, + systemEmailPool, + systemEmailIgnoreTLS, + systemEmailRequireTLS, + systemEmailRejectUnauthorized, + systemEmailTLSServername, + }); - updateAppSettings = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedBody = updateAppSettingsBodyValidation.parse(req.body); - - const updatedSettings = await this.settingsService.updateDbSettings(validatedBody); - const returnSettings = this.buildAppSettings(updatedSettings); - return res.status(200).json({ - success: true, - msg: "App settings updated successfully", - data: returnSettings, - }); - } catch (error) { - next(error); + if (!messageId) { + throw new AppError({ message: "Failed to send test email.", status: 500 }); } - }; - sendTestEmail = async (req: Request, res: Response, next: NextFunction) => { - try { - sendTestEmailBodyValidation.parse(req.body); - - const { - to, - systemEmailHost, - systemEmailPort, - systemEmailAddress, - systemEmailDisplayName, - systemEmailPassword, - systemEmailUser, - systemEmailConnectionHost, - systemEmailSecure, - systemEmailPool, - systemEmailIgnoreTLS, - systemEmailRequireTLS, - systemEmailRejectUnauthorized, - systemEmailTLSServername, - } = req.body; - - const subject = "This is a test email from Checkmate"; - const context = { testName: "Monitoring System" }; - - const html = await this.emailService.buildEmail("testEmailTemplate", context); - if (!html) { - throw new AppError({ message: "Failed to build email template.", status: 500 }); - } - const messageId = await this.emailService.sendEmail(to, subject, html, { - systemEmailHost, - systemEmailPort, - systemEmailUser, - systemEmailAddress, - systemEmailDisplayName, - systemEmailPassword, - systemEmailConnectionHost, - systemEmailSecure, - systemEmailPool, - systemEmailIgnoreTLS, - systemEmailRequireTLS, - systemEmailRejectUnauthorized, - systemEmailTLSServername, - }); - - if (!messageId) { - throw new AppError({ message: "Failed to send test email.", status: 500 }); - } - - return res.status(200).json({ - success: true, - msg: "Test email sent successfully", - data: { messageId }, - }); - } catch (error) { - next(error); - } - }; + return res.status(200).json({ + success: true, + msg: "Test email sent successfully", + data: { messageId }, + }); + }); } export default SettingsController; diff --git a/server/src/api/controllers/statusPageController.ts b/server/src/api/controllers/statusPageController.ts index d820e5eed..038ddb423 100644 --- a/server/src/api/controllers/statusPageController.ts +++ b/server/src/api/controllers/statusPageController.ts @@ -1,4 +1,5 @@ -import { Request, Response, NextFunction } from "express"; +import { Request, Response, RequestHandler } from "express"; +import { catchAsync } from "@/utils/catchAsync.js"; import { createStatusPageBodyValidation, @@ -12,155 +13,123 @@ import { requireTeamId, requireUserId } from "@/api/controllers/controllerUtils. import { IStatusPageService } from "@/domain/status-pages/status-page.service.js"; import { resolveStatusPageDomainFromRequest } from "@/utils/statusPageDomain.js"; -const SERVICE_NAME = "statusPageController"; - export interface IStatusPageController { - readonly serviceName: string; - createStatusPage(req: Request, res: Response, next: NextFunction): Promise; - updateStatusPage(req: Request, res: Response, next: NextFunction): Promise; - getStatusPageByUrl(req: Request, res: Response, next: NextFunction): Promise; - resolveStatusPageByDomain(req: Request, res: Response, next: NextFunction): Promise; - getStatusPagesByTeamId(req: Request, res: Response, next: NextFunction): Promise; - deleteStatusPage(req: Request, res: Response, next: NextFunction): Promise; + createStatusPage: RequestHandler; + updateStatusPage: RequestHandler; + getStatusPageByUrl: RequestHandler; + resolveStatusPageByDomain: RequestHandler; + getStatusPagesByTeamId: RequestHandler; + deleteStatusPage: RequestHandler; } class StatusPageController implements IStatusPageController { - static SERVICE_NAME = SERVICE_NAME; private statusPageService: IStatusPageService; constructor(statusPageService: IStatusPageService) { this.statusPageService = statusPageService; } - get serviceName() { - return StatusPageController.SERVICE_NAME; - } - - createStatusPage = async (req: Request, res: Response, next: NextFunction) => { - try { - createStatusPageBodyValidation.parse(req.body); - if (req.file) { - imageValidation.parse(req.file); - } - - const teamId = requireTeamId(req?.user?.teamId); - const userId = requireUserId(req?.user?.id); - const statusPage = await this.statusPageService.createStatusPage(userId, teamId, req.file, req.body); - - return res.status(200).json({ - success: true, - msg: "Status page created successfully", - data: statusPage, - }); - } catch (error) { - next(error); + createStatusPage = catchAsync(async (req: Request, res: Response) => { + createStatusPageBodyValidation.parse(req.body); + if (req.file) { + imageValidation.parse(req.file); } - }; - updateStatusPage = async (req: Request, res: Response, next: NextFunction) => { - try { - createStatusPageBodyValidation.parse(req.body); - if (req.file) { - imageValidation.parse(req.file); - } + const teamId = requireTeamId(req?.user?.teamId); + const userId = requireUserId(req?.user?.id); + const statusPage = await this.statusPageService.createStatusPage(userId, teamId, req.file, req.body); - const teamId = requireTeamId(req?.user?.teamId); - const statusPageId = req.params.id as string; - if (!statusPageId) { - throw new AppError({ message: "Status page ID is required", status: 400 }); - } - const statusPage = await this.statusPageService.updateStatusPage(statusPageId, teamId, req.file, req.body); - if (statusPage === null) { - throw new AppError({ message: "Status page not found", status: 404 }); - } - res.status(200).json({ - success: true, - msg: "Status page updated successfully", - data: statusPage, - }); - } catch (error) { - next(error); + return res.status(200).json({ + success: true, + msg: "Status page created successfully", + data: statusPage, + }); + }); + + updateStatusPage = catchAsync(async (req: Request, res: Response) => { + createStatusPageBodyValidation.parse(req.body); + if (req.file) { + imageValidation.parse(req.file); } - }; - getStatusPageByUrl = async (req: Request, res: Response, next: NextFunction) => { - try { - getStatusPageParamValidation.parse(req.params); - getStatusPageQueryValidation.parse(req.query); - - if (!req.params.url) { - throw new AppError({ message: "Status page URL is required", status: 400 }); - } - - const statusPage = await this.statusPageService.getStatusPageByUrl(req.params.url as string); - const data = await this.statusPageService.getPublicStatusPagePayload(statusPage, req.user?.teamId); - - return res.status(200).json({ - success: true, - msg: "Status page retrieved successfully", - data, - }); - } catch (error) { - next(error); + const teamId = requireTeamId(req?.user?.teamId); + const statusPageId = req.params.id as string; + if (!statusPageId) { + throw new AppError({ message: "Status page ID is required", status: 400 }); } - }; - - resolveStatusPageByDomain = async (req: Request, res: Response, next: NextFunction) => { - try { - resolveStatusPageQueryValidation.parse(req.query); - - const domain = resolveStatusPageDomainFromRequest(req.hostname, req.query.domain as string | undefined); - if (!domain) { - throw new AppError({ message: "Domain is required", status: 400 }); - } - - const statusPage = await this.statusPageService.getStatusPageByCustomDomain(domain); - if (!statusPage.isPublished) { - throw new AppError({ message: "Status page not found", status: 404 }); - } - - const data = await this.statusPageService.getPublicStatusPagePayload(statusPage, req.user?.teamId); - - return res.status(200).json({ - success: true, - msg: "Status page retrieved successfully", - data, - }); - } catch (error) { - next(error); + const statusPage = await this.statusPageService.updateStatusPage(statusPageId, teamId, req.file, req.body); + if (statusPage === null) { + throw new AppError({ message: "Status page not found", status: 404 }); } - }; + res.status(200).json({ + success: true, + msg: "Status page updated successfully", + data: statusPage, + }); + }); - getStatusPagesByTeamId = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = requireTeamId(req.user?.teamId); - const statusPages = await this.statusPageService.getStatusPagesByTeamId(teamId); + getStatusPageByUrl = catchAsync(async (req: Request, res: Response) => { + getStatusPageParamValidation.parse(req.params); + getStatusPageQueryValidation.parse(req.query); - return res.status(200).json({ - success: true, - msg: "Status pages retrieved successfully", - data: statusPages, - }); - } catch (error) { - next(error); + if (!req.params.url) { + throw new AppError({ message: "Status page URL is required", status: 400 }); } - }; - deleteStatusPage = async (req: Request, res: Response, next: NextFunction) => { - try { - const statusPageId = req.params.id as string; - if (!statusPageId) { - throw new AppError({ message: "Status page ID is required", status: 400 }); - } - const teamId = requireTeamId(req.user?.teamId); - await this.statusPageService.deleteStatusPage(statusPageId, teamId); - return res.status(200).json({ - success: true, - msg: "Status page deleted successfully", - }); - } catch (error) { - next(error); + const statusPage = await this.statusPageService.getStatusPageByUrl(req.params.url as string); + const data = await this.statusPageService.getPublicStatusPagePayload(statusPage, req.user?.teamId); + + return res.status(200).json({ + success: true, + msg: "Status page retrieved successfully", + data, + }); + }); + + resolveStatusPageByDomain = catchAsync(async (req: Request, res: Response) => { + resolveStatusPageQueryValidation.parse(req.query); + + const domain = resolveStatusPageDomainFromRequest(req.hostname, req.query.domain as string | undefined); + if (!domain) { + throw new AppError({ message: "Domain is required", status: 400 }); } - }; + + const statusPage = await this.statusPageService.getStatusPageByCustomDomain(domain); + if (!statusPage.isPublished) { + throw new AppError({ message: "Status page not found", status: 404 }); + } + + const data = await this.statusPageService.getPublicStatusPagePayload(statusPage, req.user?.teamId); + + return res.status(200).json({ + success: true, + msg: "Status page retrieved successfully", + data, + }); + }); + + getStatusPagesByTeamId = catchAsync(async (req: Request, res: Response) => { + const teamId = requireTeamId(req.user?.teamId); + const statusPages = await this.statusPageService.getStatusPagesByTeamId(teamId); + + return res.status(200).json({ + success: true, + msg: "Status pages retrieved successfully", + data: statusPages, + }); + }); + + deleteStatusPage = catchAsync(async (req: Request, res: Response) => { + const statusPageId = req.params.id as string; + if (!statusPageId) { + throw new AppError({ message: "Status page ID is required", status: 400 }); + } + const teamId = requireTeamId(req.user?.teamId); + await this.statusPageService.deleteStatusPage(statusPageId, teamId); + return res.status(200).json({ + success: true, + msg: "Status page deleted successfully", + }); + }); } export default StatusPageController; diff --git a/server/src/api/controllers/tagController.ts b/server/src/api/controllers/tagController.ts index 95dd81296..abe042efa 100644 --- a/server/src/api/controllers/tagController.ts +++ b/server/src/api/controllers/tagController.ts @@ -1,4 +1,5 @@ -import { Request, Response, NextFunction } from "express"; +import { Request, Response, RequestHandler } from "express"; +import { catchAsync } from "@/utils/catchAsync.js"; import { ITagsService } from "@/domain/tags/tag.service.js"; import { requireTeamId } from "./controllerUtils.js"; import { @@ -10,89 +11,69 @@ import { } from "@/api/validation/index.js"; export interface ITagsController { - createTag: (req: Request, res: Response, next: NextFunction) => Promise; - getTagById: (req: Request, res: Response, next: NextFunction) => Promise; - getTagsByTeamId: (req: Request, res: Response, next: NextFunction) => Promise; - editTag: (req: Request, res: Response, next: NextFunction) => Promise; - deleteTag: (req: Request, res: Response, next: NextFunction) => Promise; + createTag: RequestHandler; + getTagById: RequestHandler; + getTagsByTeamId: RequestHandler; + editTag: RequestHandler; + deleteTag: RequestHandler; } class TagsController implements ITagsController { constructor(private tagsService: ITagsService) {} - createTag = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedBody = createTagBodyValidation.parse(req.body); - const teamId = requireTeamId(req.user?.teamId); + createTag = catchAsync(async (req: Request, res: Response) => { + const validatedBody = createTagBodyValidation.parse(req.body); + const teamId = requireTeamId(req.user?.teamId); - const tag = await this.tagsService.createTag(validatedBody, teamId); - return res.status(200).json({ - success: true, - msg: "Tag created successfully", - data: tag, - }); - } catch (error) { - next(error); - } - }; - getTagById = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = requireTeamId(req.user?.teamId); - const { id: tagId } = getTagByIdParamValidation.parse(req.params); - const tag = await this.tagsService.getTag(tagId, teamId); - return res.status(200).json({ - success: true, - msg: "Tag retrieved successfully", - data: tag, - }); - } catch (error) { - next(error); - } - }; + const tag = await this.tagsService.createTag(validatedBody, teamId); + return res.status(200).json({ + success: true, + msg: "Tag created successfully", + data: tag, + }); + }); + getTagById = catchAsync(async (req: Request, res: Response) => { + const teamId = requireTeamId(req.user?.teamId); + const { id: tagId } = getTagByIdParamValidation.parse(req.params); + const tag = await this.tagsService.getTag(tagId, teamId); + return res.status(200).json({ + success: true, + msg: "Tag retrieved successfully", + data: tag, + }); + }); - getTagsByTeamId = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = requireTeamId(req.user?.teamId); - const tags = await this.tagsService.getTagsByTeamId(teamId); - return res.status(200).json({ - success: true, - msg: "Tags retrieved successfully", - data: tags, - }); - } catch (error) { - next(error); - } - }; + getTagsByTeamId = catchAsync(async (req: Request, res: Response) => { + const teamId = requireTeamId(req.user?.teamId); + const tags = await this.tagsService.getTagsByTeamId(teamId); + return res.status(200).json({ + success: true, + msg: "Tags retrieved successfully", + data: tags, + }); + }); - editTag = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = requireTeamId(req.user?.teamId); - const { id: tagId } = editTagParamValidation.parse(req.params); - const validatedBody = editTagBodyValidation.parse(req.body); - const updatedTag = await this.tagsService.updateTag(tagId, teamId, validatedBody); - return res.status(200).json({ - success: true, - msg: "Tag updated successfully", - data: updatedTag, - }); - } catch (error) { - next(error); - } - }; + editTag = catchAsync(async (req: Request, res: Response) => { + const teamId = requireTeamId(req.user?.teamId); + const { id: tagId } = editTagParamValidation.parse(req.params); + const validatedBody = editTagBodyValidation.parse(req.body); + const updatedTag = await this.tagsService.updateTag(tagId, teamId, validatedBody); + return res.status(200).json({ + success: true, + msg: "Tag updated successfully", + data: updatedTag, + }); + }); - deleteTag = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = requireTeamId(req.user?.teamId); - const { id: tagId } = deleteTagParamValidation.parse(req.params); - await this.tagsService.deleteTag(tagId, teamId); - return res.status(200).json({ - success: true, - msg: "Tag deleted successfully", - }); - } catch (error) { - next(error); - } - }; + deleteTag = catchAsync(async (req: Request, res: Response) => { + const teamId = requireTeamId(req.user?.teamId); + const { id: tagId } = deleteTagParamValidation.parse(req.params); + await this.tagsService.deleteTag(tagId, teamId); + return res.status(200).json({ + success: true, + msg: "Tag deleted successfully", + }); + }); } export default TagsController; diff --git a/server/src/api/routes/authRoute.ts b/server/src/api/routes/authRoute.ts deleted file mode 100644 index 3b202a502..000000000 --- a/server/src/api/routes/authRoute.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Router, RequestHandler } from "express"; -import { isAllowed } from "@/api/middleware/isAllowed.js"; -import { imageUpload } from "@/api/middleware/upload.js"; -import { IAuthController } from "@/api/controllers/authController.js"; - -class AuthRoutes { - private router: Router; - private authController: IAuthController; - - constructor(authController: IAuthController, verifyJWT: RequestHandler) { - this.router = Router(); - this.authController = authController; - this.initRoutes(verifyJWT); - } - - initRoutes(verifyJWT: RequestHandler) { - this.router.post("/register", imageUpload.single("profileImage"), this.authController.registerUser); - this.router.post("/login", this.authController.loginUser); - - this.router.post("/recovery/request", this.authController.requestRecovery); - this.router.post("/recovery/validate", this.authController.validateRecovery); - this.router.post("/recovery/reset/", this.authController.resetPassword); - - this.router.get("/users/superadmin", this.authController.checkSuperadminExists); - - this.router.get("/users", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.getAllUsers); - this.router.post("/users", verifyJWT, isAllowed(["superadmin"]), imageUpload.single("profileImage"), this.authController.createUser); - this.router.get("/users/:userId", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.getUserById); - this.router.patch("/users/:userId", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserById); - this.router.patch("/users/:userId/password", verifyJWT, isAllowed(["superadmin"]), this.authController.editUserPasswordById); - this.router.delete("/users/:userId", verifyJWT, isAllowed(["admin", "superadmin"]), this.authController.deleteUserById); - - this.router.patch( - "/user", - verifyJWT, - imageUpload.single("profileImage"), - isAllowed(["admin", "superadmin", "user"]), - this.authController.editUser - ); - this.router.delete("/user", verifyJWT, isAllowed(["admin", "superadmin", "user"]), this.authController.deleteUser); - } - - getRouter() { - return this.router; - } -} - -export default AuthRoutes; diff --git a/server/src/api/routes/authRoutes.ts b/server/src/api/routes/authRoutes.ts new file mode 100644 index 000000000..ba92091bd --- /dev/null +++ b/server/src/api/routes/authRoutes.ts @@ -0,0 +1,27 @@ +import { Router, RequestHandler } from "express"; +import { isAllowed } from "@/api/middleware/isAllowed.js"; +import { imageUpload } from "@/api/middleware/upload.js"; +import { IAuthController } from "@/api/controllers/authController.js"; + +export const createAuthRoutes = (authController: IAuthController, verifyJWT: RequestHandler): Router => { + const router = Router(); + router.post("/register", imageUpload.single("profileImage"), authController.registerUser); + router.post("/login", authController.loginUser); + + router.post("/recovery/request", authController.requestRecovery); + router.post("/recovery/validate", authController.validateRecovery); + router.post("/recovery/reset/", authController.resetPassword); + + router.get("/users/superadmin", authController.checkSuperadminExists); + + router.get("/users", verifyJWT, isAllowed(["admin", "superadmin"]), authController.getAllUsers); + router.post("/users", verifyJWT, isAllowed(["superadmin"]), imageUpload.single("profileImage"), authController.createUser); + router.get("/users/:userId", verifyJWT, isAllowed(["admin", "superadmin"]), authController.getUserById); + router.patch("/users/:userId", verifyJWT, isAllowed(["superadmin"]), authController.editUserById); + router.patch("/users/:userId/password", verifyJWT, isAllowed(["superadmin"]), authController.editUserPasswordById); + router.delete("/users/:userId", verifyJWT, isAllowed(["admin", "superadmin"]), authController.deleteUserById); + + router.patch("/user", verifyJWT, imageUpload.single("profileImage"), isAllowed(["admin", "superadmin", "user"]), authController.editUser); + router.delete("/user", verifyJWT, isAllowed(["admin", "superadmin", "user"]), authController.deleteUser); + return router; +}; diff --git a/server/src/api/routes/checkRoute.ts b/server/src/api/routes/checkRoute.ts deleted file mode 100644 index 171d79ab4..000000000 --- a/server/src/api/routes/checkRoute.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Router } from "express"; - -import { isAllowed } from "../middleware/isAllowed.js"; -import { ICheckController } from "@/api/controllers/checkController.js"; - -class CheckRoutes { - private router: Router; - private checkController: ICheckController; - - constructor(checkController: ICheckController) { - this.router = Router(); - this.checkController = checkController; - this.initRoutes(); - } - - initRoutes() { - this.router.get("/team/summary", this.checkController.getChecksSummaryByTeamId); - this.router.get("/team", this.checkController.getChecksByTeam); - this.router.delete("/team", isAllowed(["admin", "superadmin"]), this.checkController.deleteChecksByTeamId); - this.router.get("/:monitorId", this.checkController.getChecksByMonitor); - this.router.delete("/:monitorId", this.checkController.deleteChecks); - } - - getRouter() { - return this.router; - } -} - -export default CheckRoutes; diff --git a/server/src/api/routes/checkRoutes.ts b/server/src/api/routes/checkRoutes.ts new file mode 100644 index 000000000..97974f9df --- /dev/null +++ b/server/src/api/routes/checkRoutes.ts @@ -0,0 +1,14 @@ +import { Router } from "express"; + +import { isAllowed } from "../middleware/isAllowed.js"; +import { ICheckController } from "@/api/controllers/checkController.js"; + +export const createCheckRoutes = (checkController: ICheckController): Router => { + const router = Router(); + router.get("/team/summary", checkController.getChecksSummaryByTeamId); + router.get("/team", checkController.getChecksByTeam); + router.delete("/team", isAllowed(["admin", "superadmin"]), checkController.deleteChecksByTeamId); + router.get("/:monitorId", checkController.getChecksByMonitor); + router.delete("/:monitorId", checkController.deleteChecks); + return router; +}; diff --git a/server/src/api/routes/diagnosticRoute.ts b/server/src/api/routes/diagnosticRoute.ts deleted file mode 100644 index 3fe29bc1e..000000000 --- a/server/src/api/routes/diagnosticRoute.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { RequestHandler, Router } from "express"; -import { isAllowed } from "../middleware/isAllowed.js"; -import { IDiagnosticController } from "@/api/controllers/diagnosticController.js"; - -class DiagnosticRoutes { - private router: Router; - private diagnosticController: IDiagnosticController; - - constructor(diagnosticController: IDiagnosticController, verifyJWT: RequestHandler) { - this.router = Router(); - this.diagnosticController = diagnosticController; - this.initRoutes(verifyJWT); - } - - initRoutes(verifyJWT: RequestHandler) { - this.router.get("/system", verifyJWT, isAllowed(["admin", "superadmin"]), this.diagnosticController.getSystemStats); - } - - getRouter() { - return this.router; - } -} - -export default DiagnosticRoutes; diff --git a/server/src/api/routes/diagnosticRoutes.ts b/server/src/api/routes/diagnosticRoutes.ts new file mode 100644 index 000000000..680c10015 --- /dev/null +++ b/server/src/api/routes/diagnosticRoutes.ts @@ -0,0 +1,9 @@ +import { RequestHandler, Router } from "express"; +import { isAllowed } from "../middleware/isAllowed.js"; +import { IDiagnosticController } from "@/api/controllers/diagnosticController.js"; + +export const createDiagnosticRoutes = (diagnosticController: IDiagnosticController, verifyJWT: RequestHandler): Router => { + const router = Router(); + router.get("/system", verifyJWT, isAllowed(["admin", "superadmin"]), diagnosticController.getSystemStats); + return router; +}; diff --git a/server/src/api/routes/geoCheckRoutes.ts b/server/src/api/routes/geoCheckRoutes.ts index bf9178afe..08c577c0e 100644 --- a/server/src/api/routes/geoCheckRoutes.ts +++ b/server/src/api/routes/geoCheckRoutes.ts @@ -1,23 +1,9 @@ import { IGeoCheckController } from "@/api/controllers/geoCheckController.js"; import { Router } from "express"; -class GeoCheckRoutes { - private router: Router; - private geoCheckController: IGeoCheckController; +export const createGeoCheckRoutes = (geoCheckController: IGeoCheckController): Router => { + const router = Router(); + router.get("/:monitorId", geoCheckController.getGeoChecksByMonitor); - constructor(geoCheckController: IGeoCheckController) { - this.router = Router(); - this.geoCheckController = geoCheckController; - this.initRoutes(); - } - - initRoutes() { - this.router.get("/:monitorId", this.geoCheckController.getGeoChecksByMonitor); - } - - getRouter() { - return this.router; - } -} - -export default GeoCheckRoutes; + return router; +}; diff --git a/server/src/api/routes/incidentRoute.ts b/server/src/api/routes/incidentRoute.ts deleted file mode 100644 index 0f2f4485c..000000000 --- a/server/src/api/routes/incidentRoute.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { Router } from "express"; -import { isAllowed } from "../middleware/isAllowed.js"; -import { IIncidentController } from "@/api/controllers/incidentController.js"; - -class IncidentRoutes { - private router: Router; - private incidentController: IIncidentController; - - constructor(incidentController: IIncidentController) { - this.router = Router(); - this.incidentController = incidentController; - this.initRoutes(); - } - - initRoutes() { - // Team routes - this.router.get("/team", this.incidentController.getIncidentsByTeam); - this.router.get("/team/summary", this.incidentController.getIncidentSummary); - - // Individual incident routes - this.router.get("/:incidentId", this.incidentController.getIncidentById); - this.router.put("/:incidentId/resolve", isAllowed(["admin", "superadmin"]), this.incidentController.resolveIncidentManually); - } - - getRouter() { - return this.router; - } -} - -export default IncidentRoutes; diff --git a/server/src/api/routes/incidentRoutes.ts b/server/src/api/routes/incidentRoutes.ts new file mode 100644 index 000000000..f9c42d0f2 --- /dev/null +++ b/server/src/api/routes/incidentRoutes.ts @@ -0,0 +1,12 @@ +import { Router } from "express"; +import { isAllowed } from "../middleware/isAllowed.js"; +import { IIncidentController } from "@/api/controllers/incidentController.js"; + +export const createIncidentRoutes = (incidentController: IIncidentController): Router => { + const router = Router(); + router.get("/team", incidentController.getIncidentsByTeam); + router.get("/team/summary", incidentController.getIncidentSummary); + router.get("/:incidentId", incidentController.getIncidentById); + router.put("/:incidentId/resolve", isAllowed(["admin", "superadmin"]), incidentController.resolveIncidentManually); + return router; +}; diff --git a/server/src/api/routes/inviteRoute.ts b/server/src/api/routes/inviteRoute.ts deleted file mode 100644 index 2e25694d2..000000000 --- a/server/src/api/routes/inviteRoute.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { RequestHandler, Router } from "express"; -import { isAllowed } from "../middleware/isAllowed.js"; -import { IInviteController } from "@/api/controllers/inviteController.js"; - -class InviteRoutes { - private router: Router; - private inviteController: IInviteController; - - constructor(inviteController: IInviteController, verifyJWT: RequestHandler) { - this.router = Router(); - this.inviteController = inviteController; - this.initRoutes(verifyJWT); - } - - initRoutes(verifyJWT: RequestHandler) { - this.router.post("/send", verifyJWT, isAllowed(["admin", "superadmin"]), this.inviteController.sendInviteEmail); - this.router.post("/verify", this.inviteController.verifyInviteToken); - this.router.post("/", verifyJWT, isAllowed(["admin", "superadmin"]), this.inviteController.getInviteToken); - } - - getRouter() { - return this.router; - } -} - -export default InviteRoutes; diff --git a/server/src/api/routes/inviteRoutes.ts b/server/src/api/routes/inviteRoutes.ts new file mode 100644 index 000000000..fe6ba9664 --- /dev/null +++ b/server/src/api/routes/inviteRoutes.ts @@ -0,0 +1,11 @@ +import { RequestHandler, Router } from "express"; +import { isAllowed } from "../middleware/isAllowed.js"; +import { IInviteController } from "@/api/controllers/inviteController.js"; + +export const createInviteRoutes = (inviteController: IInviteController, verifyJWT: RequestHandler): Router => { + const router = Router(); + router.post("/send", verifyJWT, isAllowed(["admin", "superadmin"]), inviteController.sendInviteEmail); + router.post("/verify", inviteController.verifyInviteToken); + router.post("/", verifyJWT, isAllowed(["admin", "superadmin"]), inviteController.getInviteToken); + return router; +}; diff --git a/server/src/api/routes/logRoutes.ts b/server/src/api/routes/logRoutes.ts index ef98b5189..e0de3b93b 100644 --- a/server/src/api/routes/logRoutes.ts +++ b/server/src/api/routes/logRoutes.ts @@ -1,22 +1,10 @@ import { Router } from "express"; import { isAllowed } from "@/api/middleware/isAllowed.js"; + import { ILogController } from "@/api/controllers/logController.js"; -class LogRoutes { - private router: Router; - private logController: ILogController; - constructor(logController: ILogController) { - this.router = Router(); - this.logController = logController; - this.initRoutes(); - } - initRoutes() { - this.router.get("/", isAllowed(["admin", "superadmin"]), this.logController.getLogs); - } - - getRouter() { - return this.router; - } -} - -export default LogRoutes; +export const createLogRoutes = (logController: ILogController): Router => { + const router = Router(); + router.get("/", isAllowed(["admin", "superadmin"]), logController.getLogs); + return router; +}; diff --git a/server/src/api/routes/maintenanceWindowRoute.ts b/server/src/api/routes/maintenanceWindowRoute.ts deleted file mode 100644 index 43d03ff34..000000000 --- a/server/src/api/routes/maintenanceWindowRoute.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { IMaintenanceWindowController } from "@/api/controllers/maintenanceWindowController.js"; -import { Router } from "express"; - -class MaintenanceWindowRoutes { - private router: Router; - private mwController: IMaintenanceWindowController; - - constructor(maintenanceWindowController: IMaintenanceWindowController) { - this.router = Router(); - this.mwController = maintenanceWindowController; - this.initRoutes(); - } - initRoutes() { - this.router.post("/", this.mwController.createMaintenanceWindows); - this.router.get("/team/", this.mwController.getMaintenanceWindowsByTeamId); - - this.router.get("/monitor/:monitorId", this.mwController.getMaintenanceWindowsByMonitorId); - - this.router.get("/:id", this.mwController.getMaintenanceWindowById); - this.router.patch("/:id", this.mwController.editMaintenanceWindow); - this.router.delete("/:id", this.mwController.deleteMaintenanceWindow); - } - - getRouter() { - return this.router; - } -} - -export default MaintenanceWindowRoutes; diff --git a/server/src/api/routes/maintenanceWindowRoutes.ts b/server/src/api/routes/maintenanceWindowRoutes.ts new file mode 100644 index 000000000..22c3fd94a --- /dev/null +++ b/server/src/api/routes/maintenanceWindowRoutes.ts @@ -0,0 +1,15 @@ +import { IMaintenanceWindowController } from "@/api/controllers/maintenanceWindowController.js"; +import { Router } from "express"; + +export const createMaintenanceWindowRoutes = (maintenanceWindowController: IMaintenanceWindowController): Router => { + const router = Router(); + router.post("/", maintenanceWindowController.createMaintenanceWindows); + router.get("/team/", maintenanceWindowController.getMaintenanceWindowsByTeamId); + + router.get("/monitor/:monitorId", maintenanceWindowController.getMaintenanceWindowsByMonitorId); + + router.get("/:id", maintenanceWindowController.getMaintenanceWindowById); + router.patch("/:id", maintenanceWindowController.editMaintenanceWindow); + router.delete("/:id", maintenanceWindowController.deleteMaintenanceWindow); + return router; +}; diff --git a/server/src/api/routes/monitorRoute.ts b/server/src/api/routes/monitorRoute.ts deleted file mode 100644 index 29834ce70..000000000 --- a/server/src/api/routes/monitorRoute.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Router } from "express"; -import { isAllowed } from "@/api/middleware/isAllowed.js"; -import { IMonitorController } from "@/api/controllers/monitorController.js"; - -class MonitorRoutes { - private router: Router; - private monitorController: IMonitorController; - constructor(monitorController: IMonitorController) { - this.router = Router(); - this.monitorController = monitorController; - this.initRoutes(); - } - - initRoutes() { - // Team routes - this.router.get("/team", this.monitorController.getMonitorsByTeamId); - this.router.get("/team/with-checks", this.monitorController.getMonitorsWithChecksByTeamId); - this.router.get("/team/groups", this.monitorController.getGroupsByTeamId); - - // Uptime routes - this.router.get("/uptime/details/:monitorId", this.monitorController.getUptimeDetailsById); - - // Hardware routes - this.router.get("/hardware/details/:monitorId", this.monitorController.getHardwareDetailsById); - - // PageSpeed routes - this.router.get("/pagespeed/details/:monitorId", this.monitorController.getPageSpeedDetailsById); - - // Geo checks routes - this.router.get("/:monitorId/geo-checks", this.monitorController.getGeoChecksByMonitorId); - - // General monitor routes - this.router.post("/pause/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.pauseMonitor); - this.router.post("/bulk/pause", isAllowed(["admin", "superadmin"]), this.monitorController.bulkPauseMonitors); - - // Util routes - this.router.get("/certificate/:monitorId", (req, res, next) => { - this.monitorController.getMonitorCertificate(req, res, next); - }); - - // General monitor CRUD routes - this.router.patch("/notifications", isAllowed(["admin", "superadmin"]), this.monitorController.updateNotifications); - this.router.post("/", isAllowed(["admin", "superadmin"]), this.monitorController.createMonitor); - this.router.delete("/", isAllowed(["superadmin"]), this.monitorController.deleteAllMonitors); - - // Other static routes - this.router.post("/demo", isAllowed(["admin", "superadmin"]), this.monitorController.addDemoMonitors); - this.router.get("/export/json", isAllowed(["admin", "superadmin"]), this.monitorController.exportMonitorsToJSON); - this.router.post("/import/json", isAllowed(["admin", "superadmin"]), this.monitorController.importMonitorsFromJSON); - - this.router.get("/games", this.monitorController.getAllGames); - - // Individual monitor CRUD routes - this.router.get("/:monitorId", this.monitorController.getMonitorById); - this.router.patch("/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.editMonitor); - this.router.delete("/:monitorId", isAllowed(["admin", "superadmin"]), this.monitorController.deleteMonitor); - } - - getRouter() { - return this.router; - } -} - -export default MonitorRoutes; diff --git a/server/src/api/routes/monitorRoutes.ts b/server/src/api/routes/monitorRoutes.ts new file mode 100644 index 000000000..43a94b5c4 --- /dev/null +++ b/server/src/api/routes/monitorRoutes.ts @@ -0,0 +1,50 @@ +import { Router } from "express"; +import { isAllowed } from "@/api/middleware/isAllowed.js"; +import { IMonitorController } from "@/api/controllers/monitorController.js"; + +export const createMonitorRoutes = (monitorController: IMonitorController): Router => { + const router = Router(); + + // Team routes + router.get("/team", monitorController.getMonitorsByTeamId); + router.get("/team/with-checks", monitorController.getMonitorsWithChecksByTeamId); + + // Uptime routes + router.get("/uptime/details/:monitorId", monitorController.getUptimeDetailsById); + + // Hardware routes + router.get("/hardware/details/:monitorId", monitorController.getHardwareDetailsById); + + // PageSpeed routes + router.get("/pagespeed/details/:monitorId", monitorController.getPageSpeedDetailsById); + + // Geo checks routes + router.get("/:monitorId/geo-checks", monitorController.getGeoChecksByMonitorId); + + // General monitor routes + router.post("/pause/:monitorId", isAllowed(["admin", "superadmin"]), monitorController.pauseMonitor); + router.post("/bulk/pause", isAllowed(["admin", "superadmin"]), monitorController.bulkPauseMonitors); + + // Util routes + router.get("/certificate/:monitorId", (req, res, next) => { + monitorController.getMonitorCertificate(req, res, next); + }); + + // General monitor CRUD routes + router.patch("/notifications", isAllowed(["admin", "superadmin"]), monitorController.updateNotifications); + router.post("/", isAllowed(["admin", "superadmin"]), monitorController.createMonitor); + router.delete("/", isAllowed(["superadmin"]), monitorController.deleteAllMonitors); + + // Other static routes + router.post("/demo", isAllowed(["admin", "superadmin"]), monitorController.addDemoMonitors); + router.get("/export/json", isAllowed(["admin", "superadmin"]), monitorController.exportMonitorsToJSON); + router.post("/import/json", isAllowed(["admin", "superadmin"]), monitorController.importMonitorsFromJSON); + + router.get("/games", monitorController.getAllGames); + + // Individual monitor CRUD routes + router.get("/:monitorId", monitorController.getMonitorById); + router.patch("/:monitorId", isAllowed(["admin", "superadmin"]), monitorController.editMonitor); + router.delete("/:monitorId", isAllowed(["admin", "superadmin"]), monitorController.deleteMonitor); + return router; +}; diff --git a/server/src/api/routes/notificationRoute.ts b/server/src/api/routes/notificationRoute.ts deleted file mode 100644 index 16d9a7086..000000000 --- a/server/src/api/routes/notificationRoute.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { INotificationController } from "@/api/controllers/notificationController.js"; -import { Router } from "express"; -class NotificationRoutes { - private router: Router; - private notificationController: INotificationController; - - constructor(notificationController: INotificationController) { - this.router = Router(); - this.notificationController = notificationController; - this.initializeRoutes(); - } - - initializeRoutes() { - this.router.post("/", this.notificationController.createNotification); - - this.router.post("/test/all", this.notificationController.testAllNotifications); - this.router.post("/test", this.notificationController.testNotification); - - this.router.get("/team", this.notificationController.getNotificationsByTeamId); - - this.router.get("/:id", this.notificationController.getNotificationById); - this.router.delete("/:id", this.notificationController.deleteNotification); - this.router.patch("/:id", this.notificationController.editNotification); - } - - getRouter() { - return this.router; - } -} - -export default NotificationRoutes; diff --git a/server/src/api/routes/notificationRoutes.ts b/server/src/api/routes/notificationRoutes.ts new file mode 100644 index 000000000..32ea1923c --- /dev/null +++ b/server/src/api/routes/notificationRoutes.ts @@ -0,0 +1,14 @@ +import { INotificationController } from "@/api/controllers/notificationController.js"; +import { Router } from "express"; + +export const createNotificationRoutes = (notificationController: INotificationController): Router => { + const router = Router(); + router.post("/", notificationController.createNotification); + router.post("/test/all", notificationController.testAllNotifications); + router.post("/test", notificationController.testNotification); + router.get("/team", notificationController.getNotificationsByTeamId); + router.get("/:id", notificationController.getNotificationById); + router.delete("/:id", notificationController.deleteNotification); + router.patch("/:id", notificationController.editNotification); + return router; +}; diff --git a/server/src/api/routes/queueRoute.ts b/server/src/api/routes/queueRoute.ts deleted file mode 100644 index 0a9d41bb1..000000000 --- a/server/src/api/routes/queueRoute.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Router } from "express"; -import { isAllowed } from "@/api/middleware/isAllowed.js"; -import { IJobQueueController } from "@/api/controllers/queueController.js"; -class QueueRoutes { - private router: Router; - private queueController: IJobQueueController; - - constructor(queueController: IJobQueueController) { - this.router = Router(); - this.queueController = queueController; - this.initRoutes(); - } - initRoutes() { - this.router.get("/jobs", isAllowed(["admin", "superadmin"]), this.queueController.getJobs); - - this.router.get("/metrics", isAllowed(["admin", "superadmin"]), this.queueController.getMetrics); - this.router.get("/all-metrics", isAllowed(["admin", "superadmin"]), this.queueController.getAllMetrics); - this.router.post("/flush", isAllowed(["admin", "superadmin"]), this.queueController.flushQueue); - } - - getRouter() { - return this.router; - } -} - -export default QueueRoutes; diff --git a/server/src/api/routes/queueRoutes.ts b/server/src/api/routes/queueRoutes.ts new file mode 100644 index 000000000..9a0d2f2e5 --- /dev/null +++ b/server/src/api/routes/queueRoutes.ts @@ -0,0 +1,12 @@ +import { Router } from "express"; +import { isAllowed } from "@/api/middleware/isAllowed.js"; +import { IJobQueueController } from "@/api/controllers/queueController.js"; + +export const createQueueRoutes = (queueController: IJobQueueController): Router => { + const router = Router(); + router.get("/jobs", isAllowed(["admin", "superadmin"]), queueController.getJobs); + router.get("/metrics", isAllowed(["admin", "superadmin"]), queueController.getMetrics); + router.get("/all-metrics", isAllowed(["admin", "superadmin"]), queueController.getAllMetrics); + router.post("/flush", isAllowed(["admin", "superadmin"]), queueController.flushQueue); + return router; +}; diff --git a/server/src/api/routes/settingsRoute.ts b/server/src/api/routes/settingsRoute.ts deleted file mode 100644 index 3bb0e75fd..000000000 --- a/server/src/api/routes/settingsRoute.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { Router } from "express"; -import { isAllowed } from "../middleware/isAllowed.js"; -import { ISettingsController } from "@/api/controllers/settingsController.js"; - -class SettingsRoutes { - private router: Router; - private settingsController: ISettingsController; - - constructor(settingsController: ISettingsController) { - this.router = Router(); - this.settingsController = settingsController; - this.initRoutes(); - } - - initRoutes() { - this.router.get("/", this.settingsController.getAppSettings); - this.router.patch("/", isAllowed(["admin", "superadmin"]), this.settingsController.updateAppSettings); - this.router.post("/test-email", isAllowed(["admin", "superadmin"]), this.settingsController.sendTestEmail); - } - - getRouter() { - return this.router; - } -} - -export default SettingsRoutes; diff --git a/server/src/api/routes/settingsRoutes.ts b/server/src/api/routes/settingsRoutes.ts new file mode 100644 index 000000000..5b6e6c9a5 --- /dev/null +++ b/server/src/api/routes/settingsRoutes.ts @@ -0,0 +1,11 @@ +import { Router } from "express"; +import { isAllowed } from "../middleware/isAllowed.js"; +import { ISettingsController } from "@/api/controllers/settingsController.js"; + +export const createSettingsRoutes = (settingsController: ISettingsController): Router => { + const router = Router(); + router.get("/", settingsController.getAppSettings); + router.patch("/", isAllowed(["admin", "superadmin"]), settingsController.updateAppSettings); + router.post("/test-email", isAllowed(["admin", "superadmin"]), settingsController.sendTestEmail); + return router; +}; diff --git a/server/src/api/routes/statusPageRoute.ts b/server/src/api/routes/statusPageRoute.ts deleted file mode 100644 index 816c7212a..000000000 --- a/server/src/api/routes/statusPageRoute.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { IStatusPageController } from "@/api/controllers/statusPageController.js"; -import { RequestHandler, Router } from "express"; -import { isAllowed } from "@/api/middleware/isAllowed.js"; -import { imageUpload } from "@/api/middleware/upload.js"; - -class StatusPageRoutes { - private router: Router; - private statusPageController: IStatusPageController; - - constructor(statusPageController: IStatusPageController, verifyJWT: RequestHandler, verifyStatusPageAccess: RequestHandler) { - this.router = Router(); - this.statusPageController = statusPageController; - this.initRoutes(verifyJWT, verifyStatusPageAccess); - } - - initRoutes(verifyJWT: RequestHandler, verifyStatusPageAccess: RequestHandler) { - this.router.get("/team", verifyJWT, this.statusPageController.getStatusPagesByTeamId); - - this.router.post("/", imageUpload.single("logo"), verifyJWT, isAllowed(["admin", "superadmin"]), this.statusPageController.createStatusPage); - this.router.put("/:id", imageUpload.single("logo"), verifyJWT, isAllowed(["admin", "superadmin"]), this.statusPageController.updateStatusPage); - - this.router.get("/resolve", this.statusPageController.resolveStatusPageByDomain); - this.router.get("/:url", verifyStatusPageAccess, this.statusPageController.getStatusPageByUrl); - this.router.delete("/:id", verifyJWT, isAllowed(["admin", "superadmin"]), this.statusPageController.deleteStatusPage); - } - - getRouter() { - return this.router; - } -} - -export default StatusPageRoutes; diff --git a/server/src/api/routes/statusPageRoutes.ts b/server/src/api/routes/statusPageRoutes.ts new file mode 100644 index 000000000..b37ae5499 --- /dev/null +++ b/server/src/api/routes/statusPageRoutes.ts @@ -0,0 +1,19 @@ +import { IStatusPageController } from "@/api/controllers/statusPageController.js"; +import { RequestHandler, Router } from "express"; +import { isAllowed } from "@/api/middleware/isAllowed.js"; +import { imageUpload } from "@/api/middleware/upload.js"; + +export const createStatusPageRoutes = ( + statusPageController: IStatusPageController, + verifyJWT: RequestHandler, + verifyStatusPageAccess: RequestHandler +): Router => { + const router = Router(); + router.get("/team", verifyJWT, statusPageController.getStatusPagesByTeamId); + router.post("/", imageUpload.single("logo"), verifyJWT, isAllowed(["admin", "superadmin"]), statusPageController.createStatusPage); + router.put("/:id", imageUpload.single("logo"), verifyJWT, isAllowed(["admin", "superadmin"]), statusPageController.updateStatusPage); + router.get("/resolve", statusPageController.resolveStatusPageByDomain); + router.get("/:url", verifyStatusPageAccess, statusPageController.getStatusPageByUrl); + router.delete("/:id", verifyJWT, isAllowed(["admin", "superadmin"]), statusPageController.deleteStatusPage); + return router; +}; diff --git a/server/src/api/routes/tagRoutes.ts b/server/src/api/routes/tagRoutes.ts index c1a3f7972..d9632ce9c 100644 --- a/server/src/api/routes/tagRoutes.ts +++ b/server/src/api/routes/tagRoutes.ts @@ -1,28 +1,12 @@ import { ITagsController } from "@/api/controllers/tagController.js"; import { Router } from "express"; -class TagRoutes { - private router: Router; - private tagController: ITagsController; - constructor(tagController: ITagsController) { - this.router = Router(); - this.tagController = tagController; - this.initializeRoutes(); - } - - initializeRoutes() { - this.router.post("/", this.tagController.createTag); - - this.router.get("/team", this.tagController.getTagsByTeamId); - - this.router.get("/:id", this.tagController.getTagById); - this.router.delete("/:id", this.tagController.deleteTag); - this.router.patch("/:id", this.tagController.editTag); - } - - getRouter() { - return this.router; - } -} - -export default TagRoutes; +export const createTagRoutes = (tagController: ITagsController): Router => { + const router = Router(); + router.post("/", tagController.createTag); + router.get("/team", tagController.getTagsByTeamId); + router.get("/:id", tagController.getTagById); + router.delete("/:id", tagController.deleteTag); + router.patch("/:id", tagController.editTag); + return router; +}; diff --git a/server/src/api/validation/checkValidation.ts b/server/src/api/validation/checkValidation.ts index db0978357..e40d11024 100644 --- a/server/src/api/validation/checkValidation.ts +++ b/server/src/api/validation/checkValidation.ts @@ -1,7 +1,8 @@ import { z } from "zod"; import { booleanCoercion } from "./shared.js"; import { GeoContinents } from "@/domain/geo-checks/geo-check.type.js"; -import { MonitorTypes } from "@/domain/monitors/monitor.types.js"; +import { MonitorTypes } from "@/domain/monitors/monitor.type.js"; +import { CheckFilters, DateRanges, SortOrders } from "@/types/query.js"; //**************************************** // Check Validations @@ -13,10 +14,10 @@ export const getChecksParamValidation = z.object({ export const getChecksQueryValidation = z.object({ type: z.enum(MonitorTypes).optional(), - sortOrder: z.enum(["asc", "desc"]), + sortOrder: z.enum(SortOrders), limit: z.coerce.number().optional(), - dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]), - filter: z.enum(["all", "up", "down", "resolve"]).optional(), + dateRange: z.enum(DateRanges), + filter: z.enum(CheckFilters).optional(), ack: booleanCoercion.optional(), page: z.coerce.number(), rowsPerPage: z.coerce.number(), @@ -25,10 +26,10 @@ export const getChecksQueryValidation = z.object({ }); export const getTeamChecksQueryValidation = z.object({ - sortOrder: z.enum(["asc", "desc"]), + sortOrder: z.enum(SortOrders), limit: z.coerce.number().optional(), - dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]), - filter: z.enum(["all", "up", "down", "resolve"]).optional(), + dateRange: z.enum(DateRanges), + filter: z.enum(CheckFilters).optional(), ack: booleanCoercion.optional(), page: z.coerce.number(), rowsPerPage: z.coerce.number(), @@ -39,5 +40,5 @@ export const deleteChecksParamValidation = z.object({ }); export const getChecksSummaryByTeamIdQueryValidation = z.object({ - dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), + dateRange: z.enum(DateRanges).optional(), }); diff --git a/server/src/api/validation/incidentValidation.ts b/server/src/api/validation/incidentValidation.ts index 2047eefa5..322eaa2d3 100644 --- a/server/src/api/validation/incidentValidation.ts +++ b/server/src/api/validation/incidentValidation.ts @@ -1,18 +1,20 @@ import { z } from "zod"; import { booleanCoercion } from "./shared.js"; +import { DateRanges, SortOrders } from "@/types/query.js"; +import { IncidentResolutionTypes } from "@/domain/incidents/incident.type.js"; //**************************************** // Incident Validations //**************************************** export const getIncidentsByTeamQueryValidation = z.object({ - sortOrder: z.enum(["asc", "desc"]), - dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).default("all"), + sortOrder: z.enum(SortOrders), + dateRange: z.enum(DateRanges).default("all"), page: z.coerce.number().int().min(0), rowsPerPage: z.coerce.number().int().min(1), status: booleanCoercion.optional(), monitorId: z.string().optional(), - resolutionType: z.enum(["manual", "automatic"]).optional(), + resolutionType: z.enum(IncidentResolutionTypes).optional(), }); export const getIncidentSummaryQueryValidation = z.object({ diff --git a/server/src/api/validation/maintenanceWindowValidation.ts b/server/src/api/validation/maintenanceWindowValidation.ts index 2ee22bc19..c054d5b5d 100644 --- a/server/src/api/validation/maintenanceWindowValidation.ts +++ b/server/src/api/validation/maintenanceWindowValidation.ts @@ -1,6 +1,7 @@ import { z } from "zod"; import { booleanCoercion } from "./shared.js"; import { DurationUnits } from "@/domain/maintenance-windows/maintenance-window.type.js"; +import { SortOrders } from "@/types/query.js"; const dateToString = z.coerce.date().transform((d) => d.toISOString()); @@ -49,7 +50,7 @@ export const getMaintenanceWindowsByTeamIdQueryValidation = z.object({ page: z.coerce.number().optional(), rowsPerPage: z.coerce.number().optional(), field: z.string().optional(), - order: z.enum(["asc", "desc"]).optional(), + order: z.enum(SortOrders).optional(), }); export const getMaintenanceWindowsByMonitorIdParamValidation = z.object({ diff --git a/server/src/api/validation/monitorValidation.ts b/server/src/api/validation/monitorValidation.ts index e8bae9261..0e8b7b0bd 100644 --- a/server/src/api/validation/monitorValidation.ts +++ b/server/src/api/validation/monitorValidation.ts @@ -10,7 +10,8 @@ import { MonitorStatuses, MonitorTypes, PageSpeedStrategies, -} from "@/domain/monitors/monitor.types.js"; +} from "@/domain/monitors/monitor.type.js"; +import { DateRanges, SortOrders } from "@/types/query.js"; const httpStatusCode = z.number().refine((code) => HttpStatusCodeSet.has(code), { message: "Must be a valid HTTP status code" }); @@ -20,9 +21,9 @@ export const getMonitorByIdParamValidation = z.object({ export const getMonitorByIdQueryValidation = z.object({ status: booleanCoercion.optional(), - sortOrder: z.enum(["asc", "desc"]).optional(), + sortOrder: z.enum(SortOrders).optional(), limit: z.coerce.number().optional(), - dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), + dateRange: z.enum(DateRanges).optional(), numToDisplay: z.coerce.number().optional(), normalize: booleanCoercion.optional(), continent: z.union([z.enum(GeoContinents), z.array(z.enum(GeoContinents))]).optional(), @@ -42,7 +43,7 @@ export const getMonitorsWithChecksQueryValidation = z.object({ rowsPerPage: z.coerce.number().int().min(1).max(100).optional(), filter: z.union([z.string(), z.literal("")]).optional(), field: z.string().optional(), - order: z.enum(["asc", "desc"]).optional(), + order: z.enum(SortOrders).optional(), type: z.union([z.enum(MonitorTypes), z.array(z.enum(MonitorTypes))]).optional(), tags: z.union([z.string(), z.array(z.string())]).optional(), explain: booleanCoercion.optional(), @@ -195,8 +196,7 @@ export const getUptimeDetailsByIdParamValidation = z.object({ }); export const getUptimeDetailsByIdQueryValidation = z.object({ - dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]), - normalize: booleanCoercion.optional(), + dateRange: z.enum(DateRanges), }); const importedMonitorSchema = z @@ -206,7 +206,7 @@ const importedMonitorSchema = z teamId: z.string().optional(), name: z.string().min(1, "Name is required"), description: z.union([z.string(), z.literal("")]).optional(), - status: z.enum(["up", "down", "paused", "initializing", "maintenance", "breached"]).default("initializing"), + status: z.enum(MonitorStatuses).default("initializing"), statusWindow: z.array(z.boolean()).default([]), statusWindowSize: z.number().min(1).max(20).default(5), statusWindowThreshold: z.number().min(1).max(100).default(60), @@ -263,7 +263,7 @@ export const getHardwareDetailsByIdParamValidation = z.object({ }); export const getHardwareDetailsByIdQueryValidation = z.object({ - dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]).optional(), + dateRange: z.enum(DateRanges).optional(), }); // Canonical monitor shape returned by /monitors endpoints. Keep aligned with diff --git a/server/src/api/validation/statusPageValidation.ts b/server/src/api/validation/statusPageValidation.ts index a650129cd..ee2b7376c 100644 --- a/server/src/api/validation/statusPageValidation.ts +++ b/server/src/api/validation/statusPageValidation.ts @@ -3,6 +3,7 @@ import { booleanCoercion, dnsHostnameRegex } from "./shared.js"; import { StatusPageTypes, StatusPageThemes, StatusPageThemeModes } from "@/domain/status-pages/status-page.type.js"; import { normalizeStatusPageDomain } from "@/utils/statusPageDomain.js"; import { cssReferencesExternalResource } from "@/utils/customCss.js"; +import { ImageMimeTypes } from "@/types/upload.js"; //**************************************** // Status Page Validations @@ -67,7 +68,7 @@ export const imageValidation = z fieldname: z.string().min(1, "Field name is required"), originalname: z.string().min(1, "Original name is required"), encoding: z.string().min(1, "Encoding is required"), - mimetype: z.enum(["image/jpeg", "image/png", "image/jpg"], { + mimetype: z.enum(ImageMimeTypes, { message: "File must be a valid image (jpeg, jpg, or png)", }), size: z.number().max(3145728, "File size must be less than 3MB"), diff --git a/server/src/api/validation/userValidation.ts b/server/src/api/validation/userValidation.ts index 21fb55317..882c2d898 100644 --- a/server/src/api/validation/userValidation.ts +++ b/server/src/api/validation/userValidation.ts @@ -1,6 +1,6 @@ import { z } from "zod"; import { UserRoles } from "@/domain/users/user.type.js"; -import { nameValidation, lowercaseEmailValidation, passwordValidation } from "./shared.js"; +import { nameValidation, lowercaseEmailValidation, passwordValidation, booleanCoercion } from "@/api/validation/shared.js"; //**************************************** // User Validations @@ -46,6 +46,7 @@ export const editUserBodyValidation = z profilePicture: z.string().optional(), password: passwordValidation.optional(), newPassword: passwordValidation.optional(), + deleteProfileImage: booleanCoercion.optional(), }) // Changing a password requires both the current password and the new one. // The service only runs the change-password flow when both are present, so diff --git a/server/src/app.ts b/server/src/app.ts index 2ed1c2f01..12e552406 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -52,8 +52,16 @@ export const createApp = ({ app.use(createStatusPageDocumentCsp(allowedOrigin)); + // Client runtime config; registered before express.static so it shadows the + // fallback config.js in the client build output + const clientConfigScript = `window.__CHECKMATE_CONFIG__ = ${JSON.stringify(envSettings.clientConfig).replace(/ { + res.set("Cache-Control", "no-cache").type("application/javascript").send(clientConfigScript); + }); + app.use(express.static(frontendPath)); + app.use("/api/v1/monitors/import/json", express.json({ limit: "10mb" })); app.use(express.json()); app.use(cookieParser()); @@ -68,6 +76,7 @@ export const createApp = ({ directives: { upgradeInsecureRequests: null, "script-src": ["'self'", "'unsafe-inline'", "'unsafe-eval'"], + "img-src": ["'self'", "data:", "https://img.shields.io"], "object-src": ["'none'"], "base-uri": ["'self'"], }, diff --git a/server/src/config/envValidation.ts b/server/src/config/envValidation.ts index 05471e690..a769a28c7 100644 --- a/server/src/config/envValidation.ts +++ b/server/src/config/envValidation.ts @@ -1,5 +1,5 @@ import { z } from "zod"; -import { DbTypes, QueueModes } from "@/domain/app-settings/app-settings.type.js"; +import { DbTypes, LogLevels, QueueModes } from "@/domain/app-settings/app-settings.type.js"; import { booleanCoercion } from "@/api/validation/shared.js"; import { ILogger } from "@/utils/logger.js"; @@ -9,7 +9,7 @@ const envSchema = z.object({ // Port for worker health checks HEALTH_PORT: z.string().default("52346"), NODE_ENV: z.enum(["development", "production", "test"]).default("development"), - LOG_LEVEL: z.enum(["error", "warn", "info", "debug"]).default("debug"), + LOG_LEVEL: z.enum(LogLevels).default("debug"), // Database DB_CONNECTION_STRING: z.string().min(1, "Database connection string is required"), @@ -25,6 +25,12 @@ const envSchema = z.object({ // Client Configuration CLIENT_HOST: z.string().url("CLIENT_HOST must be a valid URL"), + // Client runtime config overrides, rendered into GET /config.js; unset keys are + // omitted and the client uses its same-origin defaults + CLIENT_CONFIG_API_BASE_URL: z.string().optional(), + CLIENT_CONFIG_CLIENT_HOST: z.string().url("CLIENT_CONFIG_CLIENT_HOST must be a valid URL").optional(), + CLIENT_CONFIG_LOG_LEVEL: z.enum(LogLevels).optional(), + // Optional ORIGIN: z.string().optional(), @@ -63,6 +69,15 @@ export const validateEnv = (logger: ILogger): ValidatedEnv => { process.exit(1); } + const legacyClientVars = Object.keys(process.env).filter((key) => key.startsWith("UPTIME_APP_")); + if (legacyClientVars.length > 0) { + logger.warn({ + message: `${legacyClientVars.join(", ")} no longer configure the client and will be ignored. The client defaults to the origin it is served from; to override, use CLIENT_CONFIG_API_BASE_URL, CLIENT_CONFIG_CLIENT_HOST, or CLIENT_CONFIG_LOG_LEVEL.`, + method: "validateEnv", + service: "Server", + }); + } + logger.info({ message: "Environment variables validated successfully", method: "validateEnv", diff --git a/server/src/config/routes.ts b/server/src/config/routes.ts index 855c18abb..810594354 100644 --- a/server/src/config/routes.ts +++ b/server/src/config/routes.ts @@ -5,52 +5,51 @@ import { authApiLimiter } from "../api/middleware/rateLimiter.js"; import type { InitializedControllers } from "./controllers.js"; import type { ApiServices } from "@/config/services.api.js"; -import AuthRoutes from "../api/routes/authRoute.js"; -import InviteRoutes from "../api/routes/inviteRoute.js"; -import MonitorRoutes from "../api/routes/monitorRoute.js"; -import CheckRoutes from "../api/routes/checkRoute.js"; -import GeoCheckRoutes from "../api/routes/geoCheckRoutes.js"; -import SettingsRoutes from "../api/routes/settingsRoute.js"; -import MaintenanceWindowRoutes from "../api/routes/maintenanceWindowRoute.js"; -import StatusPageRoutes from "../api/routes/statusPageRoute.js"; -import QueueRoutes from "../api/routes/queueRoute.js"; -import LogRoutes from "../api/routes/logRoutes.js"; -import DiagnosticRoutes from "../api/routes/diagnosticRoute.js"; -import NotificationRoutes from "../api/routes/notificationRoute.js"; -import TagRoutes from "../api/routes/tagRoutes.js"; - -import IncidentRoutes from "../api/routes/incidentRoute.js"; +import { createAuthRoutes } from "@/api/routes/authRoutes.js"; +import { createInviteRoutes } from "@/api/routes/inviteRoutes.js"; +import { createMonitorRoutes } from "@/api/routes/monitorRoutes.js"; +import { createCheckRoutes } from "@/api/routes/checkRoutes.js"; +import { createGeoCheckRoutes } from "@/api/routes/geoCheckRoutes.js"; +import { createSettingsRoutes } from "@/api/routes/settingsRoutes.js"; +import { createMaintenanceWindowRoutes } from "@/api/routes/maintenanceWindowRoutes.js"; +import { createQueueRoutes } from "@/api/routes/queueRoutes.js"; +import { createLogRoutes } from "@/api/routes/logRoutes.js"; +import { createStatusPageRoutes } from "@/api/routes/statusPageRoutes.js"; +import { createDiagnosticRoutes } from "@/api/routes/diagnosticRoutes.js"; +import { createNotificationRoutes } from "@/api/routes/notificationRoutes.js"; +import { createTagRoutes } from "@/api/routes/tagRoutes.js"; +import { createIncidentRoutes } from "@/api/routes/incidentRoutes.js"; export const setupRoutes = (app: Application, controllers: InitializedControllers, apiServices: ApiServices) => { const verifyJWT = createVerifyJWT(apiServices.settingsService); - const authRoutes = new AuthRoutes(controllers.authController, verifyJWT); - const monitorRoutes = new MonitorRoutes(controllers.monitorController); - const settingsRoutes = new SettingsRoutes(controllers.settingsController); - const checkRoutes = new CheckRoutes(controllers.checkController); - const geoCheckRoutes = new GeoCheckRoutes(controllers.geoCheckController); - const inviteRoutes = new InviteRoutes(controllers.inviteController, verifyJWT); - const maintenanceWindowRoutes = new MaintenanceWindowRoutes(controllers.maintenanceWindowController); - const queueRoutes = new QueueRoutes(controllers.queueController); - const logRoutes = new LogRoutes(controllers.logController); + const authRoutes = createAuthRoutes(controllers.authController, verifyJWT); + const monitorRoutes = createMonitorRoutes(controllers.monitorController); + const settingsRoutes = createSettingsRoutes(controllers.settingsController); + const checkRoutes = createCheckRoutes(controllers.checkController); + const geoCheckRoutes = createGeoCheckRoutes(controllers.geoCheckController); + const inviteRoutes = createInviteRoutes(controllers.inviteController, verifyJWT); + const maintenanceWindowRoutes = createMaintenanceWindowRoutes(controllers.maintenanceWindowController); + const queueRoutes = createQueueRoutes(controllers.queueController); + const logRoutes = createLogRoutes(controllers.logController); const verifyStatusPageAccess = createVerifyStatusPageAccess(apiServices.statusPagesRepository, verifyJWT); - const statusPageRoutes = new StatusPageRoutes(controllers.statusPageController, verifyJWT, verifyStatusPageAccess); - const notificationRoutes = new NotificationRoutes(controllers.notificationController); - const tagRoutes = new TagRoutes(controllers.tagController); - const diagnosticRoutes = new DiagnosticRoutes(controllers.diagnosticController, verifyJWT); - const incidentRoutes = new IncidentRoutes(controllers.incidentController); + const statusPageRoutes = createStatusPageRoutes(controllers.statusPageController, verifyJWT, verifyStatusPageAccess); + const notificationRoutes = createNotificationRoutes(controllers.notificationController); + const tagRoutes = createTagRoutes(controllers.tagController); + const diagnosticRoutes = createDiagnosticRoutes(controllers.diagnosticController, verifyJWT); + const incidentRoutes = createIncidentRoutes(controllers.incidentController); - app.use("/api/v1/auth", authApiLimiter, authRoutes.getRouter()); - app.use("/api/v1/monitors", verifyJWT, monitorRoutes.getRouter()); - app.use("/api/v1/settings", verifyJWT, settingsRoutes.getRouter()); - app.use("/api/v1/checks", verifyJWT, checkRoutes.getRouter()); - app.use("/api/v1/geo-checks", verifyJWT, geoCheckRoutes.getRouter()); - app.use("/api/v1/invite", inviteRoutes.getRouter()); - app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRoutes.getRouter()); - app.use("/api/v1/queue", verifyJWT, queueRoutes.getRouter()); - app.use("/api/v1/logs", verifyJWT, logRoutes.getRouter()); - app.use("/api/v1/status-page", statusPageRoutes.getRouter()); - app.use("/api/v1/notifications", verifyJWT, notificationRoutes.getRouter()); - app.use("/api/v1/tags", verifyJWT, tagRoutes.getRouter()); - app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes.getRouter()); - app.use("/api/v1/incidents", verifyJWT, incidentRoutes.getRouter()); + app.use("/api/v1/auth", authApiLimiter, authRoutes); + app.use("/api/v1/monitors", verifyJWT, monitorRoutes); + app.use("/api/v1/settings", verifyJWT, settingsRoutes); + app.use("/api/v1/checks", verifyJWT, checkRoutes); + app.use("/api/v1/geo-checks", verifyJWT, geoCheckRoutes); + app.use("/api/v1/invite", inviteRoutes); + app.use("/api/v1/maintenance-window", verifyJWT, maintenanceWindowRoutes); + app.use("/api/v1/queue", verifyJWT, queueRoutes); + app.use("/api/v1/logs", verifyJWT, logRoutes); + app.use("/api/v1/status-page", statusPageRoutes); + app.use("/api/v1/notifications", verifyJWT, notificationRoutes); + app.use("/api/v1/tags", verifyJWT, tagRoutes); + app.use("/api/v1/diagnostic", verifyJWT, diagnosticRoutes); + app.use("/api/v1/incidents", verifyJWT, incidentRoutes); }; diff --git a/server/src/config/services.shared.ts b/server/src/config/services.shared.ts index 49b73f52e..0f7813d8a 100644 --- a/server/src/config/services.shared.ts +++ b/server/src/config/services.shared.ts @@ -21,6 +21,7 @@ import { GlobalPingService } from "@/service/globalPingService.js"; import { ILogger } from "@/utils/logger.js"; // Notification providers +import type { NotificationProviderRegistry } from "@/domain/notifications/notification.service.js"; import { WebhookProvider } from "@/domain/notifications/providers/webhook.js"; import { SlackProvider } from "@/domain/notifications/providers/slack.js"; import { EmailProvider } from "@/domain/notifications/providers/email.js"; @@ -125,7 +126,7 @@ export const buildShared = async ({ const checksRepository = new MongoChecksRepository(logger); const incidentsRepository = new MongoIncidentsRepository(); const usersRepository = new MongoUsersRepository(); - const geoChecksRepository = new MongoGeoChecksRepository(logger); + const geoChecksRepository = new MongoGeoChecksRepository(); const notificationsRepository = new MongoNotificationsRepository(); const jobsRepository = new MongoJobsRepository(workerId); const queueWorkersRepository = new MongoQueueWorkersRepository(); @@ -167,24 +168,28 @@ export const buildShared = async ({ const twilioProvider = new TwilioProvider(logger); const ntfyProvider = new NtfyProvider(logger); - const notificationsService = new NotificationsService( + const notificationProviders: NotificationProviderRegistry = { + webhook: webhookProvider, + email: emailProvider, + slack: slackProvider, + discord: discordProvider, + pager_duty: pagerDutyProvider, + matrix: matrixProvider, + teams: teamsProvider, + telegram: telegramProvider, + pushover: pushoverProvider, + twilio: twilioProvider, + ntfy: ntfyProvider, + }; + + const notificationsService = new NotificationsService({ notificationsRepository, monitorsRepository, - webhookProvider, - emailProvider, - slackProvider, - discordProvider, - pagerDutyProvider, - matrixProvider, - teamsProvider, - telegramProvider, - pushoverProvider, - twilioProvider, - ntfyProvider, + providers: notificationProviders, settingsService, logger, - notificationMessageBuilder - ); + notificationMessageBuilder, + }); const sharedServices: SharedServices = { logger, diff --git a/server/src/config/services.worker.ts b/server/src/config/services.worker.ts index 44997af5f..b0bdff2d6 100644 --- a/server/src/config/services.worker.ts +++ b/server/src/config/services.worker.ts @@ -131,9 +131,9 @@ export const buildWorker = async (shared: SharedServices, envSettings: EnvConfig geoChecksRepository ); - const worker = await DBQueueWorker.create( + const worker = await DBQueueWorker.create({ logger, - () => mongoose.connection.readyState === 1, + isDbConnected: () => mongoose.connection.readyState === 1, jobsRepository, monitorsRepository, checksRepository, @@ -142,13 +142,13 @@ export const buildWorker = async (shared: SharedServices, envSettings: EnvConfig checkProducer, checkEvaluator, geoCheckPipeline, - reactorDispatcher, - workerHelper, + dispatcher: reactorDispatcher, + helper: workerHelper, queueWorkersRepository, - envSettings.queueMode, - envSettings.queuePrimaryProcesses, - workerId - ); + queueMode: envSettings.queueMode, + queuePrimaryProcesses: envSettings.queuePrimaryProcesses, + workerId, + }); return { worker, networkService, bufferService, statusService }; }; diff --git a/server/src/db/db.mongo.ts b/server/src/db/db.mongo.ts index 5aca5550a..e0b39c188 100755 --- a/server/src/db/db.mongo.ts +++ b/server/src/db/db.mongo.ts @@ -21,10 +21,6 @@ class MongoDB implements IDb { this.db = null; } - get serviceName() { - return MongoDB.SERVICE_NAME; - } - connect = async () => { try { const connectionString = this.envSettings.dbConnectionString || "mongodb://localhost:27017/uptime_db"; diff --git a/server/src/db/migration/0009_recomputeResponseTimeFromTimings.ts b/server/src/db/migration/0009_recomputeResponseTimeFromTimings.ts new file mode 100644 index 000000000..e65812530 --- /dev/null +++ b/server/src/db/migration/0009_recomputeResponseTimeFromTimings.ts @@ -0,0 +1,209 @@ +import mongoose from "mongoose"; +import type { Document } from "mongodb"; +import type { ILogger } from "@/utils/logger.js"; + +const SERVICE_NAME = "Migration:RecomputeResponseTimeFromTimings"; +const CHECKS_COLLECTION = "checks"; +const BACKUP_COLLECTION = "checks_backup_response_time"; +const FAILED_DOCS_COLLECTION = "checks_response_time_migration_failed"; +const BATCH_SIZE = 1000; +const NAMESPACE_EXISTS_CODE = 48; + +interface MigrationStats { + totalSource: number; + totalMigrated: number; + totalFailed: number; +} + +const getDb = () => { + const db = mongoose.connection.db; + if (!db) { + throw new Error("Database connection is not initialized"); + } + return db; +}; + +// `checks` is a time-series collection (0002): measurement fields cannot be updated in place, +// so recomputing responseTime requires a backup โ†’ drop โ†’ recreate โ†’ re-insert rewrite. +const createTimeSeriesCollection = async () => { + const db = getDb(); + const existing = await db.listCollections({ name: CHECKS_COLLECTION }).toArray(); + if (existing.length === 0) { + try { + await db.createCollection(CHECKS_COLLECTION, { + timeseries: { + timeField: "createdAt", + metaField: "metadata", + granularity: "seconds", + }, + }); + } catch (error) { + // Mongoose's autoCreate can create `checks` concurrently with this migration + if ((error as { code?: number })?.code !== NAMESPACE_EXISTS_CODE) { + throw error; + } + } + } + + await db + .collection(CHECKS_COLLECTION) + .createIndexes([ + { key: { createdAt: 1 } }, + { key: { "metadata.monitorId": 1, createdAt: 1 } }, + { key: { "metadata.monitorId": 1, createdAt: -1 } }, + { key: { "metadata.teamId": 1, createdAt: -1 } }, + { key: { "metadata.monitorId": 1, "metadata.type": 1, createdAt: -1 } }, + { key: { "metadata.teamId": 1, status: 1, createdAt: -1 } }, + ]); +}; + +const stripId = (doc: Document): Document => { + const { _id, ...rest } = doc; + void _id; + return rest; +}; + +const recomputeResponseTime = (doc: Document): Document => { + const rest = stripId(doc); + const total = doc.timings?.phases?.total; + if (typeof total === "number") { + rest.responseTime = total; + } + return rest; +}; + +const storeFailedDocument = async (doc: Document, reason: string) => { + const db = getDb(); + await db.collection(FAILED_DOCS_COLLECTION).insertOne({ + originalDoc: doc, + reason, + failedAt: new Date(), + }); +}; + +const streamBackupIntoChecks = async (transform: (doc: Document) => Document, collectFailures: boolean): Promise => { + const db = getDb(); + const source = db.collection(BACKUP_COLLECTION); + const target = db.collection(CHECKS_COLLECTION); + + const stats: MigrationStats = { + totalSource: await source.countDocuments(), + totalMigrated: 0, + totalFailed: 0, + }; + + const cursor = source.find(); + // Derive the op type from the exact bulkWrite we call so it matches whichever `mongodb` copy + // Mongoose resolves (see 0002 for the nested-node_modules background) + type BulkOp = Parameters[0][number]; + const operations: BulkOp[] = []; + const invalidDocs: Document[] = []; + + while (await cursor.hasNext()) { + const doc = await cursor.next(); + if (!doc) { + continue; + } + + // Time-series inserts reject documents without the timeField + if (!doc.createdAt) { + invalidDocs.push(doc); + continue; + } + + operations.push({ insertOne: { document: transform(doc) } }); + + if (operations.length >= BATCH_SIZE) { + const result = await target.bulkWrite(operations, { ordered: false }); + stats.totalMigrated += result.insertedCount; + operations.length = 0; + } + } + + if (operations.length) { + const result = await target.bulkWrite(operations, { ordered: false }); + stats.totalMigrated += result.insertedCount; + } + + if (collectFailures) { + for (const doc of invalidDocs) { + await storeFailedDocument(doc, "Missing createdAt (required timeField)"); + stats.totalFailed++; + } + } else { + stats.totalFailed = invalidDocs.length; + } + + await cursor.close(); + + return stats; +}; + +// Restore cannot be a simple rename like 0002's: $out produced a standard collection, and +// renaming it to `checks` would silently lose the time-series type. Rebuild and re-insert instead. +const restoreFromBackup = async () => { + const db = getDb(); + const backupExists = await db.listCollections({ name: BACKUP_COLLECTION }).toArray(); + if (backupExists.length === 0) { + throw new Error("Cannot restore: backup collection does not exist"); + } + + const checksExists = await db.listCollections({ name: CHECKS_COLLECTION }).toArray(); + if (checksExists.length > 0) { + await db.collection(CHECKS_COLLECTION).drop(); + } + + await createTimeSeriesCollection(); + await streamBackupIntoChecks(stripId, false); +}; + +export async function recomputeResponseTimeFromTimings(logger: ILogger): Promise { + const db = getDb(); + + const checksExists = await db.listCollections({ name: CHECKS_COLLECTION }).toArray(); + if (checksExists.length === 0) { + logger.info({ service: SERVICE_NAME, message: "No checks collection, nothing to migrate" }); + return; + } + + // Fresh installs and non-HTTP-only deployments have no stored timings โ€” skip the rewrite entirely + const needsMigration = await db.collection(CHECKS_COLLECTION).countDocuments({ "timings.phases.total": { $exists: true } }, { limit: 1 }); + if (needsMigration === 0) { + logger.info({ service: SERVICE_NAME, message: "No checks with timings.phases.total, nothing to migrate" }); + return; + } + + const backupExists = await db.listCollections({ name: BACKUP_COLLECTION }).toArray(); + if (backupExists.length > 0) { + throw new Error(`Backup collection "${BACKUP_COLLECTION}" already exists. Please remove it manually before running migration.`); + } + + await db + .collection(CHECKS_COLLECTION) + .aggregate([{ $match: {} }, { $out: BACKUP_COLLECTION }]) + .toArray(); + await db.collection(CHECKS_COLLECTION).drop(); + + try { + await createTimeSeriesCollection(); + const stats = await streamBackupIntoChecks(recomputeResponseTime, true); + logger.info({ + service: SERVICE_NAME, + message: + `Migrated ${stats.totalMigrated}/${stats.totalSource} checks, ${stats.totalFailed} failed ` + + `(failures stored in ${FAILED_DOCS_COLLECTION}); backup retained in ${BACKUP_COLLECTION}`, + }); + } catch (error) { + try { + await restoreFromBackup(); + logger.error({ service: SERVICE_NAME, message: "Migration failed, checks restored from backup" }); + } catch (restoreError) { + const restoreMessage = restoreError instanceof Error ? restoreError.message : String(restoreError); + logger.error({ + service: SERVICE_NAME, + message: `Migration failed AND rollback failed (${restoreMessage}) โ€” original data is preserved in ${BACKUP_COLLECTION}`, + }); + } + throw error; + } +} diff --git a/server/src/db/migration/index.ts b/server/src/db/migration/index.ts index 0b0745d19..f59af32b7 100644 --- a/server/src/db/migration/index.ts +++ b/server/src/db/migration/index.ts @@ -7,6 +7,7 @@ import { migrateStatusPageTypeToArray } from "./0005_migrateStatusPageTypeToArra import { cleanupDuplicateMonitorStatsForUniqueIndex } from "./0006_cleanupDuplicateMonitorStatsForUniqueIndex.js"; import { migrateMaintenanceWindowMonitorIdToArray } from "./0007_migrateMaintenanceWindowMonitorIdToArray.js"; import { backfillMonitorLastEvaluatedAt } from "./0008_backfillMonitorLastEvaluatedAt.js"; +import { recomputeResponseTimeFromTimings } from "./0009_recomputeResponseTimeFromTimings.js"; import type { ILogger } from "@/utils/logger.js"; type MigrationEntry = { @@ -23,6 +24,7 @@ const migrations: MigrationEntry[] = [ { name: "0006_cleanupDuplicateMonitorStatsForUniqueIndex", execute: cleanupDuplicateMonitorStatsForUniqueIndex }, { name: "0007_migrateMaintenanceWindowMonitorIdToArray", execute: migrateMaintenanceWindowMonitorIdToArray }, { name: "0008_backfillMonitorLastEvaluatedAt", execute: backfillMonitorLastEvaluatedAt }, + { name: "0009_recomputeResponseTimeFromTimings", execute: recomputeResponseTimeFromTimings }, ]; const runMigrations = async (logger: ILogger) => { diff --git a/server/src/domain/app-settings/app-settings.repository.mongo.ts b/server/src/domain/app-settings/app-settings.repository.mongo.ts index ab242e4b7..d88141ce5 100644 --- a/server/src/domain/app-settings/app-settings.repository.mongo.ts +++ b/server/src/domain/app-settings/app-settings.repository.mongo.ts @@ -1,26 +1,13 @@ -import mongoose, { type UpdateQuery } from "mongoose"; +import { type UpdateQuery } from "mongoose"; import { ISettingsRepository } from "@/domain/app-settings/app-settings-repository.interface.js"; import type { Settings, SettingsUpdate } from "@/domain/app-settings/app-settings.type.js"; import { AppSettingsModel, type AppSettingsDocument } from "@/domain/app-settings/app-settings.model.js"; +import { toStringId, toDateString } from "@/utils/mongoMappers.js"; class MongoSettingsRepository implements ISettingsRepository { - private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { - if (!value) { - return ""; - } - return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); - }; - - private toDateString = (value?: Date | string | null): string => { - if (!value) { - return new Date(0).toISOString(); - } - return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); - }; - - protected toEntity = (doc: AppSettingsDocument): Settings => { + private toEntity = (doc: AppSettingsDocument): Settings => { return { - id: this.toStringId(doc._id), + id: toStringId(doc._id), checkTTL: doc.checkTTL, language: doc.language, jwtSecret: doc.jwtSecret ?? undefined, @@ -41,8 +28,8 @@ class MongoSettingsRepository implements ISettingsRepository { singleton: doc.singleton, version: doc.version ?? 1, globalThresholds: doc.globalThresholds ?? undefined, - createdAt: this.toDateString(doc.createdAt), - updatedAt: this.toDateString(doc.updatedAt), + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), }; }; diff --git a/server/src/domain/app-settings/app-settings.service.ts b/server/src/domain/app-settings/app-settings.service.ts index 8beb65d84..2df2b7512 100755 --- a/server/src/domain/app-settings/app-settings.service.ts +++ b/server/src/domain/app-settings/app-settings.service.ts @@ -1,5 +1,5 @@ import { ISettingsRepository } from "@/domain/app-settings/app-settings-repository.interface.js"; -import { Settings, SettingsUpdate, type DbType, type QueueMode } from "@/domain/app-settings/app-settings.type.js"; +import { Settings, SettingsUpdate, type ClientRuntimeConfig, type DbType, type QueueMode } from "@/domain/app-settings/app-settings.type.js"; import { AppError } from "@/utils/AppError.js"; import { ValidatedEnv } from "@/config/envValidation.js"; import type { StringValue } from "ms"; @@ -16,11 +16,10 @@ export type EnvConfig = { queueMode: QueueMode; queuePrimaryProcesses: boolean; statusPageThemesEnabled: boolean; + clientConfig: ClientRuntimeConfig; }; export interface ISettingsService { - readonly serviceName: string; - loadSettings(): EnvConfig; getSettings(): EnvConfig; areStatusPageThemesEnabled(): boolean; getDBSettings(): Promise; @@ -44,6 +43,11 @@ export class SettingsService implements ISettingsService { queueMode: env.QUEUE_MODE, queuePrimaryProcesses: env.QUEUE_PRIMARY_PROCESSES, statusPageThemesEnabled: env.STATUS_PAGE_THEMES_ENABLED, + clientConfig: { + ...(env.CLIENT_CONFIG_API_BASE_URL && { apiBaseUrl: env.CLIENT_CONFIG_API_BASE_URL }), + ...(env.CLIENT_CONFIG_CLIENT_HOST && { clientHost: env.CLIENT_CONFIG_CLIENT_HOST }), + ...(env.CLIENT_CONFIG_LOG_LEVEL && { logLevel: env.CLIENT_CONFIG_LOG_LEVEL }), + }, }; } @@ -51,14 +55,6 @@ export class SettingsService implements ISettingsService { this.settingsRepository = settingsRepository; } - get serviceName() { - return SettingsService.SERVICE_NAME; - } - - loadSettings() { - return this.settings; - } - getSettings() { return this.settings; } diff --git a/server/src/domain/app-settings/app-settings.type.ts b/server/src/domain/app-settings/app-settings.type.ts index 8cb52e93d..8233ced63 100644 --- a/server/src/domain/app-settings/app-settings.type.ts +++ b/server/src/domain/app-settings/app-settings.type.ts @@ -4,6 +4,17 @@ export type DbType = (typeof DbTypes)[number]; export const QueueModes = ["primary", "worker"] as const; export type QueueMode = (typeof QueueModes)[number]; +export const LogLevels = ["error", "warn", "info", "debug"] as const; +export type LogLevel = (typeof LogLevels)[number]; + +// Rendered into GET /config.js as window.__CHECKMATE_CONFIG__; keys left unset +// fall back to the client's same-origin defaults. +export type ClientRuntimeConfig = { + apiBaseUrl?: string; + clientHost?: string; + logLevel?: LogLevel; +}; + export interface SettingsThresholds { cpu?: number; memory?: number; diff --git a/server/src/domain/checks/check.hardware.aggregations.ts b/server/src/domain/checks/check.hardware.aggregations.ts new file mode 100644 index 000000000..6c672b630 --- /dev/null +++ b/server/src/domain/checks/check.hardware.aggregations.ts @@ -0,0 +1,150 @@ +import { CheckModel } from "@/domain/checks/check.model.js"; +import mongoose from "mongoose"; +import type { HardwareDiskStats, HardwareNetStats } from "@/domain/checks/check.type.js"; + +type DateRange = { start: Date; end: Date }; +type HardwareUpChecks = { totalChecks: number }; + +// $avg yields null when no values are present, so metrics are nullable until the repository applies fallbacks +type NullableValues = { [K in keyof T]: T[K] | null }; + +export type HardwareStatsBucket = { + _id: string; + avgCpuUsage: number | null; + avgMemoryUsage: number | null; + avgTemperature: number[]; + disks: NullableValues[]; + net: NullableValues[]; +}; + +const NET_RATE_FIELDS = [ + ["bytesSentPerSecond", "bytes_sent"], + ["deltaBytesRecv", "bytes_recv"], + ["deltaPacketsSent", "packets_sent"], + ["deltaPacketsRecv", "packets_recv"], + ["deltaErrIn", "err_in"], + ["deltaErrOut", "err_out"], + ["deltaDropIn", "drop_in"], + ["deltaDropOut", "drop_out"], +] as const; + +const netRatePerSecond = (sourceField: string) => ({ + $let: { + vars: { + tDiff: { + $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000], + }, + f: { + $arrayElemAt: [ + { + $map: { + input: { $first: "$net" }, + as: "i", + in: `$$i.${sourceField}`, + }, + }, + "$$nIdx", + ], + }, + l: { + $arrayElemAt: [ + { + $map: { + input: { $last: "$net" }, + as: "i", + in: `$$i.${sourceField}`, + }, + }, + "$$nIdx", + ], + }, + }, + in: { + $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0], + }, + }, +}); + +export const getHardwareTotalChecks = async (monitorId: string, dates: DateRange): Promise => { + return await CheckModel.countDocuments({ + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + "metadata.type": "hardware", + createdAt: { $gte: dates.start, $lte: dates.end }, + }); +}; + +export const getHardwareUpChecks = async (monitorId: string, dates: DateRange): Promise => { + const count = await CheckModel.countDocuments({ + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + "metadata.type": "hardware", + createdAt: { $gte: dates.start, $lte: dates.end }, + status: true, + }); + return { totalChecks: count }; +}; + +export const getHardwareStats = async (monitorId: string, dates: DateRange, dateString: string): Promise => { + const netProjection = { + name: { $arrayElemAt: ["$sampleDoc.net.name", "$$nIdx"] }, + ...Object.fromEntries(NET_RATE_FIELDS.map(([outputKey, sourceField]) => [outputKey, netRatePerSecond(sourceField)])), + }; + + return await CheckModel.aggregate([ + { + $match: { + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + "metadata.type": "hardware", + createdAt: { $gte: dates.start, $lte: dates.end }, + }, + }, + { $sort: { createdAt: 1 } }, + { + $group: { + _id: { $dateToString: { format: dateString, date: "$createdAt" } }, + avgCpuUsage: { $avg: "$cpu.usage_percent" }, + avgMemoryUsage: { $avg: "$memory.usage_percent" }, + avgTemperatures: { $push: { $ifNull: ["$cpu.temperature", [0]] } }, + disks: { $push: "$disk" }, + net: { $push: "$net" }, + createdAts: { $push: "$createdAt" }, + sampleDoc: { $first: "$$ROOT" }, + }, + }, + { + $project: { + _id: 1, + avgCpuUsage: 1, + avgMemoryUsage: 1, + avgTemperature: { + $map: { + input: { $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$avgTemperatures", 0] }, [0]] } }] }, + as: "idx", + in: { $avg: { $map: { input: "$avgTemperatures", as: "t", in: { $arrayElemAt: ["$$t", "$$idx"] } } } }, + }, + }, + disks: { + $map: { + input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.disk", []] } }] }, + as: "dIdx", + in: { + name: { $concat: ["disk", { $toString: "$$dIdx" }] }, + readSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.read_bytes", "$$dIdx"] } } } }, + writeSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.write_bytes", "$$dIdx"] } } } }, + totalBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.total_bytes", "$$dIdx"] } } } }, + freeBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.free_bytes", "$$dIdx"] } } } }, + usagePercent: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.usage_percent", "$$dIdx"] } } } }, + }, + }, + }, + net: { + $map: { + input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.net", []] } }] }, + as: "nIdx", + in: netProjection, + }, + }, + }, + }, + { $sort: { _id: 1 } }, + ]); +}; diff --git a/server/src/domain/checks/check.model.ts b/server/src/domain/checks/check.model.ts index c3fbc23ff..9f3c2fbc1 100644 --- a/server/src/domain/checks/check.model.ts +++ b/server/src/domain/checks/check.model.ts @@ -1,5 +1,5 @@ import { Schema, model, Types } from "mongoose"; -import { MonitorTypes, type MonitorType } from "@/domain/monitors/monitor.types.js"; +import { MonitorTypes, type MonitorType } from "@/domain/monitors/monitor.type.js"; import type { Check, CheckAudits, diff --git a/server/src/domain/checks/check.repository.interface.ts b/server/src/domain/checks/check.repository.interface.ts index 95c0ade80..e6b7c6368 100644 --- a/server/src/domain/checks/check.repository.interface.ts +++ b/server/src/domain/checks/check.repository.interface.ts @@ -6,8 +6,9 @@ import type { PageSpeedChecksResult, UptimeChecksResult, } from "@/domain/checks/check.type.js"; -import type { MonitorType } from "@/domain/monitors/monitor.types.js"; +import type { MonitorType } from "@/domain/monitors/monitor.type.js"; import type { LatestChecksMap } from "@/domain/checks/check.repository.mongo.js"; +import { CheckFilter, DateRange } from "@/types/query.js"; export interface IChecksRepository { // create @@ -19,29 +20,27 @@ export interface IChecksRepository { findByMonitorId( monitorId: string, sortOrder: string, - dateRange: string, - filter: string | undefined, + dateRange: DateRange, page: number, rowsPerPage: number, - status: boolean | undefined + status: boolean | undefined, + filter?: CheckFilter ): Promise; findByTeamId( sortOrder: string, - dateRange: string, - filter: string | undefined, + dateRange: DateRange, page: number, rowsPerPage: number, - teamId: string + teamId: string, + filter?: CheckFilter ): Promise; findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise; findByDateRangeAndMonitorId( monitorId: string, - startDate: Date, - endDate: Date, - dateString: string, + dateRange: DateRange, options?: { type?: MonitorType } ): Promise; - findSummaryByTeamId(teamId: string, dateRange: string): Promise; + findSummaryByTeamId(teamId: string, dateRange: DateRange): Promise; findUnevaluatedByMonitorId(monitorId: string, since: number): Promise; // update //delete diff --git a/server/src/domain/checks/check.repository.mongo.ts b/server/src/domain/checks/check.repository.mongo.ts index 4f9ad6f4c..fcc967828 100644 --- a/server/src/domain/checks/check.repository.mongo.ts +++ b/server/src/domain/checks/check.repository.mongo.ts @@ -11,18 +11,23 @@ import type { CheckMetadata, CheckNetworkInterfaceInfo, GotTimings, + HardwareCheckStats, } from "@/domain/checks/check.type.js"; -import type { MonitorType } from "@/domain/monitors/monitor.types.js"; +import type { MonitorType } from "@/domain/monitors/monitor.type.js"; import { CheckModel, type CheckDocument } from "@/domain/checks/check.model.js"; import mongoose from "mongoose"; -import { getDateForRange } from "@/utils/dataUtils.js"; +import { getDateFormat, getDateForRange } from "@/utils/dataUtils.js"; import { ILogger } from "@/utils/logger.js"; +import { toStringId, toDateString } from "@/utils/mongoMappers.js"; + +import { getHardwareUpChecks, getHardwareStats, getHardwareTotalChecks } from "@/domain/checks/check.hardware.aggregations.js"; +import { CheckFilter, DateRange } from "@/types/query.js"; +import { AppError } from "@/utils/AppError.js"; +import { NETWORK_ERROR } from "@/types/network.js"; const SERVICE_NAME = "StatusService"; export type LatestChecksMap = Record; -type DateRange = { start: Date; end: Date }; -type HardwareUpChecks = { totalChecks: number }; class MongoChecksRepository implements IChecksRepository { static SERVICE_NAME = SERVICE_NAME; @@ -33,20 +38,6 @@ class MongoChecksRepository implements IChecksRepository { } private toEntity = (doc: CheckDocument): Check => { - const toStringId = (value: mongoose.Types.ObjectId | string | undefined | null): string => { - if (!value) { - return ""; - } - return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); - }; - - const toDateString = (value?: Date | string | null): string => { - if (!value) { - return new Date(0).toISOString(); - } - return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); - }; - const mapTimings = (timings?: GotTimings): GotTimings => { const phases = timings?.phases ?? { wait: 0, @@ -193,7 +184,12 @@ class MongoChecksRepository implements IChecksRepository { // Map id to _id for MongoDB storage const { id, metadata, ...rest } = check; if (!metadata || !metadata.monitorId || !metadata.teamId) { - throw new Error(`Check must have valid metadata with monitorId and teamId. Got: ${JSON.stringify({ id, metadata })}`); + throw new AppError({ + message: `Check must have valid metadata with monitorId and teamId. Got: ${JSON.stringify({ id, metadata })}`, + status: 500, + service: SERVICE_NAME, + method: "toDocument", + }); } return { _id: id ? new mongoose.Types.ObjectId(id) : new mongoose.Types.ObjectId(), @@ -217,50 +213,41 @@ class MongoChecksRepository implements IChecksRepository { return this.mapDocuments(inserted as unknown as CheckDocument[]); }; + private filterToMatch = (filter: CheckFilter | undefined): Record => { + switch (filter) { + case "up": + return { status: true }; + case "down": + return { status: false }; + case "resolve": + return { status: false, statusCode: NETWORK_ERROR }; + default: + this.logger.warn({ + message: "invalid filter", + service: SERVICE_NAME, + method: "filterToMatch", + }); + return {}; + } + }; + findByMonitorId = async ( monitorId: string, sortOrder: string, - dateRange: string, - filter: string | undefined, + dateRange: DateRange, page: number, rowsPerPage: number, - status: boolean | undefined + status: boolean | undefined, + filter?: CheckFilter ) => { // Match const matchStage: Record = { "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), ...(typeof status !== "undefined" && { status }), - ...(getDateForRange(dateRange) && { - createdAt: { - $gte: getDateForRange(dateRange), - }, - }), + createdAt: { $gte: getDateForRange(dateRange) }, + ...this.filterToMatch(filter), }; - if (filter !== undefined) { - switch (filter) { - case "all": - break; - case "up": - matchStage.status = true; - break; - case "down": - matchStage.status = false; - break; - case "resolve": - matchStage.status = false; - matchStage.statusCode = 5000; - break; - default: - this.logger.warn({ - message: "invalid filter", - service: SERVICE_NAME, - method: "getChecks", - }); - break; - } - } - //Sort const convertedSortOrder = sortOrder === "asc" ? 1 : -1; @@ -278,39 +265,12 @@ class MongoChecksRepository implements IChecksRepository { return { checksCount, checks: this.mapDocuments(checks) }; }; - findByTeamId = async (sortOrder: string, dateRange: string, filter: string, page: number, rowsPerPage: number, teamId: string) => { + findByTeamId = async (sortOrder: string, dateRange: DateRange, page: number, rowsPerPage: number, teamId: string, filter?: CheckFilter) => { const matchStage: Record = { "metadata.teamId": new mongoose.Types.ObjectId(teamId), - ...(getDateForRange(dateRange) && { - createdAt: { - $gte: getDateForRange(dateRange), - }, - }), + createdAt: { $gte: getDateForRange(dateRange) }, + ...this.filterToMatch(filter), }; - // Add filter to match stage - if (filter !== undefined) { - switch (filter) { - case "all": - break; - case "up": - matchStage.status = true; - break; - case "down": - matchStage.status = false; - break; - case "resolve": - matchStage.status = false; - matchStage.statusCode = 5000; - break; - default: - this.logger.warn({ - message: "invalid filter", - service: SERVICE_NAME, - method: "getChecksByTeam", - }); - break; - } - } const parsedSortOrder = sortOrder === "asc" ? 1 : -1; @@ -354,25 +314,25 @@ class MongoChecksRepository implements IChecksRepository { return mapped; }; - findByDateRangeAndMonitorId = async (monitorId: string, startDate: Date, endDate: Date, dateString: string, options?: { type?: MonitorType }) => { + findByDateRangeAndMonitorId = async (monitorId: string, dateRange: DateRange, options?: { type?: MonitorType }) => { const monitorObjectId = new mongoose.Types.ObjectId(monitorId); + const start = getDateForRange(dateRange); + const dateString = getDateFormat(dateRange); + + const end = new Date(); if (options?.type === "hardware") { - return this.findHardwareDateRangeChecks(monitorObjectId, startDate, endDate, dateString); + return this.findHardwareDateRangeChecks(monitorObjectId, start, end, dateString); } if (options?.type === "pagespeed") { - return this.findPageSpeedDateRangeChecks(monitorObjectId, startDate, endDate, dateString); + return this.findPageSpeedDateRangeChecks(monitorObjectId, start, end, dateString); } - return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, startDate, endDate, dateString); + return this.findUptimeDateRangeChecks(options?.type ?? "http", monitorObjectId, start, end, dateString); }; - findSummaryByTeamId = async (teamId: string, dateRange: string) => { + findSummaryByTeamId = async (teamId: string, dateRange: DateRange) => { const baseMatch = { "metadata.teamId": new mongoose.Types.ObjectId(teamId), - ...(getDateForRange(dateRange) && { - createdAt: { - $gte: getDateForRange(dateRange), - }, - }), + createdAt: { $gte: getDateForRange(dateRange) }, }; const [totalResult, downResult] = await Promise.all([ @@ -485,11 +445,30 @@ class MongoChecksRepository implements IChecksRepository { $dateToString: { format: dateString, date: "$createdAt" }, }, avgResponseTime: { $avg: "$responseTime" }, + avgDns: { $avg: { $ifNull: ["$timings.phases.dns", 0] } }, + avgTcp: { $avg: { $ifNull: ["$timings.phases.tcp", 0] } }, + avgTls: { $avg: { $ifNull: ["$timings.phases.tls", 0] } }, + avgRequest: { $avg: { $ifNull: ["$timings.phases.request", 0] } }, + avgFirstByte: { $avg: { $ifNull: ["$timings.phases.firstByte", 0] } }, + avgDownload: { $avg: { $ifNull: ["$timings.phases.download", 0] } }, totalChecks: { $sum: 1 }, }, }, { $sort: { _id: 1 } }, - { $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } }, + { + $project: { + bucketDate: "$_id", + avgResponseTime: 1, + totalChecks: 1, + avgDns: 1, + avgTcp: 1, + avgTls: 1, + avgRequest: 1, + avgFirstByte: 1, + avgDownload: 1, + _id: 0, + }, + }, ], groupedUpChecks: [ { $match: { status: true } }, @@ -540,9 +519,9 @@ class MongoChecksRepository implements IChecksRepository { const monitorId = monitorObjectId.toHexString(); const dates = { start: startDate, end: endDate }; const [aggregateDataDoc, upChecksDoc, hardwareMetrics] = await Promise.all([ - this.getHardwareTotalChecks(monitorId, dates), - this.getHardwareUpChecks(monitorId, dates), - this.getHardwareStats(monitorId, dates, dateString), + getHardwareTotalChecks(monitorId, dates), + getHardwareUpChecks(monitorId, dates), + getHardwareStats(monitorId, dates, dateString), ]); const aggregateData = { @@ -553,33 +532,35 @@ class MongoChecksRepository implements IChecksRepository { totalChecks: upChecksDoc?.totalChecks ?? 0, }; - const checks = (hardwareMetrics ?? []).map((metric) => ({ - bucketDate: metric._id, - avgCpuUsage: metric.avgCpuUsage ?? 0, - avgMemoryUsage: metric.avgMemoryUsage ?? 0, - avgTemperature: metric.avgTemperature ?? [], - disks: (metric.disks ?? []).map((disk: { [key: string]: number | string | undefined }) => ({ - name: disk?.name ?? "", - readSpeed: disk?.readSpeed ?? 0, - writeSpeed: disk?.writeSpeed ?? 0, - totalBytes: disk?.totalBytes ?? 0, - freeBytes: disk?.freeBytes ?? 0, - usagePercent: disk?.usagePercent ?? 0, - })), - net: (metric.net ?? []).map((iface: { [key: string]: number | string | undefined }) => ({ - name: iface?.name ?? "", - bytesSentPerSecond: iface?.bytesSentPerSecond ?? 0, - deltaBytesRecv: iface?.deltaBytesRecv ?? 0, - deltaPacketsSent: iface?.deltaPacketsSent ?? 0, - deltaPacketsRecv: iface?.deltaPacketsRecv ?? 0, - deltaErrIn: iface?.deltaErrIn ?? 0, - deltaErrOut: iface?.deltaErrOut ?? 0, - deltaDropIn: iface?.deltaDropIn ?? 0, - deltaDropOut: iface?.deltaDropOut ?? 0, - deltaFifoIn: iface?.deltaFifoIn ?? 0, - deltaFifoOut: iface?.deltaFifoOut ?? 0, - })), - })); + const checks = (hardwareMetrics ?? []).map( + (metric): HardwareCheckStats => ({ + bucketDate: metric._id, + avgCpuUsage: metric.avgCpuUsage ?? 0, + avgMemoryUsage: metric.avgMemoryUsage ?? 0, + avgTemperature: metric.avgTemperature ?? [], + disks: (metric.disks ?? []).map((disk) => ({ + name: disk?.name ?? "", + readSpeed: disk?.readSpeed ?? 0, + writeSpeed: disk?.writeSpeed ?? 0, + totalBytes: disk?.totalBytes ?? 0, + freeBytes: disk?.freeBytes ?? 0, + usagePercent: disk?.usagePercent ?? 0, + })), + net: (metric.net ?? []).map((iface) => ({ + name: iface?.name ?? "", + bytesSentPerSecond: iface?.bytesSentPerSecond ?? 0, + deltaBytesRecv: iface?.deltaBytesRecv ?? 0, + deltaPacketsSent: iface?.deltaPacketsSent ?? 0, + deltaPacketsRecv: iface?.deltaPacketsRecv ?? 0, + deltaErrIn: iface?.deltaErrIn ?? 0, + deltaErrOut: iface?.deltaErrOut ?? 0, + deltaDropIn: iface?.deltaDropIn ?? 0, + deltaDropOut: iface?.deltaDropOut ?? 0, + deltaFifoIn: iface?.deltaFifoIn ?? 0, + deltaFifoOut: iface?.deltaFifoOut ?? 0, + })), + }) + ); return { monitorType: "hardware" as const, @@ -635,167 +616,6 @@ class MongoChecksRepository implements IChecksRepository { groupedChecks: result?.groupedChecks ?? [], }; }; - - private getHardwareTotalChecks = async (monitorId: string, dates: DateRange): Promise => { - return await CheckModel.countDocuments({ - "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), - "metadata.type": "hardware", - createdAt: { $gte: dates.start, $lte: dates.end }, - }); - }; - - private getHardwareUpChecks = async (monitorId: string, dates: DateRange): Promise => { - const count = await CheckModel.countDocuments({ - "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), - "metadata.type": "hardware", - createdAt: { $gte: dates.start, $lte: dates.end }, - status: true, - }); - return { totalChecks: count }; - }; - - private getHardwareStats = async (monitorId: string, dates: DateRange, dateString: string) => { - return await CheckModel.aggregate([ - { - $match: { - "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), - "metadata.type": "hardware", - createdAt: { $gte: dates.start, $lte: dates.end }, - }, - }, - { $sort: { createdAt: 1 } }, - { - $group: { - _id: { $dateToString: { format: dateString, date: "$createdAt" } }, - avgCpuUsage: { $avg: "$cpu.usage_percent" }, - avgMemoryUsage: { $avg: "$memory.usage_percent" }, - avgTemperatures: { $push: { $ifNull: ["$cpu.temperature", [0]] } }, - disks: { $push: "$disk" }, - net: { $push: "$net" }, - createdAts: { $push: "$createdAt" }, - sampleDoc: { $first: "$$ROOT" }, - }, - }, - { - $project: { - _id: 1, - avgCpuUsage: 1, - avgMemoryUsage: 1, - avgTemperature: { - $map: { - input: { $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$avgTemperatures", 0] }, [0]] } }] }, - as: "idx", - in: { $avg: { $map: { input: "$avgTemperatures", as: "t", in: { $arrayElemAt: ["$$t", "$$idx"] } } } }, - }, - }, - disks: { - $map: { - input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.disk", []] } }] }, - as: "dIdx", - in: { - name: { $concat: ["disk", { $toString: "$$dIdx" }] }, - readSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.read_bytes", "$$dIdx"] } } } }, - writeSpeed: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.write_bytes", "$$dIdx"] } } } }, - totalBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.total_bytes", "$$dIdx"] } } } }, - freeBytes: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.free_bytes", "$$dIdx"] } } } }, - usagePercent: { $avg: { $map: { input: "$disks", as: "dA", in: { $arrayElemAt: ["$$dA.usage_percent", "$$dIdx"] } } } }, - }, - }, - }, - net: { - $map: { - input: { $range: [0, { $size: { $ifNull: ["$sampleDoc.net", []] } }] }, - as: "nIdx", - in: { - name: { $arrayElemAt: ["$sampleDoc.net.name", "$$nIdx"] }, - bytesSentPerSecond: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_sent" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaBytesRecv: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.bytes_recv" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaPacketsSent: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_sent" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaPacketsRecv: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.packets_recv" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaErrIn: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_in" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaErrOut: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.err_out" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaDropIn: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_in" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - deltaDropOut: { - $let: { - vars: { - tDiff: { $divide: [{ $subtract: [{ $last: "$createdAts" }, { $first: "$createdAts" }] }, 1000] }, - f: { $arrayElemAt: [{ $map: { input: { $first: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] }, - l: { $arrayElemAt: [{ $map: { input: { $last: "$net" }, as: "i", in: "$$i.drop_out" } }, "$$nIdx"] }, - }, - in: { $cond: [{ $gt: ["$$tDiff", 0] }, { $divide: [{ $subtract: ["$$l", "$$f"] }, "$$tDiff"] }, 0] }, - }, - }, - }, - }, - }, - }, - }, - { $sort: { _id: 1 } }, - ]); - }; } export default MongoChecksRepository; diff --git a/server/src/domain/checks/check.service.ts b/server/src/domain/checks/check.service.ts index ca7fca457..40dd88f80 100644 --- a/server/src/domain/checks/check.service.ts +++ b/server/src/domain/checks/check.service.ts @@ -6,6 +6,7 @@ import type { MonitorPayloadMap, MonitorStatusResponse } from "@/types/network.j import type { HardwareStatusPayload, PageSpeedStatusPayload } from "@/types/network.js"; import { AppError } from "@/utils/AppError.js"; import { ILogger } from "@/utils/logger.js"; +import { CheckFilter, DateRange } from "@/types/query.js"; const SERVICE_NAME = "checkService"; @@ -18,21 +19,21 @@ export interface ICheckService { monitorId: string; teamId: string; sortOrder: string; - dateRange: string; + dateRange: DateRange; page: number; rowsPerPage: number; - filter?: string; + filter?: CheckFilter; status?: boolean; }): Promise; getChecksByTeam(params: { teamId: string; sortOrder: string; - dateRange: string; + dateRange: DateRange; page: number; rowsPerPage: number; - filter?: string; + filter?: CheckFilter; }): Promise; - getChecksSummaryByTeamId(params: { teamId: string; dateRange: string }): Promise; + getChecksSummaryByTeamId(params: { teamId: string; dateRange: DateRange }): Promise; deleteChecks(params: { monitorId: string; teamId: string }): Promise; deleteChecksByTeamId(params: { teamId: string }): Promise; deleteOlderThan(date: Date): Promise; @@ -50,10 +51,6 @@ export class CheckService implements ICheckService { this.checksRepository = checksRepository; } - get serviceName() { - return CheckService.SERVICE_NAME; - } - createChecks = async (checks: Check[]) => { return this.checksRepository.createChecks(checks); }; @@ -168,11 +165,11 @@ export class CheckService implements ICheckService { monitorId: string; teamId: string; sortOrder: string; - dateRange: string; + dateRange: DateRange; page: number; rowsPerPage: number; status?: boolean; - filter?: string; + filter: CheckFilter; }) => { if (!monitorId) { throw new AppError({ message: "No monitor ID in request", service: SERVICE_NAME, method: "getChecksByMonitor", status: 400 }); @@ -187,7 +184,7 @@ export class CheckService implements ICheckService { const parsedPage = page ?? 0; const parsedRowsPerPage = rowsPerPage ?? 5; - const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, status); + const result = await this.checksRepository.findByMonitorId(monitorId, sortOrder, dateRange, parsedPage, parsedRowsPerPage, status, filter); return result; }; @@ -196,25 +193,25 @@ export class CheckService implements ICheckService { teamId, sortOrder, dateRange, - filter, page, rowsPerPage, + filter, }: { teamId: string; sortOrder: string; - dateRange: string; + dateRange: DateRange; page: number; rowsPerPage: number; - filter?: string; + filter?: CheckFilter; }) => { const parsedPage = page ?? 0; const parsedRowsPerPage = rowsPerPage ?? 5; - const checkData = await this.checksRepository.findByTeamId(sortOrder, dateRange, filter, parsedPage, parsedRowsPerPage, teamId); + const checkData = await this.checksRepository.findByTeamId(sortOrder, dateRange, parsedPage, parsedRowsPerPage, teamId, filter); return checkData; }; - getChecksSummaryByTeamId = async ({ teamId, dateRange }: { teamId: string; dateRange: string }) => { + getChecksSummaryByTeamId = async ({ teamId, dateRange }: { teamId: string; dateRange: DateRange }) => { const summary = await this.checksRepository.findSummaryByTeamId(teamId, dateRange); return summary; }; diff --git a/server/src/domain/checks/check.type.ts b/server/src/domain/checks/check.type.ts index fe733e4e9..d4c47cfb9 100644 --- a/server/src/domain/checks/check.type.ts +++ b/server/src/domain/checks/check.type.ts @@ -1,4 +1,4 @@ -import type { MonitorType } from "@/domain/monitors/monitor.types.js"; +import type { MonitorType } from "@/domain/monitors/monitor.type.js"; import type { Response } from "got"; export const CHECK_TTL_SENTINEL = 366; @@ -126,49 +126,21 @@ export interface PageSpeedChecksResult { groupedChecks: PageSpeedGroupedCheck[]; } -export interface HardwareChecksResult { - monitorType: "hardware"; - aggregateData: { - totalChecks: number; - }; - upChecks: { - totalChecks: number; - }; - checks: Array<{ - bucketDate: string; - avgCpuUsage: number; - avgMemoryUsage: number; - avgTemperature: number[]; - disks: Array<{ - name: string; - readSpeed: number; - writeSpeed: number; - totalBytes: number; - freeBytes: number; - usagePercent: number; - }>; - net: Array<{ - name: string; - bytesSentPerSecond: number; - deltaBytesRecv: number; - deltaPacketsSent: number; - deltaPacketsRecv: number; - deltaErrIn: number; - deltaErrOut: number; - deltaDropIn: number; - deltaDropOut: number; - deltaFifoIn: number; - deltaFifoOut: number; - }>; - }>; -} - export interface GroupedCheck { bucketDate: string; avgResponseTime: number; totalChecks: number; } +export interface GroupedUptimeCheck extends GroupedCheck { + avgDns: number; + avgTcp: number; + avgTls: number; + avgRequest: number; + avgFirstByte: number; + avgDownload: number; +} + export interface PageSpeedGroupedCheck { bucketDate: string; performance: number; @@ -180,7 +152,7 @@ export interface PageSpeedGroupedCheck { export interface UptimeChecksResult { monitorType: Exclude; - groupedChecks: GroupedCheck[]; + groupedChecks: GroupedUptimeCheck[]; groupedUpChecks: GroupedCheck[]; groupedDownChecks: GroupedCheck[]; uptimePercentage: number; @@ -200,8 +172,50 @@ export type NormalizedCheck = T & { originalResponseTime: number; }; -export type NormalizedUptimeCheck = T & { - originalAvgResponseTime: number; -}; - export type CheckSnapshot = Omit; + +export interface HardwareDiskStats { + name: string; + readSpeed: number; + writeSpeed: number; + totalBytes: number; + freeBytes: number; + usagePercent: number; +} + +export interface HardwareNetStats { + name: string; + bytesSentPerSecond: number; + deltaBytesRecv: number; + deltaPacketsSent: number; + deltaPacketsRecv: number; + deltaErrIn: number; + deltaErrOut: number; + deltaDropIn: number; + deltaDropOut: number; + deltaFifoIn: number; + deltaFifoOut: number; +} + +export interface HardwareCheckStats { + bucketDate: string; + avgCpuUsage: number; + avgMemoryUsage: number; + avgTemperature: number[]; + disks: HardwareDiskStats[]; + net: HardwareNetStats[]; +} + +export interface HardwareStats { + aggregateData: { + totalChecks: number; + }; + upChecks: { + totalChecks: number; + }; + checks: HardwareCheckStats[]; +} + +export interface HardwareChecksResult extends HardwareStats { + monitorType: "hardware"; +} diff --git a/server/src/domain/diagnostics/diagnostic.service.ts b/server/src/domain/diagnostics/diagnostic.service.ts index f26fa02e3..1f7a0314a 100644 --- a/server/src/domain/diagnostics/diagnostic.service.ts +++ b/server/src/domain/diagnostics/diagnostic.service.ts @@ -79,10 +79,6 @@ export class DiagnosticService implements IDiagnosticService { obs.observe({ entryTypes: ["measure"] }); } - get serviceName() { - return DiagnosticService.SERVICE_NAME; - } - private getCPUUsagePercentage = async (): Promise => { const startUsage = process.cpuUsage(); const timingPeriod = 1000; // measured in ms diff --git a/server/src/domain/dlq/dlq.repository.mongo.ts b/server/src/domain/dlq/dlq.repository.mongo.ts index 4cf7bad85..63fec34c5 100644 --- a/server/src/domain/dlq/dlq.repository.mongo.ts +++ b/server/src/domain/dlq/dlq.repository.mongo.ts @@ -4,40 +4,26 @@ import type { DLQItem } from "@/domain/dlq/dlq.type.js"; import type { IDLQRepository, DLQQueryFilters, DLQStatusCount } from "@/domain/dlq/dlq.repository.interface.js"; import mongoose from "mongoose"; import { AppError } from "@/utils/AppError.js"; - +import { toStringId, toDateString } from "@/utils/mongoMappers.js"; class MongoDLQRepository implements IDLQRepository { - private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { - if (!value) { - return ""; - } - return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); - }; - - private toDateString = (value?: Date | string | null): string => { - if (!value) { - return new Date(0).toISOString(); - } - return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); - }; - - protected toEntity = (doc: DLQItemDocument): DLQItem => { + private toEntity = (doc: DLQItemDocument): DLQItem => { return { - id: this.toStringId(doc._id), + id: toStringId(doc._id), type: doc.type, status: doc.status, payload: doc.payload, - monitorId: this.toStringId(doc.monitorId), - teamId: this.toStringId(doc.teamId), + monitorId: toStringId(doc.monitorId), + teamId: toStringId(doc.teamId), retryCount: doc.retryCount, maxRetries: doc.maxRetries, lastError: doc.lastError, - nextRetryAt: this.toDateString(doc.nextRetryAt), - createdAt: this.toDateString(doc.createdAt), - updatedAt: this.toDateString(doc.updatedAt), + nextRetryAt: toDateString(doc.nextRetryAt), + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), }; }; - protected mapDocuments = (documents: DLQItemDocument[] | DLQItemDocument | null): DLQItem[] => { + private mapDocuments = (documents: DLQItemDocument[]) => { if (!documents) { return []; } diff --git a/server/src/domain/geo-checks/geo-check.model.ts b/server/src/domain/geo-checks/geo-check.model.ts index 0db895d0a..512979ea5 100644 --- a/server/src/domain/geo-checks/geo-check.model.ts +++ b/server/src/domain/geo-checks/geo-check.model.ts @@ -1,5 +1,5 @@ import { Schema, model, Types } from "mongoose"; -import { MonitorTypes, type MonitorType } from "@/domain/monitors/monitor.types.js"; +import { MonitorTypes, type MonitorType } from "@/domain/monitors/monitor.type.js"; import type { GeoCheck, GeoCheckLocation, GeoCheckMetadata, GeoCheckResult, GeoCheckTimings } from "@/domain/geo-checks/geo-check.type.js"; type GeoCheckMetadataDocument = Omit & { diff --git a/server/src/domain/geo-checks/geo-check.repository.interface.ts b/server/src/domain/geo-checks/geo-check.repository.interface.ts index 2e6f33e33..a9a81f57c 100644 --- a/server/src/domain/geo-checks/geo-check.repository.interface.ts +++ b/server/src/domain/geo-checks/geo-check.repository.interface.ts @@ -1,5 +1,6 @@ import type { GeoCheck, GroupedGeoCheck } from "@/domain/geo-checks/geo-check.type.js"; import type { GeoContinent, FlatGeoCheck } from "@/domain/geo-checks/geo-check.type.js"; +import { DateRange } from "@/types/query.js"; export interface GeoChecksQueryResult { geoChecksCount: number; @@ -16,19 +17,12 @@ export interface IGeoChecksRepository { findByMonitorId( monitorId: string, sortOrder: string, - dateRange: string, + dateRange: DateRange, page: number, rowsPerPage: number, continents?: GeoContinent[] ): Promise; - findByMonitorIdAndDateRange(monitorId: string, startDate: Date, endDate: Date): Promise; - findGroupedByMonitorIdAndDateRange( - monitorId: string, - startDate: Date, - endDate: Date, - dateFormat: string, - continents?: GeoContinent[] - ): Promise; + findGroupedByMonitorIdAndDateRange(monitorId: string, DateRange: DateRange, continents?: GeoContinent[]): Promise; deleteByMonitorId(monitorId: string): Promise; deleteByTeamId(teamId: string): Promise; deleteByMonitorIdsNotIn(monitorIds: string[]): Promise; diff --git a/server/src/domain/geo-checks/geo-check.repository.mongo.ts b/server/src/domain/geo-checks/geo-check.repository.mongo.ts index 426fb533d..a56699750 100644 --- a/server/src/domain/geo-checks/geo-check.repository.mongo.ts +++ b/server/src/domain/geo-checks/geo-check.repository.mongo.ts @@ -3,34 +3,16 @@ import type { GeoCheck, GeoCheckMetadata, GeoCheckResult, GroupedGeoCheck, GeoCo import type { FlatGeoChecksQueryResult } from "./geo-check.repository.interface.js"; import { GeoCheckMetadataDocument, GeoCheckModel, type GeoCheckDocument } from "@/domain/geo-checks/geo-check.model.js"; import mongoose, { PipelineStage } from "mongoose"; -import { getDateForRange } from "@/utils/dataUtils.js"; -import { ILogger } from "@/utils/logger.js"; +import { getDateForRange, getDateFormat } from "@/utils/dataUtils.js"; +import { toStringId, toDateString } from "@/utils/mongoMappers.js"; +import { DateRange } from "@/types/query.js"; const SERVICE_NAME = "GeoChecksRepository"; class MongoGeoChecksRepository implements IGeoChecksRepository { static SERVICE_NAME = SERVICE_NAME; - private logger: ILogger; - constructor(logger: ILogger) { - this.logger = logger; - } - private toEntity = (doc: GeoCheckDocument): GeoCheck => { - const toStringId = (value: mongoose.Types.ObjectId | string | undefined | null): string => { - if (!value) { - return ""; - } - return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); - }; - - const toDateString = (value?: Date | string | null): string => { - if (!value) { - return new Date(0).toISOString(); - } - return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); - }; - const mapMetadata = (metadata: GeoCheckMetadataDocument): GeoCheckMetadata => ({ monitorId: toStringId(metadata.monitorId), teamId: toStringId(metadata.teamId), @@ -76,304 +58,214 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { }; createGeoChecks = async (geoChecks: Omit[]): Promise => { - try { - const docs = await GeoCheckModel.insertMany( - geoChecks.map((geoCheck) => ({ - metadata: { - monitorId: new mongoose.Types.ObjectId(geoCheck.metadata.monitorId), - teamId: new mongoose.Types.ObjectId(geoCheck.metadata.teamId), - type: geoCheck.metadata.type, - }, - results: geoCheck.results, - expiry: new Date(geoCheck.expiry), - })) - ); - return docs.map((doc) => this.toEntity(doc)); - } catch (error: unknown) { - this.logger.error({ - message: error instanceof Error ? `Failed to createGeoChecks: ${error.message}` : "Failed to createGeoChecks", - service: SERVICE_NAME, - method: "createGeoChecks", - stack: error instanceof Error ? error.stack : undefined, - }); - throw error; - } + const docs = await GeoCheckModel.insertMany( + geoChecks.map((geoCheck) => ({ + metadata: { + monitorId: new mongoose.Types.ObjectId(geoCheck.metadata.monitorId), + teamId: new mongoose.Types.ObjectId(geoCheck.metadata.teamId), + type: geoCheck.metadata.type, + }, + results: geoCheck.results, + expiry: new Date(geoCheck.expiry), + })) + ); + return docs.map((doc) => this.toEntity(doc)); }; findByMonitorId = async ( monitorId: string, sortOrder: string, - dateRange: string, + dateRange: DateRange, page: number, rowsPerPage: number, continents?: GeoContinent[] ): Promise => { - try { - const matchStage: Record = { - "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), - ...(getDateForRange(dateRange) && { + const matchStage: Record = { + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + createdAt: { $gte: getDateForRange(dateRange) }, + }; + + const convertedSortOrder = sortOrder === "asc" ? 1 : -1; + + let skip = 0; + if (page && rowsPerPage) { + skip = page * rowsPerPage; + } + + // Common pipeline stages for both paths + const pipeline: PipelineStage[] = [ + { $match: matchStage }, + { $unwind: "$results" }, + // Filter by continent if specified + ...(continents && continents.length > 0 + ? [ + { + $match: { + "results.location.continent": { $in: continents }, + }, + }, + ] + : []), + // Project to flat structure + { + $project: { + _id: 0, + monitorId: "$metadata.monitorId", + teamId: "$metadata.teamId", + type: "$metadata.type", + location: "$results.location", + status: "$results.status", + statusCode: "$results.statusCode", + timings: "$results.timings", + createdAt: 1, + updatedAt: 1, + }, + }, + { $sort: { createdAt: convertedSortOrder } }, + { $skip: skip }, + { $limit: rowsPerPage }, + ]; + + // Count pipeline + const countPipeline: PipelineStage[] = [ + { $match: matchStage }, + { $unwind: "$results" }, + ...(continents && continents.length > 0 + ? [ + { + $match: { + "results.location.continent": { $in: continents }, + }, + }, + ] + : []), + { $count: "count" }, + ]; + + const [countResult, dataResults] = await Promise.all([GeoCheckModel.aggregate(countPipeline), GeoCheckModel.aggregate(pipeline)]); + + const geoChecksCount = countResult[0]?.count || 0; + const geoChecks: FlatGeoCheck[] = dataResults.map((doc) => ({ + id: `${doc.monitorId.toString()}-${new Date(doc.createdAt).getTime()}-${doc.location.continent}-${doc.location.city}-${Math.random().toString(36).substring(2, 15)}`, + monitorId: doc.monitorId.toString(), + teamId: doc.teamId.toString(), + type: doc.type, + location: doc.location, + status: doc.status, + statusCode: doc.statusCode, + timings: doc.timings, + createdAt: new Date(doc.createdAt).toISOString(), + updatedAt: new Date(doc.updatedAt).toISOString(), + })); + + return { geoChecksCount, geoChecks }; + }; + + findGroupedByMonitorIdAndDateRange = async (monitorId: string, dateRange: DateRange, continents?: GeoContinent[]): Promise => { + const start = getDateForRange(dateRange); + const dateString = getDateFormat(dateRange); + + const end = new Date(); + const pipeline: PipelineStage[] = [ + // Match geo checks for this monitor in date range + { + $match: { + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), createdAt: { - $gte: getDateForRange(dateRange), + $gte: start, + $lte: end, }, - }), - }; - - const convertedSortOrder = sortOrder === "asc" ? 1 : -1; - - let skip = 0; - if (page && rowsPerPage) { - skip = page * rowsPerPage; - } - - // Common pipeline stages for both paths - const pipeline: PipelineStage[] = [ - { $match: matchStage }, - { $unwind: "$results" }, - // Filter by continent if specified - ...(continents && continents.length > 0 - ? [ + }, + }, + // Unwind the results array to process each location separately + { + $unwind: "$results", + }, + // Filter by continent if specified + ...(continents && continents.length > 0 + ? [ + { + $match: { + "results.location.continent": { $in: continents }, + }, + }, + ] + : []), + // Group by date bucket and continent + { + $group: { + _id: { + bucketDate: { + $dateToString: { + format: dateString, + date: "$createdAt", + }, + }, + continent: "$results.location.continent", + }, + avgResponseTime: { $avg: "$results.timings.total" }, + totalChecks: { $sum: 1 }, + upChecks: { + $sum: { + $cond: ["$results.status", 1, 0], + }, + }, + }, + }, + // Calculate uptime percentage + { + $project: { + _id: 0, + bucketDate: "$_id.bucketDate", + continent: "$_id.continent", + avgResponseTime: { $round: ["$avgResponseTime", 2] }, + totalChecks: 1, + uptimePercentage: { + $round: [ { - $match: { - "results.location.continent": { $in: continents }, - }, + $multiply: [ + { + $divide: ["$upChecks", "$totalChecks"], + }, + 100, + ], }, - ] - : []), - // Project to flat structure - { - $project: { - _id: 0, - monitorId: "$metadata.monitorId", - teamId: "$metadata.teamId", - type: "$metadata.type", - location: "$results.location", - status: "$results.status", - statusCode: "$results.statusCode", - timings: "$results.timings", - createdAt: 1, - updatedAt: 1, + 2, + ], }, }, - { $sort: { createdAt: convertedSortOrder } }, - { $skip: skip }, - { $limit: rowsPerPage }, - ]; - - // Count pipeline - const countPipeline: PipelineStage[] = [ - { $match: matchStage }, - { $unwind: "$results" }, - ...(continents && continents.length > 0 - ? [ - { - $match: { - "results.location.continent": { $in: continents }, - }, - }, - ] - : []), - { $count: "count" }, - ]; - - const [countResult, dataResults] = await Promise.all([GeoCheckModel.aggregate(countPipeline), GeoCheckModel.aggregate(pipeline)]); - - const geoChecksCount = countResult[0]?.count || 0; - const geoChecks: FlatGeoCheck[] = dataResults.map((doc) => ({ - id: `${doc.monitorId.toString()}-${new Date(doc.createdAt).getTime()}-${doc.location.continent}-${doc.location.city}-${Math.random().toString(36).substring(2, 15)}`, - monitorId: doc.monitorId.toString(), - teamId: doc.teamId.toString(), - type: doc.type, - location: doc.location, - status: doc.status, - statusCode: doc.statusCode, - timings: doc.timings, - createdAt: new Date(doc.createdAt).toISOString(), - updatedAt: new Date(doc.updatedAt).toISOString(), - })); - - return { geoChecksCount, geoChecks }; - } catch (error: unknown) { - this.logger.error({ - message: error instanceof Error ? `Error finding geo checks by monitor ID: ${error.message}` : "Error finding geo checks by monitor ID", - service: SERVICE_NAME, - method: "findByMonitorId", - stack: error instanceof Error ? error.stack : undefined, - }); - throw error; - } - }; - - findByMonitorIdAndDateRange = async (monitorId: string, startDate: Date, endDate: Date): Promise => { - try { - const docs = await GeoCheckModel.find({ - "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), - createdAt: { - $gte: startDate, - $lte: endDate, + }, + // Sort by date and continent + { + $sort: { + bucketDate: 1, + continent: 1, }, - }).sort({ createdAt: -1 }); - return docs.map(this.toEntity); - } catch (error: unknown) { - this.logger.error({ - message: - error instanceof Error - ? `Error finding geo checks by monitor ID and date range: ${error.message}` - : "Error finding geo checks by monitor ID and date range", - service: SERVICE_NAME, - method: "findByMonitorIdAndDateRange", - stack: error instanceof Error ? error.stack : undefined, - }); - throw error; - } - }; + }, + ]; - findGroupedByMonitorIdAndDateRange = async ( - monitorId: string, - startDate: Date, - endDate: Date, - dateFormat: string, - continents?: GeoContinent[] - ): Promise => { - try { - const pipeline: PipelineStage[] = [ - // Match geo checks for this monitor in date range - { - $match: { - "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), - createdAt: { - $gte: startDate, - $lte: endDate, - }, - }, - }, - // Unwind the results array to process each location separately - { - $unwind: "$results", - }, - // Filter by continent if specified - ...(continents && continents.length > 0 - ? [ - { - $match: { - "results.location.continent": { $in: continents }, - }, - }, - ] - : []), - // Group by date bucket and continent - { - $group: { - _id: { - bucketDate: { - $dateToString: { - format: dateFormat, - date: "$createdAt", - }, - }, - continent: "$results.location.continent", - }, - avgResponseTime: { $avg: "$results.timings.total" }, - totalChecks: { $sum: 1 }, - upChecks: { - $sum: { - $cond: ["$results.status", 1, 0], - }, - }, - }, - }, - // Calculate uptime percentage - { - $project: { - _id: 0, - bucketDate: "$_id.bucketDate", - continent: "$_id.continent", - avgResponseTime: { $round: ["$avgResponseTime", 2] }, - totalChecks: 1, - uptimePercentage: { - $round: [ - { - $multiply: [ - { - $divide: ["$upChecks", "$totalChecks"], - }, - 100, - ], - }, - 2, - ], - }, - }, - }, - // Sort by date and continent - { - $sort: { - bucketDate: 1, - continent: 1, - }, - }, - ]; - - const results = await GeoCheckModel.aggregate(pipeline); - return results as GroupedGeoCheck[]; - } catch (error: unknown) { - this.logger.error({ - message: error instanceof Error ? `Error finding grouped geo checks: ${error.message}` : "Error finding grouped geo checks", - service: SERVICE_NAME, - method: "findGroupedByMonitorIdAndDateRange", - stack: error instanceof Error ? error.stack : undefined, - }); - throw error; - } + const results = await GeoCheckModel.aggregate(pipeline); + return results as GroupedGeoCheck[]; }; deleteByMonitorId = async (monitorId: string): Promise => { - try { - const result = await GeoCheckModel.deleteMany({ - "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), - }); - return result.deletedCount || 0; - } catch (error: unknown) { - this.logger.error({ - message: error instanceof Error ? `Error deleting geo checks by monitor ID: ${error.message}` : "Error deleting geo checks by monitor ID", - service: SERVICE_NAME, - method: "deleteByMonitorId", - stack: error instanceof Error ? error.stack : undefined, - }); - throw error; - } + const result = await GeoCheckModel.deleteMany({ + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + }); + return result.deletedCount || 0; }; deleteByTeamId = async (teamId: string): Promise => { - try { - const result = await GeoCheckModel.deleteMany({ - "metadata.teamId": new mongoose.Types.ObjectId(teamId), - }); - return result.deletedCount || 0; - } catch (error: unknown) { - this.logger.error({ - message: error instanceof Error ? `Error deleting geo checks by team ID: ${error.message}` : "Error deleting geo checks by team ID", - service: SERVICE_NAME, - method: "deleteByTeamId", - stack: error instanceof Error ? error.stack : undefined, - }); - throw error; - } + const result = await GeoCheckModel.deleteMany({ + "metadata.teamId": new mongoose.Types.ObjectId(teamId), + }); + return result.deletedCount || 0; }; deleteByMonitorIdsNotIn = async (monitorIds: string[]): Promise => { - try { - const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); - const result = await GeoCheckModel.deleteMany({ "metadata.monitorId": { $nin: objectIds } }); - return result.deletedCount || 0; - } catch (error: unknown) { - this.logger.error({ - message: error instanceof Error ? `Error deleting orphaned geo checks: ${error.message}` : "Error deleting orphaned geo checks", - service: SERVICE_NAME, - method: "deleteByMonitorIdsNotIn", - stack: error instanceof Error ? error.stack : undefined, - }); - throw error; - } + const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); + const result = await GeoCheckModel.deleteMany({ "metadata.monitorId": { $nin: objectIds } }); + return result.deletedCount || 0; }; } diff --git a/server/src/domain/geo-checks/geo-check.service.ts b/server/src/domain/geo-checks/geo-check.service.ts index 0ace565fc..3b9f19b4d 100644 --- a/server/src/domain/geo-checks/geo-check.service.ts +++ b/server/src/domain/geo-checks/geo-check.service.ts @@ -1,5 +1,5 @@ import type { GeoCheck } from "@/domain/geo-checks/geo-check.type.js"; -import type { Monitor } from "@/domain/monitors/monitor.types.js"; +import type { Monitor } from "@/domain/monitors/monitor.type.js"; import type { GeoCheckResult, GeoContinent } from "@/domain/geo-checks/geo-check.type.js"; import { Types } from "mongoose"; import type { FlatGeoChecksQueryResult, IGeoChecksRepository } from "@/domain/geo-checks/geo-check.repository.interface.js"; @@ -7,18 +7,18 @@ import type { IMonitorsRepository } from "@/domain/monitors/monitor.repository.i import type { IGlobalPingService } from "@/service/globalPingService.js"; import type { ILogger } from "@/utils/logger.js"; import { AppError } from "@/utils/AppError.js"; +import { DateRange } from "@/types/query.js"; const SERVICE_NAME = "GeoChecksService"; export interface IGeoChecksService { - readonly serviceName: string; buildGeoCheck(monitor: Monitor): Promise; createGeoChecks(geoChecks: GeoCheck[]): Promise; getGeoChecksByMonitor(args: { monitorId: string; teamId: string; sortOrder: string; - dateRange: string; + dateRange: DateRange; page?: number; rowsPerPage?: number; continent?: GeoContinent | GeoContinent[]; @@ -50,10 +50,6 @@ export class GeoChecksService implements IGeoChecksService { this.monitorsRepository = monitorsRepository; } - get serviceName() { - return GeoChecksService.SERVICE_NAME; - } - async buildGeoCheck(monitor: Monitor): Promise { try { if (!monitor.url) { @@ -160,7 +156,7 @@ export class GeoChecksService implements IGeoChecksService { monitorId: string; teamId: string; sortOrder: string; - dateRange: string; + dateRange: DateRange; page?: number; rowsPerPage?: number; continent: GeoContinent | GeoContinent[]; diff --git a/server/src/domain/geo-checks/geo-check.type.ts b/server/src/domain/geo-checks/geo-check.type.ts index 044bec3b0..e2eea9f25 100644 --- a/server/src/domain/geo-checks/geo-check.type.ts +++ b/server/src/domain/geo-checks/geo-check.type.ts @@ -1,4 +1,4 @@ -import type { MonitorType } from "@/domain/monitors/monitor.types.js"; +import type { MonitorType } from "@/domain/monitors/monitor.type.js"; export const GeoContinents = ["EU", "NA", "AS", "SA", "AF", "OC"] as const; export type GeoContinent = (typeof GeoContinents)[number]; diff --git a/server/src/domain/incidents/incident.repository.mongo.ts b/server/src/domain/incidents/incident.repository.mongo.ts index 4cf410e88..61cea96b8 100644 --- a/server/src/domain/incidents/incident.repository.mongo.ts +++ b/server/src/domain/incidents/incident.repository.mongo.ts @@ -4,22 +4,9 @@ import type { Incident, IncidentSummary } from "@/domain/incidents/incident.type import type { IIncidentsRepository } from "@/domain/incidents/incident.repository.interface.js"; import mongoose from "mongoose"; import { AppError } from "@/utils/AppError.js"; +import { toStringId, toDateString } from "@/utils/mongoMappers.js"; class MongoIncidentsRepository implements IIncidentsRepository { - private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { - if (!value) { - return ""; - } - return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); - }; - - private toDateString = (value?: Date | string | null): string => { - if (!value) { - return new Date(0).toISOString(); - } - return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); - }; - private buildMatchStage({ teamId, startDate, @@ -46,26 +33,26 @@ class MongoIncidentsRepository implements IIncidentsRepository { return matchStage; } - protected toEntity = (doc: IncidentDocument): Incident => { + private toEntity = (doc: IncidentDocument): Incident => { return { - id: this.toStringId(doc._id), - monitorId: this.toStringId(doc.monitorId), - teamId: this.toStringId(doc.teamId), - startTime: this.toDateString(doc.startTime), - endTime: doc.endTime ? this.toDateString(doc.endTime) : null, + id: toStringId(doc._id), + monitorId: toStringId(doc.monitorId), + teamId: toStringId(doc.teamId), + startTime: toDateString(doc.startTime), + endTime: doc.endTime ? toDateString(doc.endTime) : null, status: doc.status, message: doc.message ?? null, statusCode: doc.statusCode ?? null, resolutionType: doc.resolutionType ?? null, - resolvedBy: doc.resolvedBy ? this.toStringId(doc.resolvedBy) : null, + resolvedBy: doc.resolvedBy ? toStringId(doc.resolvedBy) : null, resolvedByEmail: doc.resolvedByEmail ?? null, comment: doc.comment ?? null, - createdAt: this.toDateString(doc.createdAt), - updatedAt: this.toDateString(doc.updatedAt), + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), }; }; - protected mapDocuments = (documents: IncidentDocument[] | IncidentDocument | null): Incident[] => { + private mapDocuments = (documents: IncidentDocument[]): Incident[] => { if (!documents) { return []; } @@ -254,22 +241,22 @@ class MongoIncidentsRepository implements IIncidentsRepository { avgResolutionTimeHours, topMonitor: monitorResult[0] ? { - monitorId: this.toStringId(monitorResult[0].monitorId), + monitorId: toStringId(monitorResult[0].monitorId), monitorName: monitorResult[0].monitorName ?? null, incidentCount: monitorResult[0].count, } : null, latestIncidents: latestIncidents.map((incident) => ({ - id: this.toStringId(incident._id), - monitorId: this.toStringId(incident.monitorId), + id: toStringId(incident._id), + monitorId: toStringId(incident.monitorId), monitorName: incident.monitorName ?? null, status: incident.status, - startTime: this.toDateString(incident.startTime), - endTime: incident.endTime ? this.toDateString(incident.endTime) : null, + startTime: toDateString(incident.startTime), + endTime: incident.endTime ? toDateString(incident.endTime) : null, resolutionType: incident.resolutionType ?? null, message: incident.message ?? null, statusCode: incident.statusCode ?? null, - createdAt: this.toDateString(incident.createdAt), + createdAt: toDateString(incident.createdAt), })), }; }; diff --git a/server/src/domain/incidents/incident.service.ts b/server/src/domain/incidents/incident.service.ts index 311fad567..a9851d85e 100644 --- a/server/src/domain/incidents/incident.service.ts +++ b/server/src/domain/incidents/incident.service.ts @@ -1,5 +1,5 @@ const SERVICE_NAME = "incidentService"; -import type { Monitor } from "@/domain/monitors/monitor.types.js"; +import type { Monitor } from "@/domain/monitors/monitor.type.js"; import type { MonitorStatusResponse } from "@/types/network.js"; import { AppError } from "@/utils/AppError.js"; import { getDateForRange } from "@/utils/dataUtils.js"; @@ -11,6 +11,7 @@ import type { User } from "@/domain/users/user.type.js"; import type { MonitorActionDecision } from "@/worker/worker.helper.js"; import type { INotificationMessageBuilder } from "@/domain/notifications/notification.message-builder.js"; import type { ILogger } from "@/utils/logger.js"; +import { DateRange } from "@/types/query.js"; export interface IIncidentService { handleIncident( @@ -23,7 +24,7 @@ export interface IIncidentService { getIncidentsByTeam( teamId: string, sortOrder: string, - dateRange: string, + dateRange: DateRange, page: number, rowsPerPage: number, status: boolean | undefined, @@ -57,10 +58,6 @@ export class IncidentService implements IIncidentService { this.notificationMessageBuilder = notificationMessageBuilder; } - get serviceName() { - return IncidentService.SERVICE_NAME; - } - handleIncident = async ( monitor: Monitor, code: number, @@ -178,7 +175,7 @@ export class IncidentService implements IIncidentService { getIncidentsByTeam = async ( teamId: string, sortOrder: string, - dateRange: string, + dateRange: DateRange, page: number, rowsPerPage: number, status: boolean | undefined, diff --git a/server/src/domain/incidents/incident.type.ts b/server/src/domain/incidents/incident.type.ts index 6b076ff83..2b45e72c4 100644 --- a/server/src/domain/incidents/incident.type.ts +++ b/server/src/domain/incidents/incident.type.ts @@ -1,7 +1,7 @@ // export type IncidentResolutionType = "automatic" | "manual" | null; -export const IncidentResolutionTypes = ["automatic", "manual", null] as const; -export type IncidentResolutionType = (typeof IncidentResolutionTypes)[number]; +export const IncidentResolutionTypes = ["automatic", "manual"] as const; +export type IncidentResolutionType = (typeof IncidentResolutionTypes)[number] | null; export interface Incident { id: string; diff --git a/server/src/domain/invites/invite.repository.mongo.ts b/server/src/domain/invites/invite.repository.mongo.ts index 8956a08b3..78c666d52 100644 --- a/server/src/domain/invites/invite.repository.mongo.ts +++ b/server/src/domain/invites/invite.repository.mongo.ts @@ -2,34 +2,20 @@ import { IInvitesRepository } from "@/domain/invites/invite.repository.interface import type { Invite } from "@/domain/invites/invite.type.js"; import { type InviteDocument, InviteModel } from "@/domain/invites/invite.model.js"; import { AppError } from "@/utils/AppError.js"; -import mongoose from "mongoose"; import crypto from "crypto"; +import { toStringId, toDateString } from "@/utils/mongoMappers.js"; class MongoInvitesRepository implements IInvitesRepository { - private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { - if (!value) { - return ""; - } - return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); - }; - - private toDateString = (value?: Date | string | null): string => { - if (!value) { - return new Date(0).toISOString(); - } - return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); - }; - - protected toEntity = (doc: InviteDocument): Invite => { + private toEntity = (doc: InviteDocument): Invite => { return { - id: this.toStringId(doc._id), + id: toStringId(doc._id), email: doc.email, - teamId: this.toStringId(doc.teamId), + teamId: toStringId(doc.teamId), role: doc.role ?? [], token: doc.token, - expiry: this.toDateString(doc.expiry), - createdAt: this.toDateString(doc.createdAt), - updatedAt: this.toDateString(doc.updatedAt), + expiry: toDateString(doc.expiry), + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), }; }; diff --git a/server/src/domain/invites/invite.service.ts b/server/src/domain/invites/invite.service.ts index 76c5c6289..3dcffd9e0 100644 --- a/server/src/domain/invites/invite.service.ts +++ b/server/src/domain/invites/invite.service.ts @@ -35,10 +35,6 @@ export class InviteService implements IInviteService { this.emailService = emailService; } - get serviceName() { - return InviteService.SERVICE_NAME; - } - getInviteToken = async ({ invite, teamId, userRoles }: { invite: Partial; teamId: string; userRoles: UserRole[] }) => { invite.teamId = teamId; diff --git a/server/src/domain/maintenance-windows/maintenance-window.repository.mongo.ts b/server/src/domain/maintenance-windows/maintenance-window.repository.mongo.ts index ce32a22d1..cc16dad54 100644 --- a/server/src/domain/maintenance-windows/maintenance-window.repository.mongo.ts +++ b/server/src/domain/maintenance-windows/maintenance-window.repository.mongo.ts @@ -3,22 +3,8 @@ import { type MaintenanceWindowDocument, MaintenanceWindowModel } from "@/domain import { IMaintenanceWindowsRepository } from "./maintenance-window.repository.interface.js"; import mongoose, { SortOrder } from "mongoose"; import { AppError } from "@/utils/AppError.js"; - +import { toStringId, toDateString } from "@/utils/mongoMappers.js"; class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository { - private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { - if (!value) { - return ""; - } - return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); - }; - - private toDateString = (value?: Date | string | null): string => { - if (!value) { - return new Date(0).toISOString(); - } - return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); - }; - private mapDocuments = (documents: MaintenanceWindowDocument[]): MaintenanceWindow[] => { if (!documents?.length) { return []; @@ -28,18 +14,18 @@ class MongoMaintenanceWindowsRepository implements IMaintenanceWindowsRepository private toEntity = (doc: MaintenanceWindowDocument): MaintenanceWindow => { return { - id: this.toStringId(doc._id), - monitorIds: doc.monitorIds.map(this.toStringId), - teamId: this.toStringId(doc.teamId), + id: toStringId(doc._id), + monitorIds: doc.monitorIds.map(toStringId), + teamId: toStringId(doc.teamId), active: doc.active, name: doc.name, duration: doc.duration, durationUnit: doc.durationUnit, repeat: doc.repeat, - start: this.toDateString(doc.start), - end: this.toDateString(doc.end), - createdAt: this.toDateString(doc.createdAt), - updatedAt: this.toDateString(doc.updatedAt), + start: toDateString(doc.start), + end: toDateString(doc.end), + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), }; }; diff --git a/server/src/domain/maintenance-windows/maintenance-window.service.ts b/server/src/domain/maintenance-windows/maintenance-window.service.ts index 5551aef62..350e755aa 100644 --- a/server/src/domain/maintenance-windows/maintenance-window.service.ts +++ b/server/src/domain/maintenance-windows/maintenance-window.service.ts @@ -62,10 +62,6 @@ export class MaintenanceWindowService implements IMaintenanceWindowService { this.scheduler = scheduler; } - get serviceName() { - return MaintenanceWindowService.SERVICE_NAME; - } - private flipMonitorsLeavingMaintenance = async (monitorIds: string[], teamId: string, excludeWindowId: string, now: Date): Promise => { if (monitorIds.length === 0) return; diff --git a/server/src/domain/monitor-stats/monitor-stats.repository.mongo.ts b/server/src/domain/monitor-stats/monitor-stats.repository.mongo.ts index 7027197da..1cd942995 100644 --- a/server/src/domain/monitor-stats/monitor-stats.repository.mongo.ts +++ b/server/src/domain/monitor-stats/monitor-stats.repository.mongo.ts @@ -3,19 +3,9 @@ import type { CheckResultInput, MonitorStats } from "@/domain/monitor-stats/moni import { IMonitorStatsRepository } from "@/domain/monitor-stats/monitor-stats.repository.interface.js"; import mongoose from "mongoose"; import { AppError } from "@/utils/AppError.js"; +import { toStringId, toDateString } from "@/utils/mongoMappers.js"; class MongoMonitorStatsRepository implements IMonitorStatsRepository { private toEntity = (doc: MonitorStatsDocument): MonitorStats => { - const toStringId = (value: unknown): string => { - if (value instanceof mongoose.Types.ObjectId) { - return value.toString(); - } - return value?.toString() ?? ""; - }; - - const toDateString = (value: Date | string): string => { - return value instanceof Date ? value.toISOString() : value; - }; - return { id: toStringId(doc._id), monitorId: toStringId(doc.monitorId), diff --git a/server/src/domain/monitors/monitor.model.ts b/server/src/domain/monitors/monitor.model.ts index 47aa70b5e..446631f45 100644 --- a/server/src/domain/monitors/monitor.model.ts +++ b/server/src/domain/monitors/monitor.model.ts @@ -1,6 +1,6 @@ import { Schema, model, Types } from "mongoose"; -import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/domain/monitors/monitor.types.js"; -import { DnsRecordTypes, MonitorTypes, MonitorStatuses, PageSpeedStrategies, HttpMethods } from "@/domain/monitors/monitor.types.js"; +import type { Monitor, MonitorMatchMethod, CheckSnapshot } from "@/domain/monitors/monitor.type.js"; +import { DnsRecordTypes, MonitorTypes, MonitorStatuses, PageSpeedStrategies, HttpMethods } from "@/domain/monitors/monitor.type.js"; import type { CheckAudits, CheckCaptureInfo, diff --git a/server/src/domain/monitors/monitor.repository.interface.ts b/server/src/domain/monitors/monitor.repository.interface.ts index 46f3df396..d3dc0597e 100644 --- a/server/src/domain/monitors/monitor.repository.interface.ts +++ b/server/src/domain/monitors/monitor.repository.interface.ts @@ -1,4 +1,4 @@ -import type { MonitorType, Monitor, MonitorStatus, MonitorsSummary, CheckSnapshot } from "@/domain/monitors/monitor.types.js"; +import type { MonitorType, Monitor, MonitorStatus, MonitorsSummary, CheckSnapshot } from "@/domain/monitors/monitor.type.js"; export interface TeamQueryConfig { limit?: number; @@ -26,9 +26,9 @@ export interface IMonitorsRepository { findByIdLean(monitorId: string): Promise; // collection fetch - findAll(): Promise; - findByTeamId(teamId: string, config: TeamQueryConfig): Promise; - findByTeamIdWithStats(teamId: string, config: TeamQueryConfig): Promise; + findAll(): Promise; + findByTeamId(teamId: string, config: TeamQueryConfig): Promise; + findByTeamIdWithStats(teamId: string, config: TeamQueryConfig): Promise; findByIds(monitorIds: string[]): Promise; findByIdsWithChecks(monitorIds: string[], checksCount?: number): Promise; @@ -55,7 +55,6 @@ export interface IMonitorsRepository { // other findMonitorsSummaryByTeamId(teamId: string, config?: SummaryConfig): Promise; - findGroupsByTeamId(teamId: string): Promise; removeNotificationFromMonitors(notificationId: string): Promise; removeTagFromMonitors(tagId: string): Promise; updateNotifications(teamId: string, monitorIds: string[], notificationIds: string[], action: "add" | "remove" | "set"): Promise; diff --git a/server/src/domain/monitors/monitor.repository.mongo.ts b/server/src/domain/monitors/monitor.repository.mongo.ts index cba542c96..b96ec3907 100644 --- a/server/src/domain/monitors/monitor.repository.mongo.ts +++ b/server/src/domain/monitors/monitor.repository.mongo.ts @@ -1,11 +1,12 @@ import { MonitorModel } from "@/domain/monitors/monitor.model.js"; import type { MonitorDocument, CheckSnapshotDocument } from "@/domain/monitors/monitor.model.js"; import type { CheckSnapshot } from "@/domain/checks/check.type.js"; -import type { Monitor, MonitorStatus, MonitorsSummary } from "@/domain/monitors/monitor.types.js"; +import type { Monitor, MonitorStatus, MonitorsSummary } from "@/domain/monitors/monitor.type.js"; import mongoose, { type FilterQuery, type PipelineStage } from "mongoose"; import { MongoBulkWriteError } from "mongodb"; import { AppError } from "@/utils/AppError.js"; import { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "@/domain/monitors/monitor.repository.interface.js"; +import { toStringId, toDateString } from "@/utils/mongoMappers.js"; class MongoMonitorsRepository implements IMonitorsRepository { create = async (monitor: Monitor, teamId: string, userId: string) => { const monitorModel = new MonitorModel({ ...monitor, teamId, userId }); @@ -48,19 +49,19 @@ class MongoMonitorsRepository implements IMonitorsRepository { return this.mapDocuments(monitors); }; - findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise => { - const { page = 0, rowsPerPage = 0, filter, field = "createdAt", order = "desc", type, tags } = config ?? {}; + private queryBuilder = (config: TeamQueryConfig, teamId: string): FilterQuery => { + const { filter, field = "createdAt", type, tags } = config ?? {}; - const query: Record = { - teamId: new mongoose.Types.ObjectId(teamId), - }; + const query: FilterQuery = { teamId: new mongoose.Types.ObjectId(teamId) }; if (type !== undefined) { query.type = Array.isArray(type) ? { $in: type } : type; } if (tags !== undefined) { - query.tags = Array.isArray(tags) ? { $in: tags } : tags; + // Convert to ObjectIds: aggregation $match does no schema casting, so raw strings never match the ObjectId refs + const tagIds = (Array.isArray(tags) ? tags : [tags]).map((tag) => new mongoose.Types.ObjectId(tag)); + query.tags = { $in: tagIds }; } if (filter !== undefined) { @@ -81,74 +82,36 @@ class MongoMonitorsRepository implements IMonitorsRepository { break; } } + return query; + }; - const sort = { [field]: order === "asc" ? 1 : -1 } as const; - const skip = Math.max(page, 0) * rowsPerPage; + private uptimeStatsLookupStages: PipelineStage[] = [ + { $lookup: { from: "monitorstats", localField: "_id", foreignField: "monitorId", as: "stats" } }, + { $addFields: { uptimePercentage: { $arrayElemAt: ["$stats.uptimePercentage", 0] } } }, + { $project: { stats: 0 } }, + ]; - const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(rowsPerPage); + private pageOptions = (config: TeamQueryConfig) => { + const { page = 0, rowsPerPage = 0, field = "createdAt", order = "desc" } = config ?? {}; + return { sort: { [field]: order === "asc" ? 1 : -1 } as const, skip: Math.max(page, 0) * rowsPerPage, limit: rowsPerPage }; + }; + findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise => { + const query = this.queryBuilder(config, teamId); + const { sort, skip, limit } = this.pageOptions(config); + const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(limit); return this.mapDocuments(documents); }; - findByTeamIdWithStats = async (teamId: string, config: TeamQueryConfig): Promise => { - const { page = 0, rowsPerPage = 0, filter, field = "createdAt", order = "desc", type, tags } = config ?? {}; - - const query: Record = { - teamId: new mongoose.Types.ObjectId(teamId), - }; - - if (type !== undefined) { - query.type = Array.isArray(type) ? { $in: type } : type; - } - - if (tags !== undefined) { - query.tags = Array.isArray(tags) ? { $in: tags } : tags; - } - - if (filter !== undefined) { - switch (field) { - case "name": - query.$or = [{ name: { $regex: filter, $options: "i" } }, { url: { $regex: filter, $options: "i" } }]; - break; - case "isActive": - query.isActive = filter === "true"; - break; - case "status": - query.status = filter; - break; - case "type": - query.type = filter; - break; - default: - break; - } - } - - const sort = { [field]: order === "asc" ? 1 : -1 } as const; - const skip = Math.max(page, 0) * rowsPerPage; + findByTeamIdWithStats = async (teamId: string, config: TeamQueryConfig): Promise => { + const { sort, skip, limit } = this.pageOptions(config); + const query = this.queryBuilder(config, teamId); const pipeline: PipelineStage[] = [ { $match: query }, { $sort: sort }, - ...(rowsPerPage ? [{ $skip: skip }, { $limit: rowsPerPage }] : []), - { - $lookup: { - from: "monitorstats", - localField: "_id", - foreignField: "monitorId", - as: "stats", - }, - }, - { - $addFields: { - uptimePercentage: { $arrayElemAt: ["$stats.uptimePercentage", 0] }, - }, - }, - { - $project: { - stats: 0, - }, - }, + ...(limit ? [{ $skip: skip }, { $limit: limit }] : []), + ...this.uptimeStatsLookupStages, ]; const documents = await MonitorModel.aggregate(pipeline); @@ -162,27 +125,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { const objectIds = monitorIds.map((id) => new mongoose.Types.ObjectId(id)); - const pipeline: PipelineStage[] = [ - { $match: { _id: { $in: objectIds } } }, - { - $lookup: { - from: "monitorstats", - localField: "_id", - foreignField: "monitorId", - as: "stats", - }, - }, - { - $addFields: { - uptimePercentage: { $arrayElemAt: ["$stats.uptimePercentage", 0] }, - }, - }, - { - $project: { - stats: 0, - }, - }, - ]; + const pipeline: PipelineStage[] = [{ $match: { _id: { $in: objectIds } } }, ...this.uptimeStatsLookupStages]; const documents = await MonitorModel.aggregate(pipeline); return documents.map((doc) => this.toEntity(doc)); @@ -249,24 +192,11 @@ class MongoMonitorsRepository implements IMonitorsRepository { ]; const documents = await MonitorModel.aggregate(pipeline); - return documents.map((doc) => this.toEntityWithChecks(doc)); + return documents.map((doc) => this.toEntity(doc)); }; - findMonitorCountByTeamIdAndType = async (teamId: string, config?: TeamQueryConfig): Promise => { - const { type, tags } = config ?? {}; - - const query: FilterQuery = { - teamId: new mongoose.Types.ObjectId(teamId), - }; - - if (type !== undefined) { - query.type = Array.isArray(type) ? { $in: type } : type; - } - - if (tags !== undefined) { - query.tags = Array.isArray(tags) ? { $in: tags } : tags; - } - + findMonitorCountByTeamIdAndType = async (teamId: string, config: TeamQueryConfig): Promise => { + const query = this.queryBuilder(config, teamId); const count = await MonitorModel.countDocuments(query); return count; }; @@ -394,15 +324,8 @@ class MongoMonitorsRepository implements IMonitorsRepository { return { monitors: this.mapDocuments(monitors), deletedCount }; }; - findMonitorsSummaryByTeamId = async (teamId: string, config?: SummaryConfig): Promise => { - const match: FilterQuery = { teamId: new mongoose.Types.ObjectId(teamId) }; - if (config?.type !== undefined) { - match.type = Array.isArray(config.type) ? { $in: config.type } : config.type; - } - if (config?.tags !== undefined) { - const tagIds = (Array.isArray(config.tags) ? config.tags : [config.tags]).map((tag) => new mongoose.Types.ObjectId(tag)); - match.tags = { $in: tagIds }; - } + findMonitorsSummaryByTeamId = async (teamId: string, config: SummaryConfig): Promise => { + const match = this.queryBuilder(config, teamId); const pipeline = [ { $match: match }, { @@ -458,14 +381,6 @@ class MongoMonitorsRepository implements IMonitorsRepository { ); }; - findGroupsByTeamId = async (teamId: string): Promise => { - const groups = await MonitorModel.distinct("group", { - teamId: new mongoose.Types.ObjectId(teamId), - group: { $nin: [null, ""] }, - }); - return groups.sort(); - }; - removeNotificationFromMonitors = async (notificationId: string): Promise => { await MonitorModel.updateMany({ notifications: notificationId }, { $pull: { notifications: notificationId } }); }; @@ -517,17 +432,6 @@ class MongoMonitorsRepository implements IMonitorsRepository { }; private toEntity = (doc: MonitorDocument): Monitor => { - const toStringId = (value: unknown): string => { - if (value instanceof mongoose.Types.ObjectId) { - return value.toString(); - } - return value?.toString() ?? ""; - }; - - const toDateString = (value: Date | string): string => { - return value instanceof Date ? value.toISOString() : value; - }; - const notificationIds = (doc.notifications ?? []).map((notification) => toStringId(notification)); const tagIds = (doc.tags ?? []).map((tag) => toStringId(tag)); return { @@ -581,78 +485,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { }; }; - private toEntityWithChecks = (doc: MonitorDocument): Monitor => { - const toStringId = (value: unknown): string => { - if (value instanceof mongoose.Types.ObjectId) { - return value.toString(); - } - return value?.toString() ?? ""; - }; - - const toDateString = (value: Date | string): string => { - if (!value) return ""; - return value instanceof Date ? value.toISOString() : value; - }; - - const notificationIds = (doc.notifications ?? []).map((notification: unknown) => toStringId(notification)); - const tagIds = (doc.tags ?? []).map((tag: unknown) => toStringId(tag)); - - return { - id: toStringId(doc._id), - userId: toStringId(doc.userId), - teamId: toStringId(doc.teamId), - name: doc.name, - description: doc.description ?? undefined, - method: doc.method ?? "GET", - status: doc.status ?? "initializing", - statusWindow: doc.statusWindow ?? [], - statusWindowSize: doc.statusWindowSize, - statusWindowThreshold: doc.statusWindowThreshold, - type: doc.type, - ignoreTlsErrors: doc.ignoreTlsErrors, - useAdvancedMatching: doc.useAdvancedMatching ?? false, - jsonPath: doc.jsonPath ?? undefined, - expectedValue: doc.expectedValue ?? undefined, - matchMethod: doc.matchMethod ?? undefined, - url: doc.url, - port: doc.port ?? undefined, - isActive: doc.isActive, - interval: doc.interval, - uptimePercentage: doc.uptimePercentage ?? undefined, - notifications: notificationIds, - tags: tagIds, - customUpCodes: doc.customUpCodes ?? [], - secret: doc.secret ?? undefined, - cpuAlertThreshold: doc.cpuAlertThreshold, - cpuAlertCounter: doc.cpuAlertCounter, - memoryAlertThreshold: doc.memoryAlertThreshold, - memoryAlertCounter: doc.memoryAlertCounter, - diskAlertThreshold: doc.diskAlertThreshold, - diskAlertCounter: doc.diskAlertCounter, - tempAlertThreshold: doc.tempAlertThreshold, - tempAlertCounter: doc.tempAlertCounter, - selectedDisks: doc.selectedDisks ?? [], - gameId: doc.gameId ?? undefined, - grpcServiceName: doc.grpcServiceName ?? undefined, - strategy: doc.strategy ?? undefined, - group: doc.group ?? null, - recentChecks: (doc.recentChecks ?? []).map((check: CheckSnapshotDocument) => this.toCheckSnapshot(check)), - geoCheckEnabled: doc.geoCheckEnabled ?? false, - geoCheckLocations: doc.geoCheckLocations ?? [], - geoCheckInterval: doc.geoCheckInterval ?? 300000, - dnsServer: doc.dnsServer ?? undefined, - dnsRecordType: doc.dnsRecordType ?? undefined, - createdAt: toDateString(doc.createdAt), - updatedAt: toDateString(doc.updatedAt), - lastEvaluatedAt: doc.lastEvaluatedAt, - }; - }; - private toCheckSnapshot = (doc: CheckSnapshotDocument): CheckSnapshot => { - const toDateString = (value: Date | string): string => { - return value instanceof Date ? value.toISOString() : value; - }; - return { id: doc.id, status: doc.status, diff --git a/server/src/domain/monitors/monitor.service.ts b/server/src/domain/monitors/monitor.service.ts index 455a1daff..29046f83d 100644 --- a/server/src/domain/monitors/monitor.service.ts +++ b/server/src/domain/monitors/monitor.service.ts @@ -1,5 +1,5 @@ -import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js"; -import { type Monitor } from "@/domain/monitors/monitor.types.js"; +import { NormalizeData } from "@/utils/dataUtils.js"; +import { type Monitor } from "@/domain/monitors/monitor.type.js"; import type { MonitorType, MonitorsWithChecksByTeamIdResult, @@ -8,8 +8,8 @@ import type { PageSpeedDetailsResult, GamesMap, GroupedGeoCheckResult, -} from "@/domain/monitors/monitor.types.js"; -import { supportsGeoCheck, supportsUptimeDetails } from "@/domain/monitors/monitor.types.js"; +} from "@/domain/monitors/monitor.type.js"; +import { supportsGeoCheck, supportsUptimeDetails } from "@/domain/monitors/monitor.type.js"; import type { UptimeChecksResult, HardwareChecksResult, PageSpeedChecksResult } from "@/domain/checks/check.type.js"; import type { GeoContinent } from "@/domain/geo-checks/geo-check.type.js"; import type { IChecksRepository } from "@/domain/checks/check.repository.interface.js"; @@ -23,29 +23,27 @@ import { AppError } from "@/utils/AppError.js"; import type { ImportedMonitor } from "@/api/validation/monitorValidation.js"; import { ILogger } from "@/utils/logger.js"; import { IJobScheduler } from "@/worker/worker.interface.js"; +import { DateRange } from "@/types/query.js"; const SERVICE_NAME = "MonitorService"; -type DateRangeKey = "recent" | "day" | "week" | "month" | "all"; const isUptimeChecksResult = (result: UptimeChecksResult | HardwareChecksResult | PageSpeedChecksResult): result is UptimeChecksResult => supportsUptimeDetails(result.monitorType); export interface IMonitorService { - readonly serviceName: string; - // create createMonitor(teamId: string, userId: string, body: Partial): Promise; createMonitors(monitors: Array): Promise; addDemoMonitors(args: { userId: string; teamId: string }): Promise; // read - getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; - getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; - getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: string }): Promise; + getUptimeDetailsById(args: { teamId: string; monitorId: string; dateRange: DateRange }): Promise; + getHardwareDetailsById(args: { teamId: string; monitorId: string; dateRange: DateRange }): Promise; + getPageSpeedDetailsById(args: { teamId: string; monitorId: string; dateRange: DateRange }): Promise; getGeoChecksByMonitorId(args: { teamId: string; monitorId: string; - dateRange: string; + dateRange: DateRange; continents?: GeoContinent[]; }): Promise; getMonitorById(args: { teamId: string; monitorId: string }): Promise; @@ -72,7 +70,6 @@ export interface IMonitorService { order?: "asc" | "desc"; }): Promise; getAllGames(): GamesMap; - getGroupsByTeamId(args: { teamId: string }): Promise; // update editMonitor(args: { teamId: string; monitorId: string; body: Partial }): Promise; @@ -136,35 +133,6 @@ export class MonitorService implements IMonitorService { this.incidentsRepository = incidentsRepository; } - get serviceName(): string { - return MonitorService.SERVICE_NAME; - } - - private getDateRange = (dateRange: DateRangeKey) => { - const startDates = { - recent: new Date(new Date().setHours(new Date().getHours() - 2)), - day: new Date(new Date().setDate(new Date().getDate() - 1)), - week: new Date(new Date().setDate(new Date().getDate() - 7)), - month: new Date(new Date().setMonth(new Date().getMonth() - 1)), - all: new Date(0), - }; - return { - start: startDates[dateRange], - end: new Date(), - }; - }; - - private getDateFormat = (dateRange: DateRangeKey): string => { - const formatLookup = { - recent: "%Y-%m-%dT%H:%M:00Z", - day: "%Y-%m-%dT%H:00:00Z", - week: "%Y-%m-%dT00:00:00Z", - month: "%Y-%m-%dT00:00:00Z", - all: "%Y-%m-%dT00:00:00Z", - }; - return formatLookup[dateRange]; - }; - createMonitor = async (teamId: string, userId: string, body: Monitor): Promise => { const monitor = await this.monitorsRepository.create(body, teamId, userId); if (!monitor) { @@ -207,15 +175,13 @@ export class MonitorService implements IMonitorService { }: { teamId: string; monitorId: string; - dateRange: string; + dateRange: DateRange; }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { throw new AppError({ message: `Monitor with ID ${monitorId} not found.`, status: 404 }); } - const rangeKey = dateRange as DateRangeKey; - const { start, end } = this.getDateRange(rangeKey); - const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), { + const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, dateRange, { type: monitor.type, }); const monitorStats = await this.monitorStatsRepository.findByMonitorId(monitor.id); @@ -227,9 +193,9 @@ export class MonitorService implements IMonitorService { return { monitorData: { monitor, - groupedChecks: NormalizeDataUptimeDetails(checksData.groupedChecks, 10, 100), - groupedUpChecks: NormalizeDataUptimeDetails(checksData.groupedUpChecks, 10, 100), - groupedDownChecks: NormalizeDataUptimeDetails(checksData.groupedDownChecks, 10, 100), + groupedChecks: checksData.groupedChecks, + groupedUpChecks: checksData.groupedUpChecks, + groupedDownChecks: checksData.groupedDownChecks, groupedAvgResponseTime: checksData.avgResponseTime, groupedUptimePercentage: checksData.uptimePercentage, }, @@ -244,7 +210,7 @@ export class MonitorService implements IMonitorService { }: { teamId: string; monitorId: string; - dateRange: string; + dateRange: DateRange; }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { @@ -254,9 +220,7 @@ export class MonitorService implements IMonitorService { throw new AppError({ message: `${monitor.type} monitors are not supported for hardware details`, status: 400 }); } - const rangeKey = dateRange as DateRangeKey; - const { start, end } = this.getDateRange(rangeKey); - const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), { + const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, dateRange, { type: monitor.type, }); @@ -286,7 +250,7 @@ export class MonitorService implements IMonitorService { }: { teamId: string; monitorId: string; - dateRange: string; + dateRange: DateRange; }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); if (!monitor) { @@ -296,9 +260,7 @@ export class MonitorService implements IMonitorService { throw new AppError({ message: `${monitor.type} monitors are not supported for pagespeed details`, status: 400 }); } - const rangeKey = dateRange as DateRangeKey; - const { start, end } = this.getDateRange(rangeKey); - const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, start, end, this.getDateFormat(rangeKey), { + const checksData = await this.checksRepository.findByDateRangeAndMonitorId(monitor.id, dateRange, { type: monitor.type, }); @@ -325,7 +287,7 @@ export class MonitorService implements IMonitorService { }: { teamId: string; monitorId: string; - dateRange: string; + dateRange: DateRange; continents?: GeoContinent[]; }): Promise => { const monitor = await this.monitorsRepository.findById(monitorId, teamId); @@ -337,15 +299,7 @@ export class MonitorService implements IMonitorService { return { groupedGeoChecks: [] }; } - const rangeKey = dateRange as DateRangeKey; - const { start, end } = this.getDateRange(rangeKey); - const groupedGeoChecks = await this.geoChecksRepository.findGroupedByMonitorIdAndDateRange( - monitor.id, - start, - end, - this.getDateFormat(rangeKey), - continents - ); + const groupedGeoChecks = await this.geoChecksRepository.findGroupedByMonitorIdAndDateRange(monitor.id, dateRange, continents); return { groupedGeoChecks }; }; @@ -364,7 +318,7 @@ export class MonitorService implements IMonitorService { type?: MonitorType | MonitorType[]; tags?: string | string[]; filter?: string; - }): Promise => { + }): Promise => { return await this.monitorsRepository.findByTeamId(teamId, { type, tags, filter }); }; @@ -402,13 +356,12 @@ export class MonitorService implements IMonitorService { order, }); - const monitorsList = (monitors ?? []) as Monitor[]; const snapshotTypes: MonitorType[] = ["hardware"]; const requestedTypes = Array.isArray(type) ? type : type ? [type] : []; const snapshotOnlyRequest = requestedTypes.length > 0 && requestedTypes.every((requestedType) => snapshotTypes.includes(requestedType as MonitorType)); - const monitorsWithChecks = monitorsList.map((monitor: Monitor) => { + const monitorsWithChecks = monitors.map((monitor: Monitor) => { const rawChecks = monitor.recentChecks ?? []; const isSnapshotType = snapshotOnlyRequest || snapshotTypes.includes(monitor.type); const checks = isSnapshotType ? rawChecks.slice(0, 1) : NormalizeData(rawChecks, 10, 100); @@ -422,10 +375,6 @@ export class MonitorService implements IMonitorService { return this.games; }; - getGroupsByTeamId = async ({ teamId }: { teamId: string }): Promise => { - return await this.monitorsRepository.findGroupsByTeamId(teamId); - }; - editMonitor = async ({ teamId, monitorId, body }: { teamId: string; monitorId: string; body: Partial }) => { const editedMonitor = await this.monitorsRepository.updateById(monitorId, teamId, body); await this.scheduler.updateJob(editedMonitor); @@ -501,15 +450,7 @@ export class MonitorService implements IMonitorService { return { monitors, failedCount }; }; - deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { - const monitor = await this.monitorsRepository.deleteById(monitorId, teamId); - await this.monitorStatsRepository.deleteByMonitorId(monitor.id).catch((err: unknown) => { - this.logger.warn({ - message: `Error deleting monitor stats for monitor ${monitor.id} with name ${monitor.name}`, - service: SERVICE_NAME, - stack: err instanceof Error ? err.stack : undefined, - }); - }); + private deleteMonitorChildren = async (monitor: Monitor, teamId: string) => { await this.checksRepository.deleteByMonitorId(monitor.id).catch((err: unknown) => { this.logger.warn({ message: `Error deleting checks for monitor ${monitor.id} with name ${monitor.name}`, @@ -541,6 +482,19 @@ export class MonitorService implements IMonitorService { }); }); + await this.monitorStatsRepository.deleteByMonitorId(monitor.id).catch((err: unknown) => { + this.logger.warn({ + message: `Error deleting monitor stats for monitor ${monitor.id} with name ${monitor.name}`, + service: SERVICE_NAME, + stack: err instanceof Error ? err.stack : undefined, + }); + }); + }; + + deleteMonitor = async ({ teamId, monitorId }: { teamId: string; monitorId: string }): Promise => { + const monitor = await this.monitorsRepository.deleteById(monitorId, teamId); + + await this.deleteMonitorChildren(monitor, teamId); await this.scheduler.deleteJob(monitor); return monitor; }; @@ -550,11 +504,8 @@ export class MonitorService implements IMonitorService { await Promise.all( monitors.map(async (monitor) => { try { + await this.deleteMonitorChildren(monitor, teamId); await this.scheduler.deleteJob(monitor); - await this.checksRepository.deleteByMonitorId(monitor.id); - await this.geoChecksRepository.deleteByMonitorId(monitor.id); - await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id); - await this.monitorStatsRepository.deleteByMonitorId(monitor.id); } catch (error: unknown) { this.logger.warn({ message: `Error deleting associated records for monitor ${monitor.id} with name ${monitor.name}`, @@ -571,7 +522,7 @@ export class MonitorService implements IMonitorService { exportMonitorsToJSON = async ({ teamId }: { teamId: string }): Promise => { const monitors = await this.monitorsRepository.findByTeamId(teamId, {}); - if (!monitors || monitors.length === 0) { + if (monitors.length === 0) { throw new AppError({ message: "No monitors found to export.", service: SERVICE_NAME, method: "exportMonitorsToJSON", status: 400 }); } diff --git a/server/src/domain/monitors/monitor.types.ts b/server/src/domain/monitors/monitor.type.ts similarity index 87% rename from server/src/domain/monitors/monitor.types.ts rename to server/src/domain/monitors/monitor.type.ts index 22090f68c..865bafd76 100644 --- a/server/src/domain/monitors/monitor.types.ts +++ b/server/src/domain/monitors/monitor.type.ts @@ -3,6 +3,7 @@ export type { CheckSnapshot } from "@/domain/checks/check.type.js"; import type { GeoContinent, GroupedGeoCheck } from "@/domain/geo-checks/geo-check.type.js"; export type { GeoContinent } from "@/domain/geo-checks/geo-check.type.js"; import http from "node:http"; +import { HardwareStats } from "@/domain/checks/check.type.js"; export const HttpStatusCodes = [ ...Object.keys(http.STATUS_CODES).map(Number), @@ -149,48 +150,6 @@ export interface UptimeDetailsResult { monitorStats: import("../monitor-stats/monitor-stats.type.js").MonitorStats | null; } -export interface HardwareDiskStats { - name: string; - readSpeed: number; - writeSpeed: number; - totalBytes: number; - freeBytes: number; - usagePercent: number; -} - -export interface HardwareNetStats { - name: string; - bytesSentPerSecond: number; - deltaBytesRecv: number; - deltaPacketsSent: number; - deltaPacketsRecv: number; - deltaErrIn: number; - deltaErrOut: number; - deltaDropIn: number; - deltaDropOut: number; - deltaFifoIn: number; - deltaFifoOut: number; -} - -export interface HardwareCheckStats { - bucketDate: string; - avgCpuUsage: number; - avgMemoryUsage: number; - avgTemperature: number[]; - disks: HardwareDiskStats[]; - net: HardwareNetStats[]; -} - -export interface HardwareStats { - aggregateData: { - totalChecks: number; - }; - upChecks: { - totalChecks: number; - }; - checks: HardwareCheckStats[]; -} - export interface HardwareDetailsResult { monitor: Monitor; stats: HardwareStats; diff --git a/server/src/domain/notifications/notification.message-builder.ts b/server/src/domain/notifications/notification.message-builder.ts index b11d99a94..76e9f6447 100644 --- a/server/src/domain/notifications/notification.message-builder.ts +++ b/server/src/domain/notifications/notification.message-builder.ts @@ -1,4 +1,4 @@ -import type { Monitor } from "@/domain/monitors/monitor.types.js"; +import type { Monitor } from "@/domain/monitors/monitor.type.js"; import type { HardwareStatusPayload, MonitorStatusResponse } from "@/types/network.js"; import type { MonitorActionDecision } from "@/worker/worker.helper.js"; import type { diff --git a/server/src/domain/notifications/notification.repository.mongo.ts b/server/src/domain/notifications/notification.repository.mongo.ts index cc2729cf7..98c7501ed 100644 --- a/server/src/domain/notifications/notification.repository.mongo.ts +++ b/server/src/domain/notifications/notification.repository.mongo.ts @@ -3,6 +3,7 @@ import { NotificationModel, type NotificationDocument } from "@/domain/notificat import { INotificationsRepository } from "@/domain/notifications/notification.repository.interface.js"; import type { Notification } from "@/domain/notifications/notification.type.js"; import { AppError } from "@/utils/AppError.js"; +import { toStringId, toDateString } from "@/utils/mongoMappers.js"; class MongoNotificationsRepository implements INotificationsRepository { private mapDocuments = (documents: NotificationDocument[]): Notification[] => { @@ -13,14 +14,6 @@ class MongoNotificationsRepository implements INotificationsRepository { }; private toEntity = (doc: NotificationDocument): Notification => { - const toStringId = (value: mongoose.Types.ObjectId | string): string => { - return value instanceof mongoose.Types.ObjectId ? value.toString() : value; - }; - - const toDateString = (value: Date | string): string => { - return value instanceof Date ? value.toISOString() : value; - }; - return { id: toStringId(doc._id), userId: toStringId(doc.userId), diff --git a/server/src/domain/notifications/notification.service.ts b/server/src/domain/notifications/notification.service.ts index 7fc6e3543..61d8d09e3 100644 --- a/server/src/domain/notifications/notification.service.ts +++ b/server/src/domain/notifications/notification.service.ts @@ -1,4 +1,4 @@ -import type { Monitor } from "@/domain/monitors/monitor.types.js"; +import type { Monitor } from "@/domain/monitors/monitor.type.js"; import type { Notification } from "@/domain/notifications/notification.type.js"; import type { MonitorStatusResponse } from "@/types/network.js"; import type { NotificationMessage } from "@/domain/notifications/notification.type.js"; @@ -9,6 +9,9 @@ import type { MonitorActionDecision } from "@/worker/worker.helper.js"; import type { ISettingsService } from "@/domain/app-settings/app-settings.service.js"; import { ILogger } from "@/utils/logger.js"; import type { INotificationMessageBuilder } from "@/domain/notifications/notification.message-builder.js"; +import type { NotificationChannel } from "@/domain/notifications/notification.type.js"; + +export type NotificationProviderRegistry = Record; export interface INotificationsService { createNotification: (notificationData: Partial, userId: string, teamId: string) => Promise; @@ -29,52 +32,29 @@ export class NotificationsService implements INotificationsService { private notificationsRepository: INotificationsRepository; private monitorsRepository: IMonitorsRepository; - private webhookProvider: INotificationProvider; - private emailProvider: INotificationProvider; - private slackProvider: INotificationProvider; - private discordProvider: INotificationProvider; - private pagerDutyProvider: INotificationProvider; - private matrixProvider: INotificationProvider; - private teamsProvider: INotificationProvider; - private telegramProvider: INotificationProvider; - private pushoverProvider: INotificationProvider; - private twilioProvider: INotificationProvider; - private ntfyProvider: INotificationProvider; - private logger: ILogger; + private providers: NotificationProviderRegistry; private settingsService: ISettingsService; + private logger: ILogger; private notificationMessageBuilder: INotificationMessageBuilder; - constructor( - notificationsRepository: INotificationsRepository, - monitorsRepository: IMonitorsRepository, - webhookProvider: INotificationProvider, - emailProvider: INotificationProvider, - slackProvider: INotificationProvider, - discordProvider: INotificationProvider, - pagerDutyProvider: INotificationProvider, - matrixProvider: INotificationProvider, - teamsProvider: INotificationProvider, - telegramProvider: INotificationProvider, - pushoverProvider: INotificationProvider, - twilioProvider: INotificationProvider, - ntfyProvider: INotificationProvider, - settingsService: ISettingsService, - logger: ILogger, - notificationMessageBuilder: INotificationMessageBuilder - ) { + constructor({ + notificationsRepository, + monitorsRepository, + providers, + settingsService, + logger, + notificationMessageBuilder, + }: { + notificationsRepository: INotificationsRepository; + monitorsRepository: IMonitorsRepository; + providers: NotificationProviderRegistry; + settingsService: ISettingsService; + logger: ILogger; + notificationMessageBuilder: INotificationMessageBuilder; + }) { this.notificationsRepository = notificationsRepository; this.monitorsRepository = monitorsRepository; - this.webhookProvider = webhookProvider; - this.emailProvider = emailProvider; - this.slackProvider = slackProvider; - this.discordProvider = discordProvider; - this.pagerDutyProvider = pagerDutyProvider; - this.matrixProvider = matrixProvider; - this.teamsProvider = teamsProvider; - this.telegramProvider = telegramProvider; - this.pushoverProvider = pushoverProvider; - this.twilioProvider = twilioProvider; - this.ntfyProvider = ntfyProvider; + this.providers = providers; this.settingsService = settingsService; this.logger = logger; this.notificationMessageBuilder = notificationMessageBuilder; @@ -97,37 +77,16 @@ export class NotificationsService implements INotificationsService { } // Route to provider based on notification type - switch (notification.type) { - case "webhook": - return await this.webhookProvider.sendMessage!(notification, notificationMessage); - case "slack": - return await this.slackProvider.sendMessage!(notification, notificationMessage); - case "matrix": - return await this.matrixProvider.sendMessage!(notification, notificationMessage); - case "pager_duty": - return await this.pagerDutyProvider.sendMessage!(notification, notificationMessage); - case "discord": - return await this.discordProvider.sendMessage!(notification, notificationMessage); - case "email": - return await this.emailProvider.sendMessage!(notification, notificationMessage); - case "teams": - return await this.teamsProvider.sendMessage!(notification, notificationMessage); - case "telegram": - return await this.telegramProvider.sendMessage!(notification, notificationMessage); - case "pushover": - return await this.pushoverProvider.sendMessage!(notification, notificationMessage); - case "twilio": - return await this.twilioProvider.sendMessage!(notification, notificationMessage); - case "ntfy": - return await this.ntfyProvider.sendMessage!(notification, notificationMessage); - default: - this.logger.warn({ - message: `Unknown notification type: ${notification.type}`, - service: SERVICE_NAME, - method: "send", - }); - return false; + const provider = this.providers[notification.type]; + if (!provider) { + this.logger.warn({ + message: `Unknown notification type: ${notification.type}`, + service: SERVICE_NAME, + method: "send", + }); + return false; } + return await provider.sendMessage(notification, notificationMessage); }; private sendNotifications = async (monitor: Monitor, monitorStatusResponse: MonitorStatusResponse, decision: MonitorActionDecision) => { @@ -165,32 +124,26 @@ export class NotificationsService implements INotificationsService { }; sendTestNotification = async (notification: Partial) => { - switch (notification.type) { - case "email": - return await this.emailProvider.sendTestAlert(notification); - case "slack": - return await this.slackProvider.sendTestAlert(notification); - case "discord": - return await this.discordProvider.sendTestAlert(notification); - case "pager_duty": - return await this.pagerDutyProvider.sendTestAlert(notification); - case "matrix": - return await this.matrixProvider.sendTestAlert(notification); - case "webhook": - return await this.webhookProvider.sendTestAlert(notification); - case "teams": - return await this.teamsProvider.sendTestAlert(notification); - case "telegram": - return await this.telegramProvider.sendTestAlert(notification); - case "pushover": - return await this.pushoverProvider.sendTestAlert(notification); - case "twilio": - return await this.twilioProvider.sendTestAlert(notification); - case "ntfy": - return await this.ntfyProvider.sendTestAlert(notification); - default: - return false; + const type = notification.type; + if (!type) { + this.logger.warn({ + message: "Notification type not provided", + service: SERVICE_NAME, + method: "sendTestNotification", + }); + return false; } + + const provider = this.providers[type]; + if (!provider) { + this.logger.warn({ + message: `Unknown notification type: ${notification.type}`, + service: SERVICE_NAME, + method: "sendTestNotification", + }); + return false; + } + return await provider.sendTestAlert(notification); }; testAllNotifications = async (notificationIds: string[]) => { diff --git a/server/src/domain/queue-workers/queue-worker.model.ts b/server/src/domain/queue-workers/queue-worker.model.ts index 0dd90b5c5..c89ecca5d 100644 --- a/server/src/domain/queue-workers/queue-worker.model.ts +++ b/server/src/domain/queue-workers/queue-worker.model.ts @@ -4,7 +4,8 @@ import { QueueWorkerDocument } from "@/domain/queue-workers/queue-worker.type.js // Single source of truth for worker liveness: the TTL that GCs stale records and // the window readers use to count a worker as alive. Heartbeat cadence must stay well below it. -export const WORKER_TTL_SECONDS = 180; +const WORKER_TTL_SECONDS = 180; +export const WORKER_STALE_MS = WORKER_TTL_SECONDS * 1000; const QueueWorkerSchema = new Schema( { diff --git a/server/src/domain/queue-workers/queue-worker.repository.mongo.ts b/server/src/domain/queue-workers/queue-worker.repository.mongo.ts index da62d9850..3b125cbbd 100644 --- a/server/src/domain/queue-workers/queue-worker.repository.mongo.ts +++ b/server/src/domain/queue-workers/queue-worker.repository.mongo.ts @@ -7,7 +7,7 @@ const SERVICE_NAME = "MongoQueueWorkersRepository"; class MongoQueueWorkersRepository implements IQueueWorkersRepository { static SERVICE_NAME = SERVICE_NAME; - protected toEntity = (doc: QueueWorkerDocument): QueueWorker => { + private toEntity = (doc: QueueWorkerDocument): QueueWorker => { return { workerId: doc._id, mode: doc.mode, diff --git a/server/src/domain/recovery-tokens/recovery-token.repository.mongo.ts b/server/src/domain/recovery-tokens/recovery-token.repository.mongo.ts index 1e6140a88..02ba3089b 100644 --- a/server/src/domain/recovery-tokens/recovery-token.repository.mongo.ts +++ b/server/src/domain/recovery-tokens/recovery-token.repository.mongo.ts @@ -2,35 +2,22 @@ import type { RecoveryToken } from "@/domain/recovery-tokens/recovery-token.type import type { IRecoveryTokensRepository } from "./recovery-token.repository.interface.js"; import type { RecoveryTokenDocument } from "@/domain/recovery-tokens/recovery-token.model.js"; import { RecoveryTokenModel } from "@/domain/recovery-tokens/recovery-token.model.js"; -import mongoose from "mongoose"; import crypto from "crypto"; import { AppError } from "@/utils/AppError.js"; +import { toStringId, toDateString } from "@/utils/mongoMappers.js"; const SERVICE_NAME = "MongoRecoveryTokensRepository"; class MongoRecoveryTokensRepository implements IRecoveryTokensRepository { static SERVICE_NAME = SERVICE_NAME; - private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { - if (!value) { - return ""; - } - return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); - }; - private toDateString = (value?: Date | string | null): string => { - if (!value) { - return new Date(0).toISOString(); - } - return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); - }; - - protected toEntity = (doc: RecoveryTokenDocument): RecoveryToken => { + private toEntity = (doc: RecoveryTokenDocument): RecoveryToken => { return { - id: this.toStringId(doc._id), + id: toStringId(doc._id), email: doc.email, token: doc.token, - expiry: this.toDateString(doc.expiry), - createdAt: this.toDateString(doc.createdAt), - updatedAt: this.toDateString(doc.updatedAt), + expiry: toDateString(doc.expiry), + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), }; }; diff --git a/server/src/domain/status-pages/status-page-repository.mongo.ts b/server/src/domain/status-pages/status-page-repository.mongo.ts index 2dedca4ef..467ce10f5 100644 --- a/server/src/domain/status-pages/status-page-repository.mongo.ts +++ b/server/src/domain/status-pages/status-page-repository.mongo.ts @@ -4,28 +4,15 @@ import type { StatusPage, StatusPageLogo, StatusPageLogoDocument } from "@/domai import mongoose from "mongoose"; import { AppError } from "@/utils/AppError.js"; import { normalizeStatusPageDomain } from "@/utils/statusPageDomain.js"; - +import { toStringId, toDateString } from "@/utils/mongoMappers.js"; // Type for update data that can include document-level fields (Buffer for logo) type StatusPageUpdateData = Partial> & { logo?: StatusPageLogoDocument | null; }; + class MongoStatusPagesRepository implements IStatusPagesRepository { - private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { - if (!value) { - return ""; - } - return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); - }; - - private toDateString = (value?: Date | string | null): string => { - if (!value) { - return new Date(0).toISOString(); - } - return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); - }; - private mapIdArray = (values?: Array): string[] => { - return values?.map((value) => this.toStringId(value)) ?? []; + return values?.map((value) => toStringId(value)) ?? []; }; private mapLogo = (logo?: StatusPageLogoDocument | null): StatusPageLogo | undefined => { @@ -42,9 +29,9 @@ class MongoStatusPagesRepository implements IStatusPagesRepository { private toEntity = (doc: StatusPageDocument): StatusPage => { return { - id: this.toStringId(doc._id), - userId: this.toStringId(doc.userId), - teamId: this.toStringId(doc.teamId), + id: toStringId(doc._id), + userId: toStringId(doc.userId), + teamId: toStringId(doc.teamId), type: doc.type, companyName: doc.companyName, url: doc.url, @@ -63,8 +50,8 @@ class MongoStatusPagesRepository implements IStatusPagesRepository { customCSS: doc.customCSS, theme: doc.theme, themeMode: doc.themeMode, - createdAt: this.toDateString(doc.createdAt), - updatedAt: this.toDateString(doc.updatedAt), + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), }; }; diff --git a/server/src/domain/status-pages/status-page.service.ts b/server/src/domain/status-pages/status-page.service.ts index f7827041a..834f87384 100644 --- a/server/src/domain/status-pages/status-page.service.ts +++ b/server/src/domain/status-pages/status-page.service.ts @@ -9,7 +9,7 @@ import { } from "@/domain/status-pages/status-page.type.js"; import { AppError } from "@/utils/AppError.js"; import { normalizeStatusPageDomain } from "@/utils/statusPageDomain.js"; -import { Monitor } from "@/domain/monitors/monitor.types.js"; +import { Monitor } from "@/domain/monitors/monitor.type.js"; import { NormalizeData } from "@/utils/dataUtils.js"; export interface IStatusPageService { diff --git a/server/src/domain/status-pages/status-page.type.ts b/server/src/domain/status-pages/status-page.type.ts index d4642af17..65f331ad9 100644 --- a/server/src/domain/status-pages/status-page.type.ts +++ b/server/src/domain/status-pages/status-page.type.ts @@ -1,4 +1,4 @@ -import type { Monitor } from "@/domain/monitors/monitor.types.js"; +import type { Monitor } from "@/domain/monitors/monitor.type.js"; import type { NormalizedCheck, CheckSnapshot } from "@/domain/checks/check.type.js"; export const StatusPageTypes = ["uptime", "infrastructure"] as const; export type StatusPageType = (typeof StatusPageTypes)[number]; diff --git a/server/src/domain/tags/tag.repository.mongo.ts b/server/src/domain/tags/tag.repository.mongo.ts index 5d7d8d0a2..6345f23da 100644 --- a/server/src/domain/tags/tag.repository.mongo.ts +++ b/server/src/domain/tags/tag.repository.mongo.ts @@ -1,36 +1,22 @@ import { ITagsRepository } from "@/domain/tags/tag.repository.interface.js"; import { Tag } from "@/domain/tags/tag.type.js"; import { TagDocument, TagModel } from "@/domain/tags/tag.model.js"; -import mongoose from "mongoose"; import { AppError } from "@/utils/AppError.js"; +import { toStringId, toDateString } from "@/utils/mongoMappers.js"; const SERVICE_NAME = "TagsRepository"; class MongoTagsRepository implements ITagsRepository { static SERVICE_NAME = SERVICE_NAME; - private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { - if (!value) { - return ""; - } - return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); - }; - - private toDateString = (value?: Date | string | null): string => { - if (!value) { - return new Date(0).toISOString(); - } - return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); - }; - - protected toEntity = (doc: TagDocument): Tag => { + private toEntity = (doc: TagDocument): Tag => { return { - id: this.toStringId(doc._id), - teamId: this.toStringId(doc.teamId), + id: toStringId(doc._id), + teamId: toStringId(doc.teamId), name: doc.name, color: doc.color, - createdAt: this.toDateString(doc.createdAt), - updatedAt: this.toDateString(doc.updatedAt), + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), }; }; diff --git a/server/src/domain/teams/team.repository.model.ts b/server/src/domain/teams/team.repository.model.ts index d050d6f59..bcbc43938 100644 --- a/server/src/domain/teams/team.repository.model.ts +++ b/server/src/domain/teams/team.repository.model.ts @@ -1,29 +1,14 @@ import { Team } from "@/domain/teams/team.type.js"; import { TeamDocument, TeamModel } from "@/domain/teams/team.model.js"; import { ITeamsRepository } from "@/domain/teams/team.repository.interface.js"; -import mongoose from "mongoose"; - +import { toStringId, toDateString } from "@/utils/mongoMappers.js"; class MongoTeamsRepository implements ITeamsRepository { - private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { - if (!value) { - return ""; - } - return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); - }; - - private toDateString = (value?: Date | string | null): string => { - if (!value) { - return new Date(0).toISOString(); - } - return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); - }; - private toEntity = (doc: TeamDocument): Team => { return { - id: this.toStringId(doc._id), + id: toStringId(doc._id), email: doc.email, - createdAt: this.toDateString(doc.createdAt), - updatedAt: this.toDateString(doc.updatedAt), + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), }; }; @@ -34,7 +19,7 @@ class MongoTeamsRepository implements ITeamsRepository { findAllTeamIds = async (): Promise => { const teams = await TeamModel.find({}, { _id: 1 }).lean(); - return teams.map((team) => this.toStringId(team._id)); + return teams.map((team) => toStringId(team._id)); }; } diff --git a/server/src/domain/users/user.repository.mongo.ts b/server/src/domain/users/user.repository.mongo.ts index 1dc729b81..927df23ed 100644 --- a/server/src/domain/users/user.repository.mongo.ts +++ b/server/src/domain/users/user.repository.mongo.ts @@ -1,27 +1,13 @@ -import mongoose from "mongoose"; import { IUsersRepository } from "@/domain/users/user.repository.interface.js"; import { UserModel, type UserDocument } from "@/domain/users/user.model.js"; import type { User, UserProfileImage } from "@/domain/users/user.type.js"; import { GenerateAvatarImage } from "@/utils/imageProcessing.js"; -import { ParseBoolean } from "@/utils/utils.js"; import { AppError } from "@/utils/AppError.js"; +import { toStringId, toDateString } from "@/utils/mongoMappers.js"; const SERVICE_NAME = "MongoUsersRepository"; class MongoUsersRepository implements IUsersRepository { static SERVICE_NAME = SERVICE_NAME; - private toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { - if (!value) { - return ""; - } - return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); - }; - - private toDateString = (value?: Date | string | null): string => { - if (!value) { - return new Date(0).toISOString(); - } - return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); - }; private mapProfileImage = (image?: (UserProfileImage & { data?: Buffer }) | null) => { if (!image) { @@ -33,9 +19,9 @@ class MongoUsersRepository implements IUsersRepository { }; }; - protected toEntity = (doc: UserDocument): User => { + private toEntity = (doc: UserDocument): User => { return { - id: this.toStringId(doc._id), + id: toStringId(doc._id), firstName: doc.firstName, lastName: doc.lastName, email: doc.email, @@ -45,10 +31,10 @@ class MongoUsersRepository implements IUsersRepository { isActive: doc.isActive ?? false, isVerified: doc.isVerified ?? false, role: doc.role ?? [], - teamId: this.toStringId(doc.teamId), + teamId: toStringId(doc.teamId), checkTTL: doc.checkTTL ?? undefined, - createdAt: this.toDateString(doc.createdAt), - updatedAt: this.toDateString(doc.updatedAt), + createdAt: toDateString(doc.createdAt), + updatedAt: toDateString(doc.updatedAt), }; }; @@ -85,7 +71,7 @@ class MongoUsersRepository implements IUsersRepository { findById = async (id: string) => { const user = await UserModel.findById(id).select("-password").select("-profileImage"); if (!user) { - throw new Error("User not found"); + throw new AppError({ message: "User not found", service: SERVICE_NAME, status: 404 }); } return this.toEntity(user); @@ -100,9 +86,8 @@ class MongoUsersRepository implements IUsersRepository { const candidateUser = { ...patch }; let unsetFields: Record | undefined; - if (ParseBoolean(candidateUser.deleteProfileImage) === true) { + if (candidateUser.deleteProfileImage === true) { unsetFields = { profileImage: 1, avatarImage: 1 }; - delete candidateUser.deleteProfileImage; } else if (file) { // 1. Save the full size image candidateUser.profileImage = { diff --git a/server/src/domain/users/user.service.ts b/server/src/domain/users/user.service.ts index 7bdb9677f..f3db2f3b5 100644 --- a/server/src/domain/users/user.service.ts +++ b/server/src/domain/users/user.service.ts @@ -24,7 +24,7 @@ export interface IUserService { createUser(userData: Partial, teamId: string, actorRoles: UserRole[], file: Express.Multer.File | null): Promise; loginUser(email: string, password: string): Promise<{ user: User; token: string }>; editUser( - updates: Partial, + updates: Partial, file: Express.Multer.File | null, currentUserId: string, currentUserEmail: string @@ -103,10 +103,6 @@ export class UserService implements IUserService { this.teamsRepository = teamsRepository; } - get serviceName() { - return UserService.SERVICE_NAME; - } - issueToken = (payload: Partial, appSettings: EnvConfig) => { return this.jwt.sign(payload, appSettings.jwtSecret, { expiresIn: appSettings.jwtTTL }); }; @@ -242,7 +238,7 @@ export class UserService implements IUserService { }; editUser = async ( - updates: Partial, + updates: Partial, file: Express.Multer.File | null, currentUserId: string, currentUserEmail: string diff --git a/server/src/index.ts b/server/src/index.ts index 44536ce8b..68ef00a1f 100755 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -14,7 +14,7 @@ import Logger, { ILogger } from "@/utils/logger.js"; import { SettingsService } from "@/domain/app-settings/app-settings.service.js"; import { JobScheduler } from "@/worker/worker.job-scheduler.js"; import { IJobScheduler } from "@/worker/worker.interface.js"; -import { HealthServer } from "@/worker/worker.health-server.js"; +import { HealthServer, IHealthServer } from "@/worker/worker.health-server.js"; const SERVICE_NAME = "Server"; let logger: ILogger; @@ -27,7 +27,7 @@ const startApp = async () => { const env = validateEnv(logger); logger.setLogLevel(env.LOG_LEVEL); const settingsService = new SettingsService(env); - const envSettings = settingsService.loadSettings(); + const envSettings = settingsService.getSettings(); const { queueMode, queuePrimaryProcesses } = envSettings; logger.info({ @@ -63,12 +63,15 @@ const startApp = async () => { // Primary node path // *********************** let scheduler: IJobScheduler; + let healthServer: IHealthServer | undefined; // *********************** //Primary node processes jobs, need full worker // *********************** if (queuePrimaryProcesses === true) { const { worker } = await buildWorker(shared, envSettings); + healthServer = new HealthServer(logger, env.HEALTH_PORT, worker); + await healthServer.listen(); scheduler = worker; } // *********************** @@ -103,7 +106,7 @@ const startApp = async () => { logger.info({ message: `Server started on port:${env.PORT}` }); }); - initShutdownListener(server, services); + initShutdownListener(server, { ...services, healthServer }); }; startApp().catch((error) => { diff --git a/server/src/service/bufferService.ts b/server/src/service/bufferService.ts index d47a6472e..2d10aebbb 100755 --- a/server/src/service/bufferService.ts +++ b/server/src/service/bufferService.ts @@ -51,10 +51,6 @@ export class BufferService implements IBufferService { }); } - get serviceName() { - return BufferService.SERVICE_NAME; - } - addToBuffer(check: Check) { try { this.buffer.push(check); diff --git a/server/src/service/emailService.ts b/server/src/service/emailService.ts index c156cc18b..26b89f84b 100755 --- a/server/src/service/emailService.ts +++ b/server/src/service/emailService.ts @@ -56,10 +56,6 @@ export class EmailService implements IEmailService { this.init(); } - get serviceName() { - return EmailService.SERVICE_NAME; - } - init = () => { const loadTemplate = (templateName: string) => { try { @@ -79,7 +75,6 @@ export class EmailService implements IEmailService { this.templateLookup = { welcomeEmailTemplate: loadTemplate("welcomeEmail"), employeeActivationTemplate: loadTemplate("employeeActivation"), - noIncidentsThisWeekTemplate: loadTemplate("noIncidentsThisWeek"), passwordResetTemplate: loadTemplate("passwordReset"), testEmailTemplate: loadTemplate("testEmailTemplate"), unifiedNotificationTemplate: loadTemplate("unifiedNotification"), diff --git a/server/src/service/globalPingService.ts b/server/src/service/globalPingService.ts index cebd95408..5a175485f 100644 --- a/server/src/service/globalPingService.ts +++ b/server/src/service/globalPingService.ts @@ -1,6 +1,6 @@ import type { GeoContinent, GeoCheckResult, GeoCheckTimings, GeoCheckLocation } from "@/domain/geo-checks/geo-check.type.js"; -import { supportsGeoCheck, type HttpStatusCode } from "@/domain/monitors/monitor.types.js"; -import { MonitorType } from "@/domain/monitors/monitor.types.js"; +import { supportsGeoCheck, type HttpStatusCode } from "@/domain/monitors/monitor.type.js"; +import { MonitorType } from "@/domain/monitors/monitor.type.js"; import type { ILogger } from "@/utils/logger.js"; import got from "got"; import { isStatusUp } from "@/service/network/utils.js"; @@ -61,7 +61,6 @@ interface GlobalPingProbeResult { } export interface IGlobalPingService { - readonly serviceName: string; createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise; pollForResults(measurementId: string, timeoutMs?: number, customUpCodes?: HttpStatusCode[]): Promise; } @@ -75,10 +74,6 @@ export class GlobalPingService implements IGlobalPingService { this.logger = logger; } - get serviceName() { - return GlobalPingService.SERVICE_NAME; - } - async createMeasurement(monitorType: MonitorType, url: string, locations: GeoContinent[]): Promise { try { if (!supportsGeoCheck(monitorType)) { diff --git a/server/src/service/network/AdvancedMatcher.ts b/server/src/service/network/AdvancedMatcher.ts index 8e77ef3ec..af0195061 100644 --- a/server/src/service/network/AdvancedMatcher.ts +++ b/server/src/service/network/AdvancedMatcher.ts @@ -1,4 +1,4 @@ -import { Monitor } from "@/domain/monitors/monitor.types.js"; +import { Monitor } from "@/domain/monitors/monitor.type.js"; import jmespath from "jmespath"; import RE2 from "re2"; type JmesPath = typeof jmespath; diff --git a/server/src/service/network/DNSProvider.ts b/server/src/service/network/DNSProvider.ts index 1aeff7e14..10eff8d74 100644 --- a/server/src/service/network/DNSProvider.ts +++ b/server/src/service/network/DNSProvider.ts @@ -1,9 +1,10 @@ import { DNSStatusPayload, MonitorStatusResponse } from "@/types/network.js"; import { IStatusProvider } from "@/service/network/IStatusProvider.js"; import type { Resolver } from "dns/promises"; -import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js"; +import { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import { AppError } from "@/utils/AppError.js"; -import { NETWORK_ERROR, timeRequest } from "@/service/network/utils.js"; +import { timeRequest } from "@/service/network/utils.js"; +import { NETWORK_ERROR } from "@/types/network.js"; const SERVICE_NAME = "DNSProvider"; diff --git a/server/src/service/network/DockerProvider.ts b/server/src/service/network/DockerProvider.ts index 0cfb133af..9f2a99de7 100644 --- a/server/src/service/network/DockerProvider.ts +++ b/server/src/service/network/DockerProvider.ts @@ -1,11 +1,11 @@ import { IStatusProvider } from "@/service/network/IStatusProvider.js"; import { DockerStatusPayload, MonitorStatusResponse } from "@/types/network.js"; -import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js"; +import { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import { ILogger } from "@/utils/logger.js"; import { AppError } from "@/utils/AppError.js"; import Dockerode from "dockerode"; -import { NETWORK_ERROR, timeRequest } from "@/service/network/utils.js"; - +import { timeRequest } from "@/service/network/utils.js"; +import { NETWORK_ERROR } from "@/types/network.js"; type DockerodeType = typeof Dockerode; const SERVICE_NAME = "DockerProvider"; diff --git a/server/src/service/network/GameProvider.ts b/server/src/service/network/GameProvider.ts index 4e063396d..5ccfaee6e 100644 --- a/server/src/service/network/GameProvider.ts +++ b/server/src/service/network/GameProvider.ts @@ -1,9 +1,9 @@ import { IStatusProvider } from "@/service/network/IStatusProvider.js"; import { GameStatusPayload, MonitorStatusResponse } from "@/types/network.js"; -import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js"; +import { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import { GameDig } from "gamedig"; import { AppError } from "@/utils/AppError.js"; -import { NETWORK_ERROR } from "@/service/network/utils.js"; +import { NETWORK_ERROR } from "@/types/network.js"; import { ILogger } from "@/utils/logger.js"; type GameDigType = typeof GameDig; diff --git a/server/src/service/network/GrpcProvider.ts b/server/src/service/network/GrpcProvider.ts index 4e79cef7f..340fdc01a 100644 --- a/server/src/service/network/GrpcProvider.ts +++ b/server/src/service/network/GrpcProvider.ts @@ -1,6 +1,6 @@ import { IStatusProvider } from "@/service/network/IStatusProvider.js"; import { GrpcStatusPayload, MonitorStatusResponse } from "@/types/network.js"; -import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js"; +import { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import { AppError } from "@/utils/AppError.js"; import { timeRequest } from "@/service/network/utils.js"; import * as grpc from "@grpc/grpc-js"; diff --git a/server/src/service/network/HardwareProvider.ts b/server/src/service/network/HardwareProvider.ts index c612bdbad..840dbc2e8 100644 --- a/server/src/service/network/HardwareProvider.ts +++ b/server/src/service/network/HardwareProvider.ts @@ -1,6 +1,6 @@ import { IStatusProvider } from "@/service/network/IStatusProvider.js"; import { HardwareStatusPayload, MonitorStatusResponse } from "@/types/network.js"; -import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js"; +import { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import { HttpProvider } from "@/service/network/HttpProvider.js"; import { AppError } from "@/utils/AppError.js"; diff --git a/server/src/service/network/HttpProvider.ts b/server/src/service/network/HttpProvider.ts index 2f08dab00..fb5f9fea5 100644 --- a/server/src/service/network/HttpProvider.ts +++ b/server/src/service/network/HttpProvider.ts @@ -5,8 +5,9 @@ import { HttpStatusPayload } from "@/types/network.js"; import { MonitorStatusResponse } from "@/types/network.js"; import { Agent as HttpsAgent } from "https"; import { Agent as HttpAgent } from "http"; -import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js"; -import { NETWORK_ERROR, isStatusUp } from "@/service/network/utils.js"; +import { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; +import { isStatusUp } from "@/service/network/utils.js"; +import { NETWORK_ERROR } from "@/types/network.js"; import CacheableLookup from "cacheable-lookup"; export class HttpProvider implements IStatusProvider { @@ -116,7 +117,7 @@ export class HttpProvider implements IStatusProvider { if (error instanceof HTTPError || error instanceof RequestError) { const statusCode = error.response?.statusCode; const statusUp = isStatusUp(statusCode, monitor.customUpCodes); - const responseTime = error.timings?.phases?.firstByte ?? error.timings?.phases?.total ?? 0; + const responseTime = error.timings?.phases?.total ?? 0; if (!statusUp) { return { @@ -183,7 +184,7 @@ export class HttpProvider implements IStatusProvider { statusCode: response.statusCode, statusUp, message: response.statusMessage ?? "OK", - responseTime: response.timings.phases.firstByte ?? 0, + responseTime: response.timings.phases.total ?? 0, timings: response.timings, }); } catch (error: unknown) { diff --git a/server/src/service/network/IStatusProvider.ts b/server/src/service/network/IStatusProvider.ts index f077ff9b5..6da91b30f 100644 --- a/server/src/service/network/IStatusProvider.ts +++ b/server/src/service/network/IStatusProvider.ts @@ -1,4 +1,4 @@ -import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js"; +import { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import { MonitorStatusResponse } from "@/types/network.js"; export interface IStatusProvider { diff --git a/server/src/service/network/PageSpeedProvider.ts b/server/src/service/network/PageSpeedProvider.ts index a609ca80b..dfa29a5c9 100644 --- a/server/src/service/network/PageSpeedProvider.ts +++ b/server/src/service/network/PageSpeedProvider.ts @@ -1,6 +1,6 @@ import { IStatusProvider } from "@/service/network/IStatusProvider.js"; import { MonitorStatusResponse, PageSpeedStatusPayload } from "@/types/network.js"; -import { Monitor, MonitorType, DefaultPageSpeedStrategy } from "@/domain/monitors/monitor.types.js"; +import { Monitor, MonitorType, DefaultPageSpeedStrategy } from "@/domain/monitors/monitor.type.js"; import { HttpProvider } from "@/service/network/HttpProvider.js"; import { ISettingsService } from "@/domain/app-settings/app-settings.service.js"; import { ILogger } from "@/utils/logger.js"; diff --git a/server/src/service/network/PingProvider.ts b/server/src/service/network/PingProvider.ts index ee66046f9..53f6aad0d 100644 --- a/server/src/service/network/PingProvider.ts +++ b/server/src/service/network/PingProvider.ts @@ -1,6 +1,6 @@ import { PingStatusPayload } from "@/types/network.js"; import { IStatusProvider } from "./IStatusProvider.js"; -import { MonitorType, Monitor } from "@/domain/monitors/monitor.types.js"; +import { MonitorType, Monitor } from "@/domain/monitors/monitor.type.js"; import { MonitorStatusResponse } from "@/types/network.js"; import { AppError } from "@/utils/AppError.js"; import ping from "ping"; diff --git a/server/src/service/network/PortProvider.ts b/server/src/service/network/PortProvider.ts index debcd8509..692ca40af 100644 --- a/server/src/service/network/PortProvider.ts +++ b/server/src/service/network/PortProvider.ts @@ -1,8 +1,9 @@ import { IStatusProvider } from "@/service/network/IStatusProvider.js"; import { PortStatusPayload, MonitorStatusResponse } from "@/types/network.js"; -import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js"; +import { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import { AppError } from "@/utils/AppError.js"; -import { NETWORK_ERROR, timeRequest } from "@/service/network/utils.js"; +import { timeRequest } from "@/service/network/utils.js"; +import { NETWORK_ERROR } from "@/types/network.js"; import * as net from "net"; type NetType = typeof net; diff --git a/server/src/service/network/WebSocketProvider.ts b/server/src/service/network/WebSocketProvider.ts index fef5f76b1..d27106989 100644 --- a/server/src/service/network/WebSocketProvider.ts +++ b/server/src/service/network/WebSocketProvider.ts @@ -1,8 +1,9 @@ import { IStatusProvider } from "@/service/network/IStatusProvider.js"; import { WebSocketStatusPayload, MonitorStatusResponse } from "@/types/network.js"; -import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js"; +import { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import { AppError } from "@/utils/AppError.js"; -import { NETWORK_ERROR, timeRequest } from "@/service/network/utils.js"; +import { timeRequest } from "@/service/network/utils.js"; +import { NETWORK_ERROR } from "@/types/network.js"; import type WebSocket from "ws"; type WebSocketConstructor = typeof WebSocket; diff --git a/server/src/service/network/utils.ts b/server/src/service/network/utils.ts index 8bcbc4335..1a0e157ea 100644 --- a/server/src/service/network/utils.ts +++ b/server/src/service/network/utils.ts @@ -1,4 +1,4 @@ -import type { HttpStatusCode } from "@/domain/monitors/monitor.types.js"; +import type { HttpStatusCode } from "@/domain/monitors/monitor.type.js"; export const timeRequest = async (operation: () => Promise): Promise<{ response: T | null; responseTime: number; error: unknown }> => { const start = process.hrtime.bigint(); @@ -12,9 +12,6 @@ export const timeRequest = async (operation: () => Promise): Promise<{ res } }; -export const NETWORK_ERROR = 5000; -export const PING_ERROR = 5001; - export const isStatusUp = (statusCode: number | undefined, customUpCodes: HttpStatusCode[] = []): boolean => { if (statusCode === undefined) return false; return (statusCode >= 200 && statusCode < 300) || customUpCodes.includes(statusCode); diff --git a/server/src/service/networkService.ts b/server/src/service/networkService.ts index 3470c6086..75ff66bda 100644 --- a/server/src/service/networkService.ts +++ b/server/src/service/networkService.ts @@ -1,14 +1,13 @@ -import type { Monitor, MonitorType } from "@/domain/monitors/monitor.types.js"; +import type { Monitor, MonitorType } from "@/domain/monitors/monitor.type.js"; import type { MonitorPayloadMap, MonitorStatusResponse } from "@/types/network.js"; import type { AxiosStatic } from "axios"; import { AppError } from "@/utils/AppError.js"; -import { NETWORK_ERROR } from "@/service/network/utils.js"; +import { NETWORK_ERROR } from "@/types/network.js"; import { ILogger } from "@/utils/logger.js"; import { IStatusProvider } from "./network/IStatusProvider.js"; const SERVICE_NAME = "NetworkService"; export interface INetworkService { - readonly serviceName: string; requestStatus(monitor: Monitor & { type: T }): Promise>; requestWebhook( type: string, @@ -39,10 +38,6 @@ export class NetworkService implements INetworkService { this.logger = logger; } - get serviceName(): string { - return NetworkService.SERVICE_NAME; - } - // Main entry point async requestStatus(monitor: Monitor & { type: T }): Promise> { const provider = this.providers.find((p) => p.supports(monitor.type)); diff --git a/server/src/service/statusService.ts b/server/src/service/statusService.ts index ccda0c5d7..748960a37 100755 --- a/server/src/service/statusService.ts +++ b/server/src/service/statusService.ts @@ -1,7 +1,7 @@ import { IMonitorStatsRepository } from "@/domain/monitor-stats/monitor-stats.repository.interface.js"; import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js"; import type { Check, CheckDiskInfo, CheckSnapshot } from "@/domain/checks/check.type.js"; -import type { Monitor, MonitorStatus } from "@/domain/monitors/monitor.types.js"; +import type { Monitor, MonitorStatus } from "@/domain/monitors/monitor.type.js"; import type { DockerStatusPayload, GameStatusPayload, @@ -17,7 +17,7 @@ import type { import { AppError } from "@/utils/AppError.js"; import { ILogger } from "@/utils/logger.js"; import type { HardwareStatusMetrics } from "@/types/network.js"; -import { MAX_RECENT_CHECKS } from "@/domain/monitors/monitor.types.js"; +import { MAX_RECENT_CHECKS } from "@/domain/monitors/monitor.type.js"; const SERVICE_NAME = "StatusService"; const HARDWARE_ALERT_COUNTER_START = 5; @@ -57,10 +57,6 @@ export class StatusService implements IStatusService { this.monitorStatsRepository = monitorStatsRepository; } - get serviceName() { - return StatusService.SERVICE_NAME; - } - async updateRunningStats(monitor: Monitor, networkResponse: MonitorStatusResponse) { try { await this.monitorStatsRepository.updateByMonitorId(monitor.id, { diff --git a/server/src/templates/addReview.mjml b/server/src/templates/addReview.mjml deleted file mode 100755 index e193ac1ba..000000000 --- a/server/src/templates/addReview.mjml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - Message from Checkmate Service - - - - - - -

Hello {{ name }}!

-

- We hope youโ€™re finding Checkmate helpful in monitoring your infrastructure. Your support means a lot to us, and we - truly appreciate having you as part of our community. -

-

- If youโ€™re happy with Checkmate, weโ€™d love to hear about your experience! Leaving a review on G2 helps others discover Checkmate and - supports our ongoing improvements. -

- G2 Link: TBD -

Thank you for taking the time to share your thoughts - we greatly appreciate it!

- Checkmate Team -
-
- - - -

This email was sent by Checkmate.

-
-
-
-
-
diff --git a/server/src/templates/noIncidentsThisWeek.mjml b/server/src/templates/noIncidentsThisWeek.mjml deleted file mode 100755 index d160e44de..000000000 --- a/server/src/templates/noIncidentsThisWeek.mjml +++ /dev/null @@ -1,66 +0,0 @@ - - - - - - - - - - - - - Message from Checkmate Service - - - No incidents this week! - - - - - - -

Hello {{ name }}!

-

There were no incidents this week. Good job!

-

Current monitors:

-

Google: 100% availability

-

Canada.ca:100% availability

-
-
- - - -

This email was sent by Checkmate.

-
-
-
-
-
diff --git a/server/src/types/network.ts b/server/src/types/network.ts index fe85221f2..2839ed95f 100644 --- a/server/src/types/network.ts +++ b/server/src/types/network.ts @@ -9,10 +9,12 @@ import type { GotTimings, ILighthouseAudit, } from "@/domain/checks/check.type.js"; -import type { DnsRecordType, Monitor, MonitorMatchMethod, MonitorStatus, MonitorType } from "@/domain/monitors/monitor.types.js"; +import type { DnsRecordType, Monitor, MonitorMatchMethod, MonitorStatus, MonitorType } from "@/domain/monitors/monitor.type.js"; import type { QueryResult } from "gamedig"; +export const NETWORK_ERROR = 5000; + export interface MonitorStatusResponse< T = | HttpStatusPayload diff --git a/server/src/types/query.ts b/server/src/types/query.ts new file mode 100644 index 000000000..9809517ee --- /dev/null +++ b/server/src/types/query.ts @@ -0,0 +1,8 @@ +export const DateRanges = ["recent", "hour", "day", "week", "month", "all"] as const; +export type DateRange = (typeof DateRanges)[number]; + +export const SortOrders = ["asc", "desc"] as const; +export type SortOrder = (typeof SortOrders)[number]; + +export const CheckFilters = ["all", "up", "down", "resolve"] as const; +export type CheckFilter = (typeof CheckFilters)[number]; diff --git a/server/src/utils/catchAsync.ts b/server/src/utils/catchAsync.ts new file mode 100644 index 000000000..3f207608b --- /dev/null +++ b/server/src/utils/catchAsync.ts @@ -0,0 +1,14 @@ +import type { NextFunction, Request, RequestHandler, Response } from "express"; +type AsyncRequestHandler = (req: Request, res: Response) => Promise; + +/** + * Wraps an async route handler so that any error it throws or any promise it + * rejects is passed to Express's error middleware (`handleErrors`). Every async controller method must be wrapped. + */ +export const catchAsync = (fn: AsyncRequestHandler): RequestHandler => { + const wrappedHandler = (req: Request, res: Response, next: NextFunction) => { + fn(req, res).catch(next); + }; + + return wrappedHandler; +}; diff --git a/server/src/utils/dataUtils.ts b/server/src/utils/dataUtils.ts index 7b46619e8..2c15b943e 100755 --- a/server/src/utils/dataUtils.ts +++ b/server/src/utils/dataUtils.ts @@ -1,6 +1,7 @@ -import type { GroupedCheck, HasResponseTime, NormalizedCheck, NormalizedUptimeCheck } from "@/domain/checks/check.type.js"; +import type { HasResponseTime, NormalizedCheck } from "@/domain/checks/check.type.js"; +import { type DateRange } from "@/types/query.js"; -export const getDateForRange = (dateRange: string): Date | undefined => { +export const getDateForRange = (dateRange: DateRange): Date => { const now = Date.now(); switch (dateRange) { case "recent": @@ -14,78 +15,49 @@ export const getDateForRange = (dateRange: string): Date | undefined => { case "month": return new Date(now - 30 * 24 * 60 * 60 * 1000); // 30 days case "all": - return undefined; + return new Date(0); default: - return undefined; + return new Date(0); } }; -const calculatePercentile = (arr: T[], percentile: number): number => { - const sorted = arr.slice().sort((a, b) => a.responseTime - b.responseTime); - const index = (percentile / 100) * (sorted.length - 1); - const lower = Math.floor(index); - const upper = lower + 1; - const weight = index % 1; - if (upper >= sorted.length) return sorted[lower]!.responseTime; - return sorted[lower]!.responseTime * (1 - weight) + sorted[upper]!.responseTime * weight; +export const getDateFormat = (dateRange: DateRange): string => { + const formatLookup = { + hour: "%Y-%m-%dT%H:%M:00Z", + recent: "%Y-%m-%dT%H:%M:00Z", + day: "%Y-%m-%dT%H:00:00Z", + week: "%Y-%m-%dT00:00:00Z", + month: "%Y-%m-%dT00:00:00Z", + all: "%Y-%m-%dT00:00:00Z", + }; + return formatLookup[dateRange]; }; -const calculatePercentileUptimeDetails = (arr: GroupedCheck[], percentile: number): number => { - const sorted = arr.slice().sort((a, b) => a.avgResponseTime - b.avgResponseTime); +const percentileBy = (arr: T[], percentile: number, value: (item: T) => number): number => { + const sorted = arr.slice().sort((a, b) => value(a) - value(b)); const index = (percentile / 100) * (sorted.length - 1); const lower = Math.floor(index); const upper = lower + 1; const weight = index % 1; - if (upper >= sorted.length) return sorted[lower]!.avgResponseTime; - return sorted[lower]!.avgResponseTime * (1 - weight) + sorted[upper]!.avgResponseTime * weight; + if (upper >= sorted.length) return value(sorted[lower]!); + return value(sorted[lower]!) * (1 - weight) + value(sorted[upper]!) * weight; +}; + +const rescale = (raw: number, min: number, max: number, rangeMin: number, rangeMax: number): number => { + const scaled = rangeMin + ((raw - min) * (rangeMax - rangeMin)) / (max - min); + return Math.max(rangeMin, Math.min(rangeMax, scaled)); }; export const NormalizeData = (checks: T[], rangeMin: number, rangeMax: number): NormalizedCheck[] => { - if (checks.length > 1) { - const min = calculatePercentile(checks, 0); - const max = calculatePercentile(checks, 95); - const normalizedChecks = checks.map((check) => { - const originalResponseTime = typeof check.responseTime === "number" ? check.responseTime : 0; - let normalizedResponseTime = rangeMin + ((check.responseTime - min) * (rangeMax - rangeMin)) / (max - min); - - normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime)); - return { - ...check, - responseTime: normalizedResponseTime, - originalResponseTime: originalResponseTime, - }; - }); - - return normalizedChecks; - } else { - return checks.map((check) => { - const originalResponseTime = typeof check.responseTime === "number" ? check.responseTime : 0; - return { ...check, originalResponseTime: originalResponseTime }; - }); - } -}; - -export const NormalizeDataUptimeDetails = (checks: T[], rangeMin: number, rangeMax: number): NormalizedUptimeCheck[] => { - if (checks.length > 1) { - const min = calculatePercentileUptimeDetails(checks, 0); - const max = calculatePercentileUptimeDetails(checks, 95); - - const normalizedChecks = checks.map((check) => { - const originalResponseTime = check.avgResponseTime; - let normalizedResponseTime = rangeMin + ((check.avgResponseTime - min) * (rangeMax - rangeMin)) / (max - min); - - normalizedResponseTime = Math.max(rangeMin, Math.min(rangeMax, normalizedResponseTime)); - return { - ...check, - avgResponseTime: normalizedResponseTime, - originalAvgResponseTime: originalResponseTime, - }; - }); - - return normalizedChecks; - } else { - return checks.map((check) => { - return { ...check, originalAvgResponseTime: check.avgResponseTime }; - }); + const original = (check: T) => (typeof check.responseTime === "number" ? check.responseTime : 0); + if (checks.length <= 1) { + return checks.map((check) => ({ ...check, originalResponseTime: original(check) })); } + const min = percentileBy(checks, 0, (c) => c.responseTime); + const max = percentileBy(checks, 95, (c) => c.responseTime); + return checks.map((check) => ({ + ...check, + responseTime: rescale(check.responseTime, min, max, rangeMin, rangeMax), + originalResponseTime: original(check), + })); }; diff --git a/server/src/utils/logger.ts b/server/src/utils/logger.ts index 41142fd92..3cd8570ca 100755 --- a/server/src/utils/logger.ts +++ b/server/src/utils/logger.ts @@ -19,7 +19,6 @@ interface LogEntry extends LogConfig { } export interface ILogger { - readonly serviceName: string; info(config: LogConfig): void; warn(config: LogConfig): void; error(config: LogConfig): void; @@ -97,10 +96,6 @@ class Logger implements ILogger { }); } - get serviceName() { - return Logger.SERVICE_NAME; - } - info(config: LogConfig) { const logEntry = this.buildLogEntry("info", config); this.cacheLog(logEntry); diff --git a/server/src/utils/mongoMappers.ts b/server/src/utils/mongoMappers.ts new file mode 100644 index 000000000..c817c1e6c --- /dev/null +++ b/server/src/utils/mongoMappers.ts @@ -0,0 +1,15 @@ +import mongoose from "mongoose"; + +export const toStringId = (value?: mongoose.Types.ObjectId | string | null): string => { + if (!value) { + return ""; + } + return value instanceof mongoose.Types.ObjectId ? value.toString() : String(value); +}; + +export const toDateString = (value?: Date | string | null): string => { + if (!value) { + return new Date(0).toISOString(); + } + return value instanceof Date ? value.toISOString() : new Date(value).toISOString(); +}; diff --git a/server/src/utils/utils.ts b/server/src/utils/utils.ts deleted file mode 100755 index a82d8734f..000000000 --- a/server/src/utils/utils.ts +++ /dev/null @@ -1,17 +0,0 @@ -export const ParseBoolean = (value: boolean | string | null | undefined) => { - if (value === true || value === "true") { - return true; - } else if (value === false || value === "false" || value === null || value === undefined) { - return false; - } -}; - -export const getTokenFromHeaders = (headers: Record) => { - const authorizationHeader = headers.authorization; - if (!authorizationHeader) throw new Error("No auth headers"); - - const parts = authorizationHeader.split(" "); - if (parts.length !== 2 || parts[0] !== "Bearer") throw new Error("Invalid auth headers"); - - return parts[1]; -}; diff --git a/server/src/worker/worker.check-evaluator.ts b/server/src/worker/worker.check-evaluator.ts index 34c444271..da7e13dbe 100644 --- a/server/src/worker/worker.check-evaluator.ts +++ b/server/src/worker/worker.check-evaluator.ts @@ -1,7 +1,7 @@ import { MonitorStatusResponse } from "@/types/network.js"; import { MonitorEvaluation } from "@/worker/worker.interface.js"; import { Check } from "@/domain/checks/check.type.js"; -import { Monitor } from "@/domain/monitors/monitor.types.js"; +import { Monitor } from "@/domain/monitors/monitor.type.js"; import { IMonitorStatusPolicy } from "@/worker/worker.monitor-status-policy.js"; import { IStatusService } from "@/service/statusService.js"; diff --git a/server/src/worker/worker.check-pipeline.ts b/server/src/worker/worker.check-pipeline.ts index 045cea1ad..9b97de354 100644 --- a/server/src/worker/worker.check-pipeline.ts +++ b/server/src/worker/worker.check-pipeline.ts @@ -1,11 +1,11 @@ -import { Monitor } from "@/domain/monitors/monitor.types.js"; +import { Monitor } from "@/domain/monitors/monitor.type.js"; import { MonitorEvaluation } from "@/worker/worker.interface.js"; import { IMaintenanceWindowsRepository } from "@/domain/maintenance-windows/maintenance-window.repository.interface.js"; import { isWindowActive } from "@/utils/maintenanceWindow.js"; import { ILogger } from "@/utils/logger.js"; import { IBufferService } from "@/service/bufferService.js"; import { AppError } from "@/utils/AppError.js"; -import { supportsGeoCheck } from "@/domain/monitors/monitor.types.js"; +import { supportsGeoCheck } from "@/domain/monitors/monitor.type.js"; import { IGeoChecksService } from "@/domain/geo-checks/geo-check.service.js"; const SERVICE_NAME = "CheckPipeline"; diff --git a/server/src/worker/worker.check-producer.ts b/server/src/worker/worker.check-producer.ts index 3365ea93b..b73e2c6de 100644 --- a/server/src/worker/worker.check-producer.ts +++ b/server/src/worker/worker.check-producer.ts @@ -1,4 +1,4 @@ -import { Monitor } from "@/domain/monitors/monitor.types.js"; +import { Monitor } from "@/domain/monitors/monitor.type.js"; import { Check } from "@/domain/checks/check.type.js"; import { MonitorStatusResponse } from "@/types/network.js"; import { AppError } from "@/utils/AppError.js"; diff --git a/server/src/worker/worker.db-queue.ts b/server/src/worker/worker.db-queue.ts index 3ea2fddb8..6f4f31353 100644 --- a/server/src/worker/worker.db-queue.ts +++ b/server/src/worker/worker.db-queue.ts @@ -11,14 +11,13 @@ import { ICheckPipeline } from "@/worker/worker.check-pipeline.js"; import { IReactorDispatcher } from "@/worker/reactors/reactor.dispatcher.js"; import { IWorkerHelper } from "@/worker/worker.helper.js"; import { IQueueWorkersRepository } from "@/domain/queue-workers/queue-worker.repository.interface.js"; -import { WORKER_TTL_SECONDS } from "@/domain/queue-workers/queue-worker.model.js"; +import { WORKER_STALE_MS } from "@/domain/queue-workers/queue-worker.model.js"; import { QueueMode } from "@/domain/app-settings/app-settings.type.js"; import { JobScheduler } from "@/worker/worker.job-scheduler.js"; import { IBufferService } from "@/service/bufferService.js"; +import { POLL_MS } from "@/worker/worker.job-scheduler.js"; const SERVICE_NAME = "JobQueue"; -const POLL_MS = 250; // base poll interval while a loop is actively claiming work const POLL_MAX_MS = 5000; // back off poll interval to 5s if no jobs -const WORKER_STALE_MS = WORKER_TTL_SECONDS * 1000; // a worker counts as alive if seen within this window const LOCK_RENEW_MS = LOCK_MS / 3; // renew an in-flight job's lock at 1/3 the lease, so it survives two missed renewals const DRAIN_TIMEOUT_MS = 25_000; const DRAIN_POLL_MS = 200; @@ -31,6 +30,25 @@ const CONCURRENCY: Record = { "cleanup-retention": 1, }; +export interface DBQueueWorkerDependencies { + logger: ILogger; // forwarded to super + isDbConnected: () => boolean; + jobsRepository: IJobsRepository; // forwarded + monitorsRepository: IMonitorsRepository; // forwarded + checksRepository: IChecksRepository; + checkService: ICheckService; + bufferService: IBufferService; + checkProducer: ICheckProducer; + checkEvaluator: ICheckEvaluator; + geoCheckPipeline: ICheckPipeline; + dispatcher: IReactorDispatcher; + helper: IWorkerHelper; + queueWorkersRepository: IQueueWorkersRepository; // forwarded + queueMode: QueueMode; // forwarded + queuePrimaryProcesses: boolean; // consumed immediately + workerId: string; +} + export class DBQueueWorker extends JobScheduler implements IQueueWorker { static SERVICE_NAME = SERVICE_NAME; @@ -45,68 +63,39 @@ export class DBQueueWorker extends JobScheduler implements IQueueWorker { "cleanup-retention": 0, }; - constructor( - logger: ILogger, // forwarded to super - private isDbConnected: () => boolean, - jobsRepository: IJobsRepository, // forwarded - monitorsRepository: IMonitorsRepository, // forwarded - private checksRepository: IChecksRepository, - private checkService: ICheckService, - private bufferService: IBufferService, - private checkProducer: ICheckProducer, - private checkEvaluator: ICheckEvaluator, - private geoCheckPipeline: ICheckPipeline, - private dispatcher: IReactorDispatcher, - private helper: IWorkerHelper, - queueWorkersRepository: IQueueWorkersRepository, // forwarded - queueMode: QueueMode, // forwarded - queuePrimaryProcesses: boolean, // consumed immediately - workerId: string - ) { - super(jobsRepository, queueWorkersRepository, monitorsRepository, queueMode, logger, workerId); - this.processesJobs = queuePrimaryProcesses === true || queueMode === "worker"; - } + private checksRepository: IChecksRepository; + private checkProducer: ICheckProducer; + private checkService: ICheckService; + private checkEvaluator: ICheckEvaluator; + private dispatcher: IReactorDispatcher; + private geoCheckPipeline: ICheckPipeline; + private helper: IWorkerHelper; + private bufferService: IBufferService; + private isDbConnected: () => boolean; - get serviceName() { - return DBQueueWorker.SERVICE_NAME; - } - - static async create( - logger: ILogger, - isDbConnected: () => boolean, - jobsRepository: IJobsRepository, - monitorsRepository: IMonitorsRepository, - checksRepository: IChecksRepository, - checkService: ICheckService, - bufferService: IBufferService, - checkProducer: ICheckProducer, - checkEvaluator: ICheckEvaluator, - geoCheckPipeline: ICheckPipeline, - dispatcher: IReactorDispatcher, - helper: IWorkerHelper, - queueWorkersRepository: IQueueWorkersRepository, - queueMode: QueueMode, - queuePrimaryProcesses: boolean, - workerId: string - ): Promise { - const instance = new DBQueueWorker( - logger, - isDbConnected, - jobsRepository, - monitorsRepository, - checksRepository, - checkService, - bufferService, - checkProducer, - checkEvaluator, - geoCheckPipeline, - dispatcher, - helper, - queueWorkersRepository, - queueMode, - queuePrimaryProcesses, - workerId + constructor(dependencies: DBQueueWorkerDependencies) { + super( + dependencies.jobsRepository, + dependencies.queueWorkersRepository, + dependencies.monitorsRepository, + dependencies.queueMode, + dependencies.logger, + dependencies.workerId ); + this.processesJobs = dependencies.queuePrimaryProcesses === true || dependencies.queueMode === "worker"; + this.checksRepository = dependencies.checksRepository; + this.checkProducer = dependencies.checkProducer; + this.checkService = dependencies.checkService; + this.checkEvaluator = dependencies.checkEvaluator; + this.dispatcher = dependencies.dispatcher; + this.geoCheckPipeline = dependencies.geoCheckPipeline; + this.helper = dependencies.helper; + this.bufferService = dependencies.bufferService; + this.isDbConnected = dependencies.isDbConnected; + } + + static async create(dependencies: DBQueueWorkerDependencies): Promise { + const instance = new DBQueueWorker(dependencies); await instance.init(); return instance; } diff --git a/server/src/worker/worker.helper.ts b/server/src/worker/worker.helper.ts index 3d2d2e183..9f044763d 100644 --- a/server/src/worker/worker.helper.ts +++ b/server/src/worker/worker.helper.ts @@ -12,7 +12,6 @@ import { ITeamsRepository } from "@/domain/teams/team.repository.interface.js"; import { ILogger } from "@/utils/logger.js"; export interface IWorkerHelper { - readonly serviceName: string; getCleanupOrphanedJob(): () => Promise; getCleanupRetentionJob(): () => Promise; } @@ -69,10 +68,6 @@ export class WorkerHelper implements IWorkerHelper { this.geoChecksRepository = geoChecksRepository; } - get serviceName() { - return WorkerHelper.SERVICE_NAME; - } - getCleanupOrphanedJob = () => { return async () => { try { diff --git a/server/src/worker/worker.interface.ts b/server/src/worker/worker.interface.ts index 98120fa47..bb6cb0c53 100644 --- a/server/src/worker/worker.interface.ts +++ b/server/src/worker/worker.interface.ts @@ -1,4 +1,4 @@ -import { Monitor } from "@/domain/monitors/monitor.types.js"; +import { Monitor } from "@/domain/monitors/monitor.type.js"; import { Check } from "@/domain/checks/check.type.js"; import { JobType } from "@/domain/jobs/job.type.js"; import { MonitorPayloadMap, MonitorStatusResponse, StatusChangeResult } from "@/types/network.js"; @@ -67,7 +67,6 @@ export type WorkerHealth = { inFlight: number; // total in-flight jobs across types }; export interface IJobScheduler { - readonly serviceName: string; addJob(monitorId: string, monitor: Monitor): Promise; deleteJob(monitor: Monitor): Promise; pauseJob(monitor: Monitor): Promise; @@ -83,7 +82,6 @@ export interface IJobScheduler { } export interface IQueueWorker extends IJobScheduler { - readonly serviceName: string; getHealth(): WorkerHealth; countDueBacklog(): Promise; countAliveWorkers(): Promise; diff --git a/server/src/worker/worker.job-scheduler.ts b/server/src/worker/worker.job-scheduler.ts index 458db1d08..8d103e87f 100644 --- a/server/src/worker/worker.job-scheduler.ts +++ b/server/src/worker/worker.job-scheduler.ts @@ -1,17 +1,16 @@ import { IJobScheduler } from "@/worker/worker.interface.js"; import { IJobsRepository } from "@/domain/jobs/job.repository.interface.js"; import { IMonitorsRepository } from "@/domain/monitors/monitor.repository.interface.js"; -import { Monitor, supportsGeoCheck } from "@/domain/monitors/monitor.types.js"; +import { Monitor, supportsGeoCheck } from "@/domain/monitors/monitor.type.js"; import { type Job, type JobSeed, type JobType, jobId } from "@/domain/jobs/job.type.js"; import { IQueueWorkersRepository } from "@/domain/queue-workers/queue-worker.repository.interface.js"; import { WorkerJobsPagination, WorkerJobSummary, WorkerMetrics } from "@/worker/worker.interface.js"; -import { WORKER_TTL_SECONDS } from "@/domain/queue-workers/queue-worker.model.js"; +import { WORKER_STALE_MS } from "@/domain/queue-workers/queue-worker.model.js"; import { QueueMode } from "@/domain/app-settings/app-settings.type.js"; import { ILogger } from "@/utils/logger.js"; -const POLL_MS = 250; // base poll interval while a loop is actively claiming work +export const POLL_MS = 250; // base poll interval while a loop is actively claiming work const SERVICE_NAME = "JobScheduler"; -const WORKER_STALE_MS = WORKER_TTL_SECONDS * 1000; // a worker counts as alive if seen within this window const HEARTBEAT_MS = WORKER_STALE_MS / 3; // Worker can miss two beats without being considered stale const CLEANUP_INTERVAL_MS = 24 * 60 * 60 * 1000; @@ -26,9 +25,6 @@ export class JobScheduler implements IJobScheduler { protected readonly workerId: string // same id MongoJobsRepository stamps into lockedBy ) {} - get serviceName() { - return JobScheduler.SERVICE_NAME; - } private heartbeatInFlight: Promise = Promise.resolve(); protected processesJobs = false; // scheduler-only by default; DBQueueWorker flips this on when it runs job loops @@ -191,7 +187,7 @@ export class JobScheduler implements IJobScheduler { // Seed queue protected reconcile = async () => { const now = Date.now(); - const monitors = (await this.monitorsRepository.findAll()) ?? []; + const monitors = await this.monitorsRepository.findAll(); for (const monitor of monitors) { await this.jobsRepository.upsertJob(this.toCheckJob(monitor, now)); if (supportsGeoCheck(monitor.type) && monitor.geoCheckEnabled) { diff --git a/server/test/helpers/InMemoryMonitorsRepository.ts b/server/test/helpers/InMemoryMonitorsRepository.ts index 45db5b3f7..1ff0b0383 100644 --- a/server/test/helpers/InMemoryMonitorsRepository.ts +++ b/server/test/helpers/InMemoryMonitorsRepository.ts @@ -1,6 +1,6 @@ import type { IMonitorsRepository, TeamQueryConfig, SummaryConfig } from "../../src/domain/monitors/monitor.repository.interface.ts"; import type { CheckSnapshot } from "../../src/domain/checks/check.type.ts"; -import type { Monitor, MonitorsSummary } from "../../src/domain/monitors/monitor.types.ts"; +import type { Monitor, MonitorsSummary } from "../../src/domain/monitors/monitor.type.ts"; export class InMemoryMonitorsRepository implements IMonitorsRepository { private monitors: Monitor[] = []; @@ -119,10 +119,6 @@ export class InMemoryMonitorsRepository implements IMonitorsRepository { throw new Error("Not implemented"); } - async findGroupsByTeamId(_teamId: string): Promise { - throw new Error("Not implemented"); - } - async removeNotificationFromMonitors(_notificationId: string): Promise { throw new Error("Not implemented"); } diff --git a/server/test/helpers/heartbeatTestHarness.ts b/server/test/helpers/heartbeatTestHarness.ts index 5711f5aa9..898c3c91e 100644 --- a/server/test/helpers/heartbeatTestHarness.ts +++ b/server/test/helpers/heartbeatTestHarness.ts @@ -10,7 +10,7 @@ import { IncidentService } from "../../src/domain/incidents/incident.service.ts" import { InMemoryMonitorsRepository } from "./InMemoryMonitorsRepository.ts"; import { InMemoryIncidentsRepository } from "./InMemoryIncidentsRepository.ts"; import { createMockLogger } from "./createMockLogger.ts"; -import type { Monitor } from "../../src/domain/monitors/monitor.types.ts"; +import type { Monitor } from "../../src/domain/monitors/monitor.type.ts"; import type { MonitorStatusResponse } from "../../src/types/network.ts"; import type { Check } from "../../src/domain/checks/check.type.ts"; import type { MaintenanceWindow } from "../../src/domain/maintenance-windows/maintenance-window.type.ts"; diff --git a/server/test/helpers/statusProviderContract.ts b/server/test/helpers/statusProviderContract.ts index af625f64d..707d8b3c3 100644 --- a/server/test/helpers/statusProviderContract.ts +++ b/server/test/helpers/statusProviderContract.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "@jest/globals"; import type { IStatusProvider } from "../../src/service/network/IStatusProvider.ts"; -import type { Monitor } from "../../src/domain/monitors/monitor.types.ts"; +import type { Monitor } from "../../src/domain/monitors/monitor.type.ts"; /** * Shared contract tests for all IStatusProvider implementations. diff --git a/server/test/integration/incidentLifecycle.test.ts b/server/test/integration/incidentLifecycle.test.ts index 73b25d893..6f7d8bc9a 100644 --- a/server/test/integration/incidentLifecycle.test.ts +++ b/server/test/integration/incidentLifecycle.test.ts @@ -5,7 +5,7 @@ import { createMockLogger } from "../helpers/createMockLogger.ts"; import type { IMonitorsRepository } from "../../src/domain/monitors/monitor.repository.interface.ts"; import type { IUsersRepository } from "../../src/domain/users/user.repository.interface.ts"; import type { INotificationMessageBuilder } from "../../src/domain/notifications/notification.message-builder.ts"; -import type { Monitor } from "../../src/domain/monitors/monitor.types.ts"; +import type { Monitor } from "../../src/domain/monitors/monitor.type.ts"; import type { MonitorActionDecision } from "../../src/worker/worker.helper.ts"; // โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/server/test/unit/providers/network/advancedMatcher.test.ts b/server/test/unit/providers/network/advancedMatcher.test.ts index 063591e4f..b66d5ed56 100644 --- a/server/test/unit/providers/network/advancedMatcher.test.ts +++ b/server/test/unit/providers/network/advancedMatcher.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, jest } from "@jest/globals"; import { AdvancedMatcher } from "../../../../src/service/network/AdvancedMatcher.ts"; -import type { Monitor } from "../../../../src/domain/monitors/monitor.types.ts"; +import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; // โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/server/test/unit/providers/network/dnsProvider.test.ts b/server/test/unit/providers/network/dnsProvider.test.ts index 1c31b3b1d..d485ca599 100644 --- a/server/test/unit/providers/network/dnsProvider.test.ts +++ b/server/test/unit/providers/network/dnsProvider.test.ts @@ -1,7 +1,7 @@ import { beforeEach, describe, expect, it, jest } from "@jest/globals"; import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; -import { NETWORK_ERROR } from "../../../../src/service/network/utils.ts"; -import type { Monitor } from "../../../../src/domain/monitors/monitor.types.ts"; +import { NETWORK_ERROR } from "../../../../src/types/network.ts"; +import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; import { DNSProvider } from "../../../../src/service/network/DNSProvider.ts"; // โ”€โ”€ Mocks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/server/test/unit/providers/network/dockerProvider.test.ts b/server/test/unit/providers/network/dockerProvider.test.ts index f0b743f16..6d16b3e5c 100644 --- a/server/test/unit/providers/network/dockerProvider.test.ts +++ b/server/test/unit/providers/network/dockerProvider.test.ts @@ -2,8 +2,8 @@ import { describe, expect, it, jest } from "@jest/globals"; import { DockerProvider } from "../../../../src/service/network/DockerProvider.ts"; import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; import { createMockLogger } from "../../../helpers/createMockLogger.ts"; -import { NETWORK_ERROR } from "../../../../src/service/network/utils.ts"; -import type { Monitor } from "../../../../src/domain/monitors/monitor.types.ts"; +import { NETWORK_ERROR } from "../../../../src/types/network.ts"; +import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; // โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/server/test/unit/providers/network/gameProvider.test.ts b/server/test/unit/providers/network/gameProvider.test.ts index d166c30f3..eac775b3b 100644 --- a/server/test/unit/providers/network/gameProvider.test.ts +++ b/server/test/unit/providers/network/gameProvider.test.ts @@ -2,8 +2,8 @@ import { describe, expect, it, jest } from "@jest/globals"; import { GameProvider } from "../../../../src/service/network/GameProvider.ts"; import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; import { createMockLogger } from "../../../helpers/createMockLogger.ts"; -import { NETWORK_ERROR } from "../../../../src/service/network/utils.ts"; -import type { Monitor } from "../../../../src/domain/monitors/monitor.types.ts"; +import { NETWORK_ERROR } from "../../../../src/types/network.ts"; +import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; // โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/server/test/unit/providers/network/grpcProvider.test.ts b/server/test/unit/providers/network/grpcProvider.test.ts index bb1c78aac..63e8859f2 100644 --- a/server/test/unit/providers/network/grpcProvider.test.ts +++ b/server/test/unit/providers/network/grpcProvider.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, jest } from "@jest/globals"; import { GrpcProvider } from "../../../../src/service/network/GrpcProvider.ts"; import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; -import type { Monitor } from "../../../../src/domain/monitors/monitor.types.ts"; +import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; // โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/server/test/unit/providers/network/hardwareProvider.test.ts b/server/test/unit/providers/network/hardwareProvider.test.ts index 2b4583af9..b8bb66531 100644 --- a/server/test/unit/providers/network/hardwareProvider.test.ts +++ b/server/test/unit/providers/network/hardwareProvider.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, jest } from "@jest/globals"; import { HardwareProvider } from "../../../../src/service/network/HardwareProvider.ts"; import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; import type { HttpProvider } from "../../../../src/service/network/HttpProvider.ts"; -import type { Monitor } from "../../../../src/domain/monitors/monitor.types.ts"; +import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; // โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/server/test/unit/providers/network/httpProvider.test.ts b/server/test/unit/providers/network/httpProvider.test.ts index 7556f7595..9ab69f25d 100644 --- a/server/test/unit/providers/network/httpProvider.test.ts +++ b/server/test/unit/providers/network/httpProvider.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, jest } from "@jest/globals"; import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; -import { NETWORK_ERROR } from "../../../../src/service/network/utils.ts"; -import type { Monitor } from "../../../../src/domain/monitors/monitor.types.ts"; +import { NETWORK_ERROR } from "../../../../src/types/network.ts"; +import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; // โ”€โ”€ Mocks โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -112,7 +112,7 @@ describe("HttpProvider", () => { status: true, code: 200, message: "OK", - responseTime: 100, + responseTime: 120, }) ); }); @@ -182,8 +182,8 @@ describe("HttpProvider", () => { ); }); - it("defaults responseTime to 0 when timings.phases.firstByte is undefined", async () => { - mockGot.mockResolvedValue(makeGotResponse({ timings: { phases: { firstByte: undefined } } })); + it("defaults responseTime to 0 when timings.phases.total is undefined", async () => { + mockGot.mockResolvedValue(makeGotResponse({ timings: { phases: { total: undefined } } })); const { provider } = createProvider(); const result = await provider.handle(makeMonitor()); @@ -224,11 +224,11 @@ describe("HttpProvider", () => { expect(result.message).toBe("Response is not JSON"); }); - it("defaults responseTime to 0 in non-JSON jsonPath response when firstByte is undefined", async () => { + it("defaults responseTime to 0 in non-JSON jsonPath response when total is undefined", async () => { mockGot.mockResolvedValue( makeGotResponse({ headers: { "content-type": "text/html" }, - timings: { phases: { firstByte: undefined } }, + timings: { phases: { total: undefined } }, }) ); const { provider } = createProvider(); diff --git a/server/test/unit/providers/network/pageSpeedProvider.test.ts b/server/test/unit/providers/network/pageSpeedProvider.test.ts index df5beb0be..2767e8e56 100644 --- a/server/test/unit/providers/network/pageSpeedProvider.test.ts +++ b/server/test/unit/providers/network/pageSpeedProvider.test.ts @@ -4,7 +4,7 @@ import { testStatusProviderContract } from "../../../helpers/statusProviderContr import { createMockLogger } from "../../../helpers/createMockLogger.ts"; import type { HttpProvider } from "../../../../src/service/network/HttpProvider.ts"; import type { ISettingsService } from "../../../../src/domain/app-settings/app-settings.service.ts"; -import type { Monitor } from "../../../../src/domain/monitors/monitor.types.ts"; +import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; // โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/server/test/unit/providers/network/pingProvider.test.ts b/server/test/unit/providers/network/pingProvider.test.ts index 8f006a470..04e4ad74e 100644 --- a/server/test/unit/providers/network/pingProvider.test.ts +++ b/server/test/unit/providers/network/pingProvider.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, jest } from "@jest/globals"; import { PingProvider } from "../../../../src/service/network/PingProvider.ts"; import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; -import type { Monitor } from "../../../../src/domain/monitors/monitor.types.ts"; +import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; // โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/server/test/unit/providers/network/portProvider.test.ts b/server/test/unit/providers/network/portProvider.test.ts index 8a7d55c4c..fbbfb5767 100644 --- a/server/test/unit/providers/network/portProvider.test.ts +++ b/server/test/unit/providers/network/portProvider.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, jest } from "@jest/globals"; import { PortProvider } from "../../../../src/service/network/PortProvider.ts"; import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; -import { NETWORK_ERROR } from "../../../../src/service/network/utils.ts"; -import type { Monitor } from "../../../../src/domain/monitors/monitor.types.ts"; +import { NETWORK_ERROR } from "../../../../src/types/network.ts"; +import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; // โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/server/test/unit/providers/network/utils.test.ts b/server/test/unit/providers/network/utils.test.ts index b37a5597c..a83e14cff 100644 --- a/server/test/unit/providers/network/utils.test.ts +++ b/server/test/unit/providers/network/utils.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "@jest/globals"; -import { timeRequest, NETWORK_ERROR, PING_ERROR, isStatusUp } from "../../../../src/service/network/utils.ts"; +import { timeRequest, isStatusUp } from "../../../../src/service/network/utils.ts"; +import { NETWORK_ERROR } from "../../../../src/types/network.ts"; describe("network utils", () => { describe("timeRequest", () => { @@ -36,10 +37,6 @@ describe("network utils", () => { it("NETWORK_ERROR is 5000", () => { expect(NETWORK_ERROR).toBe(5000); }); - - it("PING_ERROR is 5001", () => { - expect(PING_ERROR).toBe(5001); - }); }); describe("isStatusUp", () => { diff --git a/server/test/unit/providers/network/webSocketProvider.test.ts b/server/test/unit/providers/network/webSocketProvider.test.ts index 12ff32d63..b765b2f74 100644 --- a/server/test/unit/providers/network/webSocketProvider.test.ts +++ b/server/test/unit/providers/network/webSocketProvider.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, jest, beforeEach, afterEach } from "@jest/globals"; import { WebSocketProvider } from "../../../../src/service/network/WebSocketProvider.ts"; import { testStatusProviderContract } from "../../../helpers/statusProviderContract.ts"; -import { NETWORK_ERROR } from "../../../../src/service/network/utils.ts"; -import type { Monitor } from "../../../../src/domain/monitors/monitor.types.ts"; +import { NETWORK_ERROR } from "../../../../src/types/network.ts"; +import type { Monitor } from "../../../../src/domain/monitors/monitor.type.ts"; // โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ diff --git a/server/test/unit/services/bufferService.test.ts b/server/test/unit/services/bufferService.test.ts index d994e5f24..90b5489bf 100644 --- a/server/test/unit/services/bufferService.test.ts +++ b/server/test/unit/services/bufferService.test.ts @@ -71,17 +71,6 @@ describe("BufferService", () => { // โ”€โ”€ Static / instance properties โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - describe("serviceName", () => { - it("returns BufferService from static property", () => { - expect(BufferService.SERVICE_NAME).toBe("BufferService"); - }); - - it("returns BufferService from instance getter", () => { - const { service } = createService(); - expect(service.serviceName).toBe("BufferService"); - }); - }); - // โ”€โ”€ constructor โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("constructor", () => { diff --git a/server/test/unit/services/checkEvaluator.test.ts b/server/test/unit/services/checkEvaluator.test.ts index a22ceb1c7..22919495a 100644 --- a/server/test/unit/services/checkEvaluator.test.ts +++ b/server/test/unit/services/checkEvaluator.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, jest } from "@jest/globals"; import { CheckEvaluator } from "../../../src/worker/worker.check-evaluator.ts"; import { MonitorStatusPolicy } from "../../../src/worker/worker.monitor-status-policy.ts"; -import type { Monitor } from "../../../src/domain/monitors/monitor.types.ts"; +import type { Monitor } from "../../../src/domain/monitors/monitor.type.ts"; const makeMonitor = (overrides?: Partial): Monitor => ({ diff --git a/server/test/unit/services/checkPipeline.test.ts b/server/test/unit/services/checkPipeline.test.ts index 008e879fa..2e7026f4d 100644 --- a/server/test/unit/services/checkPipeline.test.ts +++ b/server/test/unit/services/checkPipeline.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, jest } from "@jest/globals"; import { GeoChecksPipeline } from "../../../src/worker/worker.check-pipeline.ts"; -import type { Monitor } from "../../../src/domain/monitors/monitor.types.ts"; +import type { Monitor } from "../../../src/domain/monitors/monitor.type.ts"; import { createMockLogger } from "../../helpers/createMockLogger.ts"; const makeMonitor = (overrides?: Partial): Monitor => diff --git a/server/test/unit/services/checkProducer.test.ts b/server/test/unit/services/checkProducer.test.ts index 4edd12bb5..dacff3d9b 100644 --- a/server/test/unit/services/checkProducer.test.ts +++ b/server/test/unit/services/checkProducer.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, jest } from "@jest/globals"; import { CheckProducer } from "../../../src/worker/worker.check-producer.ts"; -import type { Monitor } from "../../../src/domain/monitors/monitor.types.ts"; +import type { Monitor } from "../../../src/domain/monitors/monitor.type.ts"; import { createMockLogger } from "../../helpers/createMockLogger.ts"; const makeMonitor = (overrides?: Partial): Monitor => diff --git a/server/test/unit/services/checkService.test.ts b/server/test/unit/services/checkService.test.ts index 104ccbaa2..80b941b6f 100644 --- a/server/test/unit/services/checkService.test.ts +++ b/server/test/unit/services/checkService.test.ts @@ -5,7 +5,7 @@ import type { IChecksRepository } from "../../../src/domain/checks/check.reposit import type { IMonitorsRepository } from "../../../src/domain/monitors/monitor.repository.interface.ts"; import type { MonitorStatusResponse, HardwareStatusPayload, PageSpeedStatusPayload } from "../../../src/types/network.ts"; import { NotificationMessageBuilder } from "../../../src/domain/notifications/notification.message-builder.ts"; -import type { Monitor } from "../../../src/domain/monitors/monitor.types.ts"; +import type { Monitor } from "../../../src/domain/monitors/monitor.type.ts"; // โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -48,13 +48,6 @@ const makeStatusResponse = (overrides?: Partial): Monitor // โ”€โ”€ Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("CheckService", () => { - describe("serviceName", () => { - it("returns checkService", () => { - const { service } = createService(); - expect(service.serviceName).toBe("checkService"); - }); - }); - // โ”€โ”€ toCheck โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("toCheck", () => { @@ -382,7 +375,7 @@ describe("CheckService", () => { rowsPerPage: null as any, }); - expect(checksRepository.findByMonitorId).toHaveBeenCalledWith("mon-1", "desc", "day", undefined, 0, 5, undefined); + expect(checksRepository.findByMonitorId).toHaveBeenCalledWith("mon-1", "desc", "day", 0, 5, undefined, undefined); }); it("passes filter and status parameters", async () => { @@ -395,11 +388,11 @@ describe("CheckService", () => { dateRange: "week", page: 2, rowsPerPage: 20, - filter: "error", + filter: "resolve", status: false, }); - expect(checksRepository.findByMonitorId).toHaveBeenCalledWith("mon-1", "asc", "week", "error", 2, 20, false); + expect(checksRepository.findByMonitorId).toHaveBeenCalledWith("mon-1", "asc", "week", 2, 20, false, "resolve"); }); it("throws when monitorId is missing", async () => { @@ -450,7 +443,7 @@ describe("CheckService", () => { await service.getChecksByTeam({ teamId: "team-1", sortOrder: "desc", dateRange: "day", page: null as any, rowsPerPage: null as any }); - expect(checksRepository.findByTeamId).toHaveBeenCalledWith("desc", "day", undefined, 0, 5, "team-1"); + expect(checksRepository.findByTeamId).toHaveBeenCalledWith("desc", "day", 0, 5, "team-1", undefined); }); }); diff --git a/server/test/unit/services/dbQueueWorker.test.ts b/server/test/unit/services/dbQueueWorker.test.ts index 68df5f02a..d903ae887 100644 --- a/server/test/unit/services/dbQueueWorker.test.ts +++ b/server/test/unit/services/dbQueueWorker.test.ts @@ -2,7 +2,7 @@ import { describe, expect, it, jest, beforeEach, afterEach } from "@jest/globals import { DBQueueWorker } from "../../../src/worker/worker.db-queue.ts"; import { LOCK_MS } from "../../../src/domain/jobs/job.type.ts"; import type { Job } from "../../../src/domain/jobs/job.type.ts"; -import type { Monitor } from "../../../src/domain/monitors/monitor.types.ts"; +import type { Monitor } from "../../../src/domain/monitors/monitor.type.ts"; import type { QueueMode } from "../../../src/domain/app-settings/app-settings.type.ts"; import { createMockLogger } from "../../helpers/createMockLogger.ts"; @@ -139,24 +139,12 @@ const createWorker = (overrides?: { queueMode?: QueueMode; queuePrimaryProcesses ...overrides?.mocks, }; - const worker = new DBQueueWorker( - mocks.logger as any, - mocks.isDbConnected as any, - mocks.jobsRepository as any, - mocks.monitorsRepository as any, - mocks.checksRepository as any, - mocks.checkService as any, - mocks.bufferService as any, - mocks.checkProducer as any, - mocks.checkEvaluator as any, - mocks.geoCheckPipeline as any, - mocks.dispatcher as any, - mocks.helper as any, - mocks.queueWorkersRepository as any, - overrides?.queueMode ?? "worker", - overrides?.queuePrimaryProcesses ?? true, - "worker-1" - ); + const worker = new DBQueueWorker({ + ...mocks, + queueMode: overrides?.queueMode ?? "worker", + queuePrimaryProcesses: overrides?.queuePrimaryProcesses ?? true, + workerId: "worker-1", + } as any); return { worker, mocks }; }; @@ -243,24 +231,12 @@ describe("DBQueueWorker", () => { it("create() constructs the worker and runs init", async () => { const { mocks } = createWorker(); // reuse the mock set; the un-init'd instance is discarded - const worker = await DBQueueWorker.create( - mocks.logger as any, - mocks.isDbConnected as any, - mocks.jobsRepository as any, - mocks.monitorsRepository as any, - mocks.checksRepository as any, - mocks.checkService as any, - mocks.bufferService as any, - mocks.checkProducer as any, - mocks.checkEvaluator as any, - mocks.geoCheckPipeline as any, - mocks.dispatcher as any, - mocks.helper as any, - mocks.queueWorkersRepository as any, - "worker", - true, - "worker-1" - ); + const worker = await DBQueueWorker.create({ + ...mocks, + queueMode: "worker", + queuePrimaryProcesses: true, + workerId: "worker-1", + } as any); active = worker; await jest.advanceTimersByTimeAsync(1); diff --git a/server/test/unit/services/diagnosticService.test.ts b/server/test/unit/services/diagnosticService.test.ts index 07962a68e..d2512717f 100644 --- a/server/test/unit/services/diagnosticService.test.ts +++ b/server/test/unit/services/diagnosticService.test.ts @@ -56,17 +56,6 @@ describe("DiagnosticService", () => { jest.useRealTimers(); }); - describe("serviceName", () => { - it("returns diagnosticService from static property", () => { - expect(DiagnosticService.SERVICE_NAME).toBe("diagnosticService"); - }); - - it("returns diagnosticService from instance getter", () => { - const service = new DiagnosticService(makeDb()); - expect(service.serviceName).toBe("diagnosticService"); - }); - }); - // โ”€โ”€ constructor โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("constructor", () => { diff --git a/server/test/unit/services/emailService.test.ts b/server/test/unit/services/emailService.test.ts index fbfdf15fd..971c712da 100644 --- a/server/test/unit/services/emailService.test.ts +++ b/server/test/unit/services/emailService.test.ts @@ -82,11 +82,6 @@ describe("EmailService", () => { it("returns EmailService from static property", () => { expect(EmailService.SERVICE_NAME).toBe("EmailService"); }); - - it("returns EmailService from instance getter", () => { - const { service } = createService(); - expect(service.serviceName).toBe("EmailService"); - }); }); // โ”€โ”€ init / loadTemplate โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -95,14 +90,14 @@ describe("EmailService", () => { it("loads all expected templates on construction", () => { const { mockFs } = createService(); - // 6 templates loaded - expect(mockFs.readFileSync).toHaveBeenCalledTimes(6); + // 5 templates loaded + expect(mockFs.readFileSync).toHaveBeenCalledTimes(5); }); it("compiles each loaded template", () => { const { compile } = createService(); - expect(compile).toHaveBeenCalledTimes(6); + expect(compile).toHaveBeenCalledTimes(5); }); it("logs error when a template file is not found", () => { @@ -146,7 +141,7 @@ describe("EmailService", () => { service.init(); - expect(mockFs.readFileSync).toHaveBeenCalledTimes(initialCalls + 6); + expect(mockFs.readFileSync).toHaveBeenCalledTimes(initialCalls + 5); }); }); diff --git a/server/test/unit/services/geoChecksService.test.ts b/server/test/unit/services/geoChecksService.test.ts index 8e50fa3e8..a7b1a5db4 100644 --- a/server/test/unit/services/geoChecksService.test.ts +++ b/server/test/unit/services/geoChecksService.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, jest } from "@jest/globals"; import { GeoChecksService } from "../../../src/domain/geo-checks/geo-check.service.ts"; import { createMockLogger } from "../../helpers/createMockLogger.ts"; -import type { Monitor } from "../../../src/domain/monitors/monitor.types.ts"; +import type { Monitor } from "../../../src/domain/monitors/monitor.type.ts"; import type { GeoCheckResult } from "../../../src/domain/geo-checks/geo-check.type.ts"; // โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -48,17 +48,6 @@ const createService = (overrides?: Record) => { // โ”€โ”€ Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("GeoChecksService", () => { - describe("serviceName", () => { - it("returns GeoChecksService from static property", () => { - expect(GeoChecksService.SERVICE_NAME).toBe("GeoChecksService"); - }); - - it("returns GeoChecksService from instance getter", () => { - const { service } = createService(); - expect(service.serviceName).toBe("GeoChecksService"); - }); - }); - // โ”€โ”€ buildGeoCheck โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("buildGeoCheck", () => { diff --git a/server/test/unit/services/globalPingService.test.ts b/server/test/unit/services/globalPingService.test.ts index 376653110..5169f205b 100644 --- a/server/test/unit/services/globalPingService.test.ts +++ b/server/test/unit/services/globalPingService.test.ts @@ -66,17 +66,6 @@ describe("GlobalPingService", () => { // โ”€โ”€ Static / instance properties โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - describe("serviceName", () => { - it("returns GlobalPingService from static property", () => { - expect(GlobalPingService.SERVICE_NAME).toBe("GlobalPingService"); - }); - - it("returns GlobalPingService from instance getter", () => { - const { service } = createService(); - expect(service.serviceName).toBe("GlobalPingService"); - }); - }); - // โ”€โ”€ createMeasurement โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("createMeasurement", () => { diff --git a/server/test/unit/services/incidentService.test.ts b/server/test/unit/services/incidentService.test.ts index aa4fffce8..af73623df 100644 --- a/server/test/unit/services/incidentService.test.ts +++ b/server/test/unit/services/incidentService.test.ts @@ -5,7 +5,7 @@ import type { IIncidentsRepository } from "../../../src/domain/incidents/inciden import type { IMonitorsRepository } from "../../../src/domain/monitors/monitor.repository.interface.ts"; import type { IUsersRepository } from "../../../src/domain/users/user.repository.interface.ts"; import type { INotificationMessageBuilder } from "../../../src/domain/notifications/notification.message-builder.ts"; -import type { Monitor } from "../../../src/domain/monitors/monitor.types.ts"; +import type { Monitor } from "../../../src/domain/monitors/monitor.type.ts"; import type { Incident } from "../../../src/domain/incidents/incident.type.ts"; import type { MonitorActionDecision } from "../../../src/worker/worker.helper.ts"; @@ -91,13 +91,6 @@ const makeDecision = (overrides?: Partial): MonitorAction // โ”€โ”€ Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("IncidentService", () => { - describe("serviceName", () => { - it("returns incidentService", () => { - const { service } = createService(); - expect(service.serviceName).toBe("incidentService"); - }); - }); - // โ”€โ”€ handleIncident โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("handleIncident", () => { diff --git a/server/test/unit/services/inviteService.test.ts b/server/test/unit/services/inviteService.test.ts index c050d91e6..59d472539 100644 --- a/server/test/unit/services/inviteService.test.ts +++ b/server/test/unit/services/inviteService.test.ts @@ -39,17 +39,6 @@ const createService = (overrides?: Record) => { // โ”€โ”€ Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("InviteService", () => { - describe("serviceName", () => { - it("returns inviteService from static property", () => { - expect(InviteService.SERVICE_NAME).toBe("inviteService"); - }); - - it("returns inviteService from instance getter", () => { - const { service } = createService(); - expect(service.serviceName).toBe("inviteService"); - }); - }); - // โ”€โ”€ getInviteToken โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("getInviteToken", () => { diff --git a/server/test/unit/services/jobScheduler.test.ts b/server/test/unit/services/jobScheduler.test.ts index 1528c06a6..098cef245 100644 --- a/server/test/unit/services/jobScheduler.test.ts +++ b/server/test/unit/services/jobScheduler.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, jest, beforeEach, afterEach } from "@jest/globals"; import { JobScheduler } from "../../../src/worker/worker.job-scheduler.ts"; -import type { Monitor } from "../../../src/domain/monitors/monitor.types.ts"; +import type { Monitor } from "../../../src/domain/monitors/monitor.type.ts"; import type { QueueMode } from "../../../src/domain/app-settings/app-settings.type.ts"; import { createMockLogger } from "../../helpers/createMockLogger.ts"; diff --git a/server/test/unit/services/maintenanceWindowService.test.ts b/server/test/unit/services/maintenanceWindowService.test.ts index 9547eaac8..e15f20d46 100644 --- a/server/test/unit/services/maintenanceWindowService.test.ts +++ b/server/test/unit/services/maintenanceWindowService.test.ts @@ -94,17 +94,6 @@ const defaultCreateParams = { // โ”€โ”€ Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("MaintenanceWindowService", () => { - describe("serviceName", () => { - it("returns maintenanceWindowService from static property", () => { - expect(MaintenanceWindowService.SERVICE_NAME).toBe("maintenanceWindowService"); - }); - - it("returns maintenanceWindowService from instance getter", () => { - const { service } = createService(); - expect(service.serviceName).toBe("maintenanceWindowService"); - }); - }); - // โ”€โ”€ createMaintenanceWindow โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("createMaintenanceWindow", () => { diff --git a/server/test/unit/services/monitorService.test.ts b/server/test/unit/services/monitorService.test.ts index 65d6923a6..69edbba9c 100644 --- a/server/test/unit/services/monitorService.test.ts +++ b/server/test/unit/services/monitorService.test.ts @@ -18,7 +18,6 @@ const createMonitorsRepositoryMock = () => findByIds: jest.fn(), findMonitorCountByTeamIdAndType: jest.fn(), findMonitorsSummaryByTeamId: jest.fn(), - findGroupsByTeamId: jest.fn(), updateById: jest.fn(), updateNotifications: jest.fn(), togglePauseById: jest.fn(), @@ -112,13 +111,6 @@ const createService = ( }; describe("MonitorService", () => { - describe("serviceName", () => { - it("returns 'MonitorService'", () => { - const { service } = createService(); - expect(service.serviceName).toBe("MonitorService"); - }); - }); - describe("createMonitor", () => { it("creates a monitor and adds a job", async () => { const monitorsRepository = createMonitorsRepositoryMock(); @@ -582,7 +574,7 @@ describe("MonitorService", () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findMonitorsSummaryByTeamId as jest.Mock).mockResolvedValue(null); (monitorsRepository.findMonitorCountByTeamIdAndType as jest.Mock).mockResolvedValue(0); - (monitorsRepository.findByTeamIdWithStats as jest.Mock).mockResolvedValue(null); + (monitorsRepository.findByTeamIdWithStats as jest.Mock).mockResolvedValue([]); const { service } = createService({ monitorsRepository }); const result = await service.getMonitorsWithChecksByTeamId({ teamId: TEAM_ID }); @@ -746,18 +738,6 @@ describe("MonitorService", () => { }); }); - describe("getGroupsByTeamId", () => { - it("delegates to repository", async () => { - const monitorsRepository = createMonitorsRepositoryMock(); - (monitorsRepository.findGroupsByTeamId as jest.Mock).mockResolvedValue(["group1", "group2"]); - const { service } = createService({ monitorsRepository }); - - const result = await service.getGroupsByTeamId({ teamId: TEAM_ID }); - expect(result).toEqual(["group1", "group2"]); - expect(monitorsRepository.findGroupsByTeamId).toHaveBeenCalledWith(TEAM_ID); - }); - }); - describe("editMonitor", () => { it("updates monitor and refreshes job", async () => { const monitorsRepository = createMonitorsRepositoryMock(); @@ -1463,12 +1443,15 @@ describe("MonitorService", () => { const statusPagesRepository = createStatusPagesRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); + const incidentsRepository = createIncidentsRepositoryMock(); + const monitors = [makeMonitor({ id: "m1" }), makeMonitor({ id: "m2" })]; (monitorsRepository.deleteByTeamId as jest.Mock).mockResolvedValue({ monitors, deletedCount: 2 }); (checksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (geoChecksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); (statusPagesRepository.removeMonitorFromStatusPages as jest.Mock).mockResolvedValue(0); (monitorStatsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue({}); + (incidentsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); const { service, jobQueue } = createService({ monitorsRepository, @@ -1476,6 +1459,7 @@ describe("MonitorService", () => { monitorStatsRepository, statusPagesRepository, geoChecksRepository, + incidentsRepository, }); const result = await service.deleteAllMonitors({ teamId: TEAM_ID }); @@ -1486,26 +1470,33 @@ describe("MonitorService", () => { expect(geoChecksRepository.deleteByMonitorId).toHaveBeenCalledTimes(2); expect(statusPagesRepository.removeMonitorFromStatusPages).toHaveBeenCalledTimes(2); expect(monitorStatsRepository.deleteByMonitorId).toHaveBeenCalledTimes(2); + expect(incidentsRepository.deleteByMonitorId).toHaveBeenCalledTimes(2); }); - it("logs warning when associated record deletion fails with Error", async () => { + it("logs a per-child warning and continues when a child deletion fails with Error", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const statusPagesRepository = createStatusPagesRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); + const incidentsRepository = createIncidentsRepositoryMock(); const monitors = [makeMonitor({ id: "m1" })]; (monitorsRepository.deleteByTeamId as jest.Mock).mockResolvedValue({ monitors, deletedCount: 1 }); (checksRepository.deleteByMonitorId as jest.Mock).mockRejectedValue(new Error("fail")); + (geoChecksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); + (statusPagesRepository.removeMonitorFromStatusPages as jest.Mock).mockResolvedValue(0); + (monitorStatsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue({}); + (incidentsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); const logger = createMockLogger(); - const { service } = createService({ + const { service, jobQueue } = createService({ monitorsRepository, checksRepository, monitorStatsRepository, statusPagesRepository, geoChecksRepository, + incidentsRepository, logger, }); @@ -1514,21 +1505,28 @@ describe("MonitorService", () => { expect(result).toBe(1); expect(logger.warn).toHaveBeenCalledWith( expect.objectContaining({ - message: expect.stringContaining("Error deleting associated records"), + message: expect.stringContaining("Error deleting checks"), stack: expect.any(String), }) ); + expect(jobQueue.deleteJob).toHaveBeenCalledTimes(1); }); - it("logs warning when associated record deletion fails with non-Error", async () => { + it("logs warning when scheduler job deletion fails with non-Error", async () => { const monitorsRepository = createMonitorsRepositoryMock(); const checksRepository = createChecksRepositoryMock(); const monitorStatsRepository = createMonitorStatsRepositoryMock(); const statusPagesRepository = createStatusPagesRepositoryMock(); const geoChecksRepository = createGeoChecksRepositoryMock(); + const incidentsRepository = createIncidentsRepositoryMock(); const monitors = [makeMonitor({ id: "m1" })]; (monitorsRepository.deleteByTeamId as jest.Mock).mockResolvedValue({ monitors, deletedCount: 1 }); + (checksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); + (geoChecksRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); + (statusPagesRepository.removeMonitorFromStatusPages as jest.Mock).mockResolvedValue(0); + (monitorStatsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue({}); + (incidentsRepository.deleteByMonitorId as jest.Mock).mockResolvedValue(0); // deleteJob throws non-Error const jobQueue = createJobQueueMock(); (jobQueue.deleteJob as jest.Mock).mockRejectedValue("string error"); @@ -1543,7 +1541,7 @@ describe("MonitorService", () => { geoChecksRepository, monitorStatsRepository, statusPagesRepository, - incidentsRepository: createIncidentsRepositoryMock(), + incidentsRepository, }); const result = await service.deleteAllMonitors({ teamId: TEAM_ID }); @@ -1578,14 +1576,6 @@ describe("MonitorService", () => { expect(result).toEqual(monitors); }); - it("throws when no monitors found (null)", async () => { - const monitorsRepository = createMonitorsRepositoryMock(); - (monitorsRepository.findByTeamId as jest.Mock).mockResolvedValue(null); - const { service } = createService({ monitorsRepository }); - - await expect(service.exportMonitorsToJSON({ teamId: TEAM_ID })).rejects.toThrow("No monitors found to export."); - }); - it("throws when monitors array is empty", async () => { const monitorsRepository = createMonitorsRepositoryMock(); (monitorsRepository.findByTeamId as jest.Mock).mockResolvedValue([]); diff --git a/server/test/unit/services/networkService.test.ts b/server/test/unit/services/networkService.test.ts index 268b52e3a..2e6627452 100644 --- a/server/test/unit/services/networkService.test.ts +++ b/server/test/unit/services/networkService.test.ts @@ -1,9 +1,9 @@ import { describe, expect, it, jest } from "@jest/globals"; import { NetworkService } from "../../../src/service/networkService.ts"; import { createMockLogger } from "../../helpers/createMockLogger.ts"; -import { NETWORK_ERROR } from "../../../src/service/network/utils.ts"; +import { NETWORK_ERROR } from "../../../src/types/network.ts"; import type { IStatusProvider } from "../../../src/service/network/IStatusProvider.ts"; -import type { Monitor, MonitorType } from "../../../src/domain/monitors/monitor.types.ts"; +import type { Monitor, MonitorType } from "../../../src/domain/monitors/monitor.type.ts"; import type { AxiosStatic } from "axios"; // โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ @@ -43,17 +43,6 @@ const createService = (providers: IStatusProvider[] = []) => { describe("NetworkService", () => { // โ”€โ”€ Static / instance properties โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - describe("serviceName", () => { - it("returns NetworkService from static property", () => { - expect(NetworkService.SERVICE_NAME).toBe("NetworkService"); - }); - - it("returns NetworkService from instance getter", () => { - const { service } = createService(); - expect(service.serviceName).toBe("NetworkService"); - }); - }); - // โ”€โ”€ requestStatus โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("requestStatus", () => { diff --git a/server/test/unit/services/notificationMessageBuilder.test.ts b/server/test/unit/services/notificationMessageBuilder.test.ts index 6e1c4f4dc..1b9e42772 100644 --- a/server/test/unit/services/notificationMessageBuilder.test.ts +++ b/server/test/unit/services/notificationMessageBuilder.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it, beforeEach } from "@jest/globals"; import { NotificationMessageBuilder } from "../../../src/domain/notifications/notification.message-builder.ts"; -import type { Monitor } from "../../../src/domain/monitors/monitor.types.ts"; +import type { Monitor } from "../../../src/domain/monitors/monitor.type.ts"; import type { MonitorStatusResponse, HardwareStatusPayload } from "../../../src/types/network.ts"; import type { MonitorActionDecision } from "../../../src/worker/worker.helper.ts"; diff --git a/server/test/unit/services/notificationsService.test.ts b/server/test/unit/services/notificationsService.test.ts index 8ba8b34aa..8e1633803 100644 --- a/server/test/unit/services/notificationsService.test.ts +++ b/server/test/unit/services/notificationsService.test.ts @@ -1,7 +1,7 @@ import { describe, expect, it, jest, beforeEach } from "@jest/globals"; import { NotificationsService } from "../../../src/domain/notifications/notification.service.ts"; import { createMockLogger } from "../../helpers/createMockLogger.ts"; -import type { Monitor } from "../../../src/domain/monitors/monitor.types.ts"; +import type { Monitor } from "../../../src/domain/monitors/monitor.type.ts"; import type { Notification } from "../../../src/domain/notifications/notification.type.ts"; import type { MonitorStatusResponse } from "../../../src/types/network.ts"; import type { MonitorActionDecision } from "../../../src/worker/worker.helper.ts"; @@ -73,24 +73,26 @@ const createService = (overrides?: Record) => { ...overrides, }; - const service = new NotificationsService( - defaults.notificationsRepository as any, - defaults.monitorsRepository as any, - defaults.webhookProvider as any, - defaults.emailProvider as any, - defaults.slackProvider as any, - defaults.discordProvider as any, - defaults.pagerDutyProvider as any, - defaults.matrixProvider as any, - defaults.teamsProvider as any, - defaults.telegramProvider as any, - defaults.pushoverProvider as any, - defaults.twilioProvider as any, - defaults.ntfyProvider as any, - defaults.settingsService as any, - defaults.logger as any, - defaults.notificationMessageBuilder as any - ); + const service = new NotificationsService({ + notificationsRepository: defaults.notificationsRepository, + monitorsRepository: defaults.monitorsRepository, + providers: { + webhook: defaults.webhookProvider, + email: defaults.emailProvider, + slack: defaults.slackProvider, + discord: defaults.discordProvider, + pager_duty: defaults.pagerDutyProvider, + matrix: defaults.matrixProvider, + teams: defaults.teamsProvider, + telegram: defaults.telegramProvider, + pushover: defaults.pushoverProvider, + twilio: defaults.twilioProvider, + ntfy: defaults.ntfyProvider, + }, + settingsService: defaults.settingsService, + logger: defaults.logger, + notificationMessageBuilder: defaults.notificationMessageBuilder, + } as any); return { service, ...defaults }; }; diff --git a/server/test/unit/services/settingsService.test.ts b/server/test/unit/services/settingsService.test.ts index af58b5705..063a64f6d 100644 --- a/server/test/unit/services/settingsService.test.ts +++ b/server/test/unit/services/settingsService.test.ts @@ -56,23 +56,12 @@ const createService = (envOverrides?: Partial) => { // โ”€โ”€ Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("SettingsService", () => { - describe("serviceName", () => { - it("returns SettingsService from static property", () => { - expect(SettingsService.SERVICE_NAME).toBe("SettingsService"); - }); + // โ”€โ”€ getSettings โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - it("returns SettingsService from instance getter", () => { - const { service } = createService(); - expect(service.serviceName).toBe("SettingsService"); - }); - }); - - // โ”€โ”€ loadSettings โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - describe("loadSettings", () => { + describe("getSettings", () => { it("returns env config mapped from constructor", () => { const { service } = createService(); - const settings = service.loadSettings(); + const settings = service.getSettings(); expect(settings).toEqual({ jwtSecret: "test-secret", @@ -83,28 +72,31 @@ describe("SettingsService", () => { dbConnectionString: "mongodb://localhost:27017/test_db", dbType: "mongodb", statusPageThemesEnabled: false, + clientConfig: {}, + }); + }); + + it("maps only set CLIENT_CONFIG_* env vars into clientConfig", () => { + const { service } = createService({ + CLIENT_CONFIG_API_BASE_URL: "https://api.example.com/api/v1", + CLIENT_CONFIG_LOG_LEVEL: "warn", + }); + + expect(service.getSettings().clientConfig).toEqual({ + apiBaseUrl: "https://api.example.com/api/v1", + logLevel: "warn", }); }); it("reflects custom env values", () => { const { service } = createService({ NODE_ENV: "production", LOG_LEVEL: "error" }); - const settings = service.loadSettings(); + const settings = service.getSettings(); expect(settings.nodeEnv).toBe("production"); expect(settings.logLevel).toBe("error"); }); }); - // โ”€โ”€ getSettings โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ - - describe("getSettings", () => { - it("returns the same config as loadSettings", () => { - const { service } = createService(); - - expect(service.getSettings()).toEqual(service.loadSettings()); - }); - }); - // โ”€โ”€ areStatusPageThemesEnabled โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("areStatusPageThemesEnabled", () => { diff --git a/server/test/unit/services/statusPageService.test.ts b/server/test/unit/services/statusPageService.test.ts index 7ab2da69d..e0115ab99 100644 --- a/server/test/unit/services/statusPageService.test.ts +++ b/server/test/unit/services/statusPageService.test.ts @@ -3,7 +3,7 @@ import { StatusPageService } from "../../../src/domain/status-pages/status-page. import type { IStatusPagesRepository } from "../../../src/domain/status-pages/status-page-repository.interface.ts"; import type { ISettingsService } from "../../../src/domain/app-settings/app-settings.service.ts"; import type { IMonitorsRepository } from "../../../src/domain/monitors/monitor.repository.interface.ts"; -import type { Monitor } from "../../../src/domain/monitors/monitor.types.ts"; +import type { Monitor } from "../../../src/domain/monitors/monitor.type.ts"; import type { StatusPage } from "../../../src/domain/status-pages/status-page.type.ts"; import { DEFAULT_STATUS_PAGE_THEME, DEFAULT_STATUS_PAGE_THEME_MODE } from "../../../src/domain/status-pages/status-page.type.ts"; diff --git a/server/test/unit/services/statusService.test.ts b/server/test/unit/services/statusService.test.ts index 5712de301..9e66ddbec 100644 --- a/server/test/unit/services/statusService.test.ts +++ b/server/test/unit/services/statusService.test.ts @@ -1,8 +1,8 @@ import { describe, expect, it, jest, beforeEach } from "@jest/globals"; import { StatusService } from "../../../src/service/statusService.ts"; import { createMockLogger } from "../../helpers/createMockLogger.ts"; -import { MAX_RECENT_CHECKS } from "../../../src/domain/monitors/monitor.types.ts"; -import type { Monitor, MonitorStatus } from "../../../src/domain/monitors/monitor.types.ts"; +import { MAX_RECENT_CHECKS } from "../../../src/domain/monitors/monitor.type.ts"; +import type { Monitor, MonitorStatus } from "../../../src/domain/monitors/monitor.type.ts"; import type { Check } from "../../../src/domain/checks/check.type.ts"; import type { MonitorStatusResponse, HardwareStatusPayload } from "../../../src/types/network.ts"; import type { IMonitorsRepository } from "../../../src/domain/monitors/monitor.repository.interface.ts"; @@ -143,13 +143,6 @@ const makeExistingStats = (overrides?: Record) => ({ // โ”€โ”€ Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("StatusService", () => { - describe("serviceName", () => { - it("returns StatusService", () => { - const { service } = createService(); - expect(service.serviceName).toBe("StatusService"); - }); - }); - // โ”€โ”€ updateRunningStats โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("updateRunningStats", () => { diff --git a/server/test/unit/services/userService.test.ts b/server/test/unit/services/userService.test.ts index 061003e7f..bce8769cf 100644 --- a/server/test/unit/services/userService.test.ts +++ b/server/test/unit/services/userService.test.ts @@ -98,13 +98,6 @@ const createService = (overrides?: Record) => { // โ”€โ”€ Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("UserService", () => { - describe("serviceName", () => { - it("returns userService", () => { - const { service } = createService(); - expect(service.serviceName).toBe("userService"); - }); - }); - // โ”€โ”€ issueToken โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("issueToken", () => { diff --git a/server/test/unit/services/workerHelper.test.ts b/server/test/unit/services/workerHelper.test.ts index 96b45d3b3..fe7dca42d 100644 --- a/server/test/unit/services/workerHelper.test.ts +++ b/server/test/unit/services/workerHelper.test.ts @@ -71,13 +71,6 @@ const createHelper = (overrides?: Record) => { // โ”€โ”€ Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("WorkerHelper", () => { - describe("serviceName", () => { - it("returns JobQueueHelper", () => { - const { helper } = createHelper(); - expect(helper.serviceName).toBe("JobQueueHelper"); - }); - }); - // โ”€โ”€ getCleanupOrphanedJob โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ describe("getCleanupOrphanedJob", () => { diff --git a/server/test/unit/utils/dataUtils.test.ts b/server/test/unit/utils/dataUtils.test.ts new file mode 100644 index 000000000..af9434bab --- /dev/null +++ b/server/test/unit/utils/dataUtils.test.ts @@ -0,0 +1,120 @@ +import { describe, expect, it } from "@jest/globals"; +import { getDateForRange, getDateFormat, NormalizeData } from "../../../src/utils/dataUtils.ts"; +import { DateRanges, type DateRange } from "../../../src/types/query.ts"; + +// โ”€โ”€ Helpers โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +const makeCheck = (responseTime: number, overrides?: Record) => ({ + id: `check-${responseTime}`, + status: true, + responseTime, + ...overrides, +}); + +// โ”€โ”€ Tests โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ + +describe("getDateForRange", () => { + it.each([ + ["recent", 2 * 60 * 60 * 1000], + ["hour", 60 * 60 * 1000], + ["day", 24 * 60 * 60 * 1000], + ["week", 7 * 24 * 60 * 60 * 1000], + ["month", 30 * 24 * 60 * 60 * 1000], + ])("returns now minus the %s window", (range, offsetMs) => { + const before = Date.now(); + const result = getDateForRange(range as DateRange); + const after = Date.now(); + + expect(result).toBeInstanceOf(Date); + expect(result.getTime()).toBeGreaterThanOrEqual(before - offsetMs); + expect(result.getTime()).toBeLessThanOrEqual(after - offsetMs); + }); + + it("returns the epoch for 'all'", () => { + expect(getDateForRange("all")).toEqual(new Date(0)); + }); +}); + +describe("getDateFormat", () => { + it.each([ + ["recent", "%Y-%m-%dT%H:%M:00Z"], + ["hour", "%Y-%m-%dT%H:%M:00Z"], + ["day", "%Y-%m-%dT%H:00:00Z"], + ["week", "%Y-%m-%dT00:00:00Z"], + ["month", "%Y-%m-%dT00:00:00Z"], + ["all", "%Y-%m-%dT00:00:00Z"], + ])("returns the %s bucket format", (range, format) => { + expect(getDateFormat(range as DateRange)).toBe(format); + }); + + it("has a format for every member of DateRanges", () => { + for (const range of DateRanges) { + expect(getDateFormat(range)).toMatch(/^%Y-%m-%d/); + } + }); +}); + +describe("NormalizeData", () => { + // Fixture math: p0 of [100..500] is 100; p95 interpolates between the 4th and 5th + // sorted values at weight 0.8 โ†’ 400*0.2 + 500*0.8 = 480. Values rescale into + // [10, 100] via 10 + (rt - 100) * 90 / 380, clamped. + it("rescales responseTime into [rangeMin, rangeMax] anchored to the p0โ€“p95 window", () => { + const checks = [100, 200, 300, 400, 500].map((rt) => makeCheck(rt)); + + const result = NormalizeData(checks, 10, 100); + + expect(result[0]!.responseTime).toBe(10); + expect(result[1]!.responseTime).toBeCloseTo(33.6842, 4); + expect(result[2]!.responseTime).toBeCloseTo(57.3684, 4); + expect(result[3]!.responseTime).toBeCloseTo(81.0526, 4); + }); + + it("clamps values above the p95 anchor to rangeMax", () => { + const checks = [100, 200, 300, 400, 500].map((rt) => makeCheck(rt)); + + const result = NormalizeData(checks, 10, 100); + + expect(result[4]!.responseTime).toBe(100); + }); + + it("preserves the raw value in originalResponseTime and spreads other fields", () => { + const checks = [makeCheck(100, { message: "OK" }), makeCheck(500)]; + + const result = NormalizeData(checks, 10, 100); + + expect(result[0]!.originalResponseTime).toBe(100); + expect(result[1]!.originalResponseTime).toBe(500); + expect(result[0]).toMatchObject({ id: "check-100", status: true, message: "OK" }); + }); + + it("passes a single check through, only adding originalResponseTime", () => { + const result = NormalizeData([makeCheck(250)], 10, 100); + + expect(result).toEqual([{ id: "check-250", status: true, responseTime: 250, originalResponseTime: 250 }]); + }); + + it("returns an empty array for empty input", () => { + expect(NormalizeData([], 10, 100)).toEqual([]); + }); + + it("records originalResponseTime as 0 for a non-numeric responseTime (failed checks)", () => { + const checks = [makeCheck(undefined as unknown as number), makeCheck(100), makeCheck(200)]; + + const result = NormalizeData(checks, 10, 100); + + expect(result[0]!.originalResponseTime).toBe(0); + }); + + // Characterization, not endorsement: when every value is equal the p0โ€“p95 window + // has zero width and the rescale divides by zero. Fixing this is a planned + // follow-up (docs/planning/dataUtils-generic-normalization-refactor.md); this + // test pins the current behavior so the fix is a deliberate, visible change. + it("produces NaN when all values are equal (division by zero โ€” known edge case)", () => { + const result = NormalizeData([makeCheck(50), makeCheck(50), makeCheck(50)], 10, 100); + + result.forEach((check) => { + expect(check.responseTime).toBeNaN(); + expect(check.originalResponseTime).toBe(50); + }); + }); +}); diff --git a/server/test/unit/validation/userValidation.test.ts b/server/test/unit/validation/userValidation.test.ts index c50a70caa..1c2ffb83a 100644 --- a/server/test/unit/validation/userValidation.test.ts +++ b/server/test/unit/validation/userValidation.test.ts @@ -43,4 +43,25 @@ describe("editUserBodyValidation", () => { }); expect(result.success).toBe(false); }); + + // Regression: `deleteProfileImage` was missing from this schema, so z.object() + // stripped it before it reached the repository and the remove-profile-image + // flow was silently a no-op. The flag arrives as the multipart form-data + // string "true", so it must both survive parsing and coerce to a boolean. + it("retains deleteProfileImage and coerces the multipart string 'true' to boolean true", () => { + const result = editUserBodyValidation.safeParse({ deleteProfileImage: "true" }); + expect(result.success).toBe(true); + expect(result.data).toEqual({ deleteProfileImage: true }); + }); + + it("coerces the multipart string 'false' to boolean false", () => { + const result = editUserBodyValidation.safeParse({ deleteProfileImage: "false" }); + expect(result.success).toBe(true); + expect(result.data).toEqual({ deleteProfileImage: false }); + }); + + it("rejects a non-boolean deleteProfileImage value", () => { + const result = editUserBodyValidation.safeParse({ deleteProfileImage: "banana" }); + expect(result.success).toBe(false); + }); });