From 4a093e2b97dff0fde711c891a4aee6e55d478945 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 8 Jul 2026 10:46:34 -0700 Subject: [PATCH 01/84] start health server on primary process true --- server/src/index.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/server/src/index.ts b/server/src/index.ts index 44536ce8b..b5e870ded 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; @@ -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) => { From f7d3bcb69617614b40b2de2b74ffca5c0486482b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 8 Jul 2026 10:47:03 -0700 Subject: [PATCH 02/84] add root docker-compose --- .dockerignore | 6 +++++ docker/docker-compose.yaml | 45 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 .dockerignore create mode 100644 docker/docker-compose.yaml 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/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: From 6d91efecad3ccf24a8937e6f9d73d7030b93c204 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 8 Jul 2026 11:04:47 -0700 Subject: [PATCH 03/84] default client to same-origin /api/v1 and window.location.origin, drop UPTIME_APP_* placeholders --- client/.env.production | 10 ++++++---- .../Account/components/InviteTeamMemberDialog.tsx | 2 +- client/src/Types/env.d.ts | 3 ++- client/src/Utils/ApiClient.ts | 2 +- 4 files changed, 10 insertions(+), 7 deletions(-) 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/src/Pages/Account/components/InviteTeamMemberDialog.tsx b/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx index e23f24e53..b3e08fe67 100644 --- a/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx +++ b/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx @@ -56,7 +56,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/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..d88dca6b1 100644 --- a/client/src/Utils/ApiClient.ts +++ b/client/src/Utils/ApiClient.ts @@ -2,7 +2,7 @@ import axios from "axios"; import type { AxiosError } from "axios"; import type { AxiosRequestConfig, AxiosResponse } from "axios"; -const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL; +const BASE_URL = import.meta.env.VITE_APP_API_BASE_URL || "/api/v1"; const api = axios.create({ baseURL: BASE_URL, From ef175950b253fdd9dc1a7f390e1cb53e22bf00c9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 9 Jul 2026 09:29:31 -0700 Subject: [PATCH 04/84] add dockerfile and build-image workflow --- .github/workflows/build-image.yml | 59 +++++++++++++++++++++++++++++++ client/package-lock.json | 24 ++----------- docker/Dockerfile | 45 +++++++++++++++++++++++ server/package-lock.json | 42 ++++++++++++---------- 4 files changed, 130 insertions(+), 40 deletions(-) create mode 100644 .github/workflows/build-image.yml create mode 100644 docker/Dockerfile diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml new file mode 100644 index 000000000..7d59f19cf --- /dev/null +++ b/.github/workflows/build-image.yml @@ -0,0 +1,59 @@ +name: Build image + +on: + push: + branches: ["develop", "master"] + tags: ["v*"] + workflow_dispatch: + +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/bluewave-labs/checkmate + ghcr.io/bluewave-labs/checkmate-backend-mono + flavor: | + latest=false + tags: | + type=ref,event=tag + type=semver,pattern={{version}} + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} + type=raw,value=edge,enable=${{ startsWith(github.ref, 'refs/heads/') }} + + - 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 diff --git a/client/package-lock.json b/client/package-lock.json index 8af4fb08a..ba1812e60 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -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/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 000000000..380210f53 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,45 @@ +# --------------------- +# Frontend build stage +# --------------------- +# 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:22-slim AS frontend-build + +WORKDIR /app/client + +COPY client/package*.json ./ + +RUN npm ci + +COPY client ./ + +RUN npm run build + +# --------------------- +# Backend stage +# --------------------- +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/package*.json ./ + +RUN npm ci + +COPY server ./ + +RUN npm run build + +COPY --from=frontend-build /app/client/dist ./public + +USER node + +EXPOSE 52345 52346 + +CMD ["node", "dist/index.js"] 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" } From 62031a0ab1d663b5a956f8abb8e700bd9c829ea9 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 9 Jul 2026 09:49:45 -0700 Subject: [PATCH 05/84] add a CLIENT_CONFIG to the client, serve from BE with env vars --- client/index.html | 1 + client/public/config.js | 4 ++++ .../components/InviteTeamMemberDialog.tsx | 3 ++- client/src/Utils/ApiClient.ts | 4 +++- client/src/Utils/logger.ts | 4 +++- client/src/Utils/runtimeConfig.ts | 18 ++++++++++++++++++ client/src/Utils/statusPageUrl.ts | 5 ++++- server/src/app.ts | 7 +++++++ server/src/config/envValidation.ts | 10 ++++++++-- .../app-settings/app-settings.service.ts | 8 +++++++- .../domain/app-settings/app-settings.type.ts | 11 +++++++++++ .../test/unit/services/settingsService.test.ts | 13 +++++++++++++ 12 files changed, 81 insertions(+), 7 deletions(-) create mode 100644 client/public/config.js create mode 100644 client/src/Utils/runtimeConfig.ts 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/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/Pages/Account/components/InviteTeamMemberDialog.tsx b/client/src/Pages/Account/components/InviteTeamMemberDialog.tsx index b3e08fe67..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; diff --git a/client/src/Utils/ApiClient.ts b/client/src/Utils/ApiClient.ts index d88dca6b1..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 || "/api/v1"; +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/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/server/src/app.ts b/server/src/app.ts index 2ed1c2f01..c363db5d2 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -52,6 +52,13 @@ 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(express.json()); diff --git a/server/src/config/envValidation.ts b/server/src/config/envValidation.ts index 05471e690..0bfaa01ce 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(), diff --git a/server/src/domain/app-settings/app-settings.service.ts b/server/src/domain/app-settings/app-settings.service.ts index 8beb65d84..92a54702e 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,6 +16,7 @@ export type EnvConfig = { queueMode: QueueMode; queuePrimaryProcesses: boolean; statusPageThemesEnabled: boolean; + clientConfig: ClientRuntimeConfig; }; export interface ISettingsService { @@ -44,6 +45,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 }), + }, }; } 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/test/unit/services/settingsService.test.ts b/server/test/unit/services/settingsService.test.ts index af58b5705..43ea7fe18 100644 --- a/server/test/unit/services/settingsService.test.ts +++ b/server/test/unit/services/settingsService.test.ts @@ -83,6 +83,19 @@ 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.loadSettings().clientConfig).toEqual({ + apiBaseUrl: "https://api.example.com/api/v1", + logLevel: "warn", }); }); From dffce2709eec746e04f5a6f59d9451a5e08b8fc8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 9 Jul 2026 10:19:38 -0700 Subject: [PATCH 06/84] clean up docker files --- docker/coolify/build_images.bat | 33 ----- docker/coolify/build_images.sh | 24 ---- docker/coolify/client.Dockerfile | 22 ---- docker/coolify/mongoDB.Dockerfile | 3 - docker/coolify/nginx/conf.d/default.conf | 35 ------ docker/coolify/server.Dockerfile | 20 --- docker/dev/build_images.bat | 33 ----- docker/dev/build_images.sh | 29 ----- docker/dev/client.Dockerfile | 20 --- .../dev/docker-compose.custom-ca-example.yaml | 21 +--- docker/dev/docker-compose.yaml | 30 ++--- docker/dev/mongoDB.Dockerfile | 3 - docker/dev/nginx/conf.d/default.conf | 35 ------ docker/dev/server.Dockerfile | 20 --- docker/dist-arm/build_images.sh | 44 ------- docker/dist-arm/docker-compose.yaml | 28 ----- docker/dist-arm/server.Dockerfile | 50 -------- docker/dist-mono/build_images.bat | 30 ----- docker/dist-mono/build_images.sh | 28 ----- docker/dist-mono/docker-compose-local.yaml | 27 ----- docker/dist-mono/docker-compose.yaml | 28 ----- docker/dist-mono/mongoDB.Dockerfile | 3 - docker/dist-mono/server.Dockerfile | 37 ------ docker/dist/build_images.bat | 33 ----- docker/dist/build_images.sh | 25 ---- docker/dist/client.Dockerfile | 22 ---- docker/dist/docker-compose.yaml | 38 ------ docker/dist/mongoDB.Dockerfile | 3 - docker/dist/nginx/conf.d/default.conf | 35 ------ docker/dist/server.Dockerfile | 20 --- docker/prod/build_images.bat | 33 ----- docker/prod/build_images.sh | 25 ---- docker/prod/certbot-compose.yaml | 19 --- docker/prod/client.Dockerfile | 20 --- docker/prod/docker-compose.yaml | 53 -------- docker/prod/mongo/init/init.js | 41 ------- docker/prod/mongoDB.Dockerfile | 3 - docker/prod/nginx/conf.d/cerbot | 15 --- docker/prod/nginx/conf.d/default.conf | 69 ----------- docker/prod/server.Dockerfile | 24 ---- docker/staging/build_images.bat | 33 ----- docker/staging/build_images.sh | 25 ---- docker/staging/cerbot-compose.yaml | 19 --- docker/staging/client.Dockerfile | 20 --- docker/staging/docker-compose.yaml | 62 ---------- .../staging/mongo/init/01_init_replica_set.js | 15 --- docker/staging/mongo/init/02_create_users.js | 16 --- docker/staging/mongoDB.Dockerfile | 3 - docker/staging/nginx/conf.d/certbot | 15 --- docker/staging/nginx/conf.d/default.conf | 77 ------------ docker/staging/server.Dockerfile | 24 ---- docker/unified/build_images.bat | 33 ----- docker/unified/build_images.sh | 25 ---- docker/unified/certbot-compose.yaml | 19 --- docker/unified/client.Dockerfile | 20 --- docker/unified/docker-compose.yaml | 114 ------------------ docker/unified/gateway/nginx.conf | 55 --------- docker/unified/mongoDB.Dockerfile | 3 - docker/unified/nginx/conf.d/prod.conf | 31 ----- docker/unified/nginx/conf.d/staging.conf | 31 ----- docker/unified/server.Dockerfile | 22 ---- 61 files changed, 15 insertions(+), 1728 deletions(-) delete mode 100644 docker/coolify/build_images.bat delete mode 100755 docker/coolify/build_images.sh delete mode 100755 docker/coolify/client.Dockerfile delete mode 100755 docker/coolify/mongoDB.Dockerfile delete mode 100755 docker/coolify/nginx/conf.d/default.conf delete mode 100755 docker/coolify/server.Dockerfile delete mode 100644 docker/dev/build_images.bat delete mode 100755 docker/dev/build_images.sh delete mode 100755 docker/dev/client.Dockerfile delete mode 100755 docker/dev/mongoDB.Dockerfile delete mode 100755 docker/dev/nginx/conf.d/default.conf delete mode 100755 docker/dev/server.Dockerfile delete mode 100755 docker/dist-arm/build_images.sh delete mode 100644 docker/dist-arm/docker-compose.yaml delete mode 100644 docker/dist-arm/server.Dockerfile delete mode 100644 docker/dist-mono/build_images.bat delete mode 100755 docker/dist-mono/build_images.sh delete mode 100755 docker/dist-mono/docker-compose-local.yaml delete mode 100755 docker/dist-mono/docker-compose.yaml delete mode 100755 docker/dist-mono/mongoDB.Dockerfile delete mode 100644 docker/dist-mono/server.Dockerfile delete mode 100644 docker/dist/build_images.bat delete mode 100755 docker/dist/build_images.sh delete mode 100755 docker/dist/client.Dockerfile delete mode 100755 docker/dist/docker-compose.yaml delete mode 100755 docker/dist/mongoDB.Dockerfile delete mode 100755 docker/dist/nginx/conf.d/default.conf delete mode 100755 docker/dist/server.Dockerfile delete mode 100644 docker/prod/build_images.bat delete mode 100755 docker/prod/build_images.sh delete mode 100755 docker/prod/certbot-compose.yaml delete mode 100755 docker/prod/client.Dockerfile delete mode 100755 docker/prod/docker-compose.yaml delete mode 100644 docker/prod/mongo/init/init.js delete mode 100755 docker/prod/mongoDB.Dockerfile delete mode 100755 docker/prod/nginx/conf.d/cerbot delete mode 100755 docker/prod/nginx/conf.d/default.conf delete mode 100755 docker/prod/server.Dockerfile delete mode 100644 docker/staging/build_images.bat delete mode 100755 docker/staging/build_images.sh delete mode 100755 docker/staging/cerbot-compose.yaml delete mode 100644 docker/staging/client.Dockerfile delete mode 100755 docker/staging/docker-compose.yaml delete mode 100644 docker/staging/mongo/init/01_init_replica_set.js delete mode 100755 docker/staging/mongo/init/02_create_users.js delete mode 100755 docker/staging/mongoDB.Dockerfile delete mode 100755 docker/staging/nginx/conf.d/certbot delete mode 100755 docker/staging/nginx/conf.d/default.conf delete mode 100755 docker/staging/server.Dockerfile delete mode 100644 docker/unified/build_images.bat delete mode 100755 docker/unified/build_images.sh delete mode 100755 docker/unified/certbot-compose.yaml delete mode 100755 docker/unified/client.Dockerfile delete mode 100755 docker/unified/docker-compose.yaml delete mode 100644 docker/unified/gateway/nginx.conf delete mode 100755 docker/unified/mongoDB.Dockerfile delete mode 100755 docker/unified/nginx/conf.d/prod.conf delete mode 100755 docker/unified/nginx/conf.d/staging.conf delete mode 100755 docker/unified/server.Dockerfile 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-arm/server.Dockerfile b/docker/dist-arm/server.Dockerfile deleted file mode 100644 index 5bdefd5bf..000000000 --- a/docker/dist-arm/server.Dockerfile +++ /dev/null @@ -1,50 +0,0 @@ -# --------------------- -# Frontend build stage -# --------------------- -# Build on the host arch (BUILDPLATFORM) rather than under QEMU — the output is -# pure JS so it's arch-independent, and this avoids the esbuild host/native -# binary version mismatch under emulation. -FROM --platform=$BUILDPLATFORM node:20-slim AS frontend-build - -WORKDIR /app/client - -COPY client/package*.json ./ - -RUN npm ci - -COPY client ./ - -RUN npm run build - -# --------------------- -# Backend stage -# --------------------- -FROM node:22-slim AS backend - -# Install ping -RUN apt-get update \ - && apt-get install -y iputils-ping \ - && rm -rf /var/lib/apt/lists/* - -WORKDIR /app/server - -COPY server/package.json ./ - -RUN npm install - -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 - - -EXPOSE 52345 - -CMD ["sh", "-c", "./scripts/inject-vars.sh && node ./dist/index.js"] 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/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/certbot-compose.yaml b/docker/prod/certbot-compose.yaml deleted file mode 100755 index 9220d07a1..000000000 --- a/docker/prod/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/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/gateway/nginx.conf b/docker/unified/gateway/nginx.conf deleted file mode 100644 index 47ad4695e..000000000 --- a/docker/unified/gateway/nginx.conf +++ /dev/null @@ -1,55 +0,0 @@ -proxy_set_header Host $host; -proxy_set_header X-Real-IP $remote_addr; -proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; -proxy_set_header X-Forwarded-Proto $scheme; - - -server { - listen 80; - server_name demo.checkmate.so staging.checkmate.so; - - location /.well-known/acme-challenge/ { - # Forward the challenge to Certbot listening on 8080 - proxy_pass http://172.17.0.1:8080; - } - - location / { - return 301 https://$host$request_uri; - } -} - - -# --- PRODUCTION: demo.checkmate.so --- -server { - listen 443 ssl; - server_name demo.checkmate.so; - - ssl_certificate /etc/nginx/ssl/live/demo.checkmate.so/fullchain.pem; - ssl_certificate_key /etc/nginx/ssl/live/demo.checkmate.so/privkey.pem; - - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; - - location / { - # Passes traffic to the prod client on port 80 - proxy_pass http://client-prod:80; - } -} - -# --- STAGING: staging.checkmate.so --- - - server { - listen 443 ssl; - server_name staging.checkmate.so; - - ssl_certificate /etc/nginx/ssl/live/staging.checkmate.so/fullchain.pem; - ssl_certificate_key /etc/nginx/ssl/live/staging.checkmate.so/privkey.pem; - - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers HIGH:!aNULL:!MD5; - - location / { - # Passes traffic to the staging client on port 80 - proxy_pass http://client-staging:80; - } - } 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 From 40a7599388374afb0855780e716402ed42a1c989 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 9 Jul 2026 10:20:24 -0700 Subject: [PATCH 07/84] update workflows --- .github/workflows/build-image.yml | 44 +++++- .../workflows/deploy-images-on-release.yml | 148 ------------------ .github/workflows/deploy-images.yml | 143 ----------------- .github/workflows/production-deploy.yml | 91 ----------- .github/workflows/staging-deploy.yml | 91 ----------- 5 files changed, 42 insertions(+), 475 deletions(-) delete mode 100644 .github/workflows/deploy-images-on-release.yml delete mode 100644 .github/workflows/deploy-images.yml delete mode 100644 .github/workflows/production-deploy.yml delete mode 100644 .github/workflows/staging-deploy.yml diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 7d59f19cf..b1c80ee36 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -2,7 +2,7 @@ name: Build image on: push: - branches: ["develop", "master"] + branches: ["develop", "master", "demo"] tags: ["v*"] workflow_dispatch: @@ -41,10 +41,10 @@ jobs: 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') }} - type=raw,value=edge,enable=${{ startsWith(github.ref, 'refs/heads/') }} - name: Build and push multi-arch image uses: docker/build-push-action@v6 @@ -57,3 +57,43 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max + + # The hosted demo/staging instances share one host and one compose file; a + # deploy recreates whichever service's image tag was just pushed. + 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 + docker compose up -d + docker system prune -af + + deploy-demo: + needs: build-and-push + if: github.ref == 'refs/heads/demo' + 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 + docker compose up -d + 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 From bbb051406f71df17f0cce15b1b1c30aebf5e5c97 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 9 Jul 2026 10:21:09 -0700 Subject: [PATCH 08/84] udpate docs --- CLAUDE.md | 6 +- CONTRIBUTING.md | 26 +------- README.md | 11 +++- client/.gitignore | 1 - client/env.sh | 8 --- docs/README.md | 2 - docs/custom-ca-trust.md | 51 ++++++---------- server/.github/pull_request_template.md | 25 -------- server/.github/workflows/staging-deploy.yml | 68 --------------------- server/scripts/inject-vars.sh | 7 --- 10 files changed, 32 insertions(+), 173 deletions(-) delete mode 100755 client/env.sh delete mode 100755 server/.github/pull_request_template.md delete mode 100755 server/.github/workflows/staging-deploy.yml delete mode 100755 server/scripts/inject-vars.sh 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..b93d6e7d8 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) diff --git a/README.md b/README.md index c6830f566..05eb688ff 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,16 @@ 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. + +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/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/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/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/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 From 0475f194e9b0cae5caaba95a34b61417d50a83a1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 9 Jul 2026 10:24:06 -0700 Subject: [PATCH 09/84] temporary branch --- .github/workflows/build-image.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index b1c80ee36..a9d488a06 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -2,7 +2,9 @@ name: Build image on: push: - branches: ["develop", "master", "demo"] + # chore/simplify-deployment is temporary, for pre-merge testing of this + # workflow itself — remove before merge + branches: ["develop", "master", "demo", "chore/simplify-deployment"] tags: ["v*"] workflow_dispatch: From 312961e8c95b4c7451c40b59f3b5af999e9d722e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 9 Jul 2026 10:27:56 -0700 Subject: [PATCH 10/84] use repo owner --- .github/workflows/build-image.yml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index a9d488a06..1356c8df5 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -8,6 +8,10 @@ on: tags: ["v*"] workflow_dispatch: +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: build-and-push: runs-on: ubuntu-latest @@ -38,8 +42,8 @@ jobs: uses: docker/metadata-action@v5 with: images: | - ghcr.io/bluewave-labs/checkmate - ghcr.io/bluewave-labs/checkmate-backend-mono + ghcr.io/${{ github.repository_owner }}/checkmate + ghcr.io/${{ github.repository_owner }}/checkmate-backend-mono flavor: | latest=false tags: | From 91343f996b2b2c69af7e5e66e1d5bc0100f2cbf0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 9 Jul 2026 10:55:04 -0700 Subject: [PATCH 11/84] hosting docs --- docs/hosting/checkmate-so/README.md | 51 ++++++++ .../hosting/checkmate-so/certbot-compose.yaml | 19 +++ docs/hosting/checkmate-so/docker-compose.yaml | 122 ++++++++++++++++++ docs/hosting/checkmate-so/gateway/nginx.conf | 54 ++++++++ 4 files changed, 246 insertions(+) create mode 100644 docs/hosting/checkmate-so/README.md create mode 100755 docs/hosting/checkmate-so/certbot-compose.yaml create mode 100644 docs/hosting/checkmate-so/docker-compose.yaml create mode 100644 docs/hosting/checkmate-so/gateway/nginx.conf diff --git a/docs/hosting/checkmate-so/README.md b/docs/hosting/checkmate-so/README.md new file mode 100644 index 000000000..d2a89cf09 --- /dev/null +++ b/docs/hosting/checkmate-so/README.md @@ -0,0 +1,51 @@ +# 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: + +- push to `develop` → image tagged `:develop` → `checkmate-staging` recreated +- push to `demo` → image tagged `:demo` → `checkmate-demo` recreated + +The host keeps a git checkout of this repo; the deploy does `git pull` first so +compose/gateway changes land with the code. + +## 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. +- `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/docs/hosting/checkmate-so/certbot-compose.yaml b/docs/hosting/checkmate-so/certbot-compose.yaml new file mode 100755 index 000000000..9220d07a1 --- /dev/null +++ b/docs/hosting/checkmate-so/certbot-compose.yaml @@ -0,0 +1,19 @@ +version: "3" + +services: + webserver: + image: nginx:latest + ports: + - 80:80 + - 443:443 + restart: always + volumes: + - ./nginx/conf.d/:/etc/nginx/conf.d/:ro + - ./certbot/www/:/var/www/certbot/:ro + certbot: + image: certbot/certbot:latest + volumes: + - ./certbot/www/:/var/www/certbot/:rw + - ./certbot/conf/:/etc/letsencrypt/:rw + depends_on: + - webserver diff --git a/docs/hosting/checkmate-so/docker-compose.yaml b/docs/hosting/checkmate-so/docker-compose.yaml new file mode 100644 index 000000000..65bee76d1 --- /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 + + # Demo: demo.checkmate.so ----- + checkmate-demo: + image: ghcr.io/bluewave-labs/checkmate:demo + 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/docs/hosting/checkmate-so/gateway/nginx.conf b/docs/hosting/checkmate-so/gateway/nginx.conf new file mode 100644 index 000000000..3523872c5 --- /dev/null +++ b/docs/hosting/checkmate-so/gateway/nginx.conf @@ -0,0 +1,54 @@ +proxy_set_header Host $host; +proxy_set_header X-Real-IP $remote_addr; +proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; +proxy_set_header X-Forwarded-Proto $scheme; + + +server { + listen 80; + server_name demo.checkmate.so staging.checkmate.so; + + location /.well-known/acme-challenge/ { + # Forward the challenge to Certbot listening on 8080 + proxy_pass http://172.17.0.1:8080; + } + + location / { + return 301 https://$host$request_uri; + } +} + + +# --- DEMO: demo.checkmate.so --- +server { + listen 443 ssl; + server_name demo.checkmate.so; + + ssl_certificate /etc/nginx/ssl/live/demo.checkmate.so/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/live/demo.checkmate.so/privkey.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + location / { + # 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; + + ssl_certificate /etc/nginx/ssl/live/staging.checkmate.so/fullchain.pem; + ssl_certificate_key /etc/nginx/ssl/live/staging.checkmate.so/privkey.pem; + + ssl_protocols TLSv1.2 TLSv1.3; + ssl_ciphers HIGH:!aNULL:!MD5; + + location / { + # The mono image serves both the SPA and the API + proxy_pass http://checkmate-staging:52345; + } +} From ceacaa54998aafd442b9a4f41459a4ef250ab33f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 9 Jul 2026 11:24:35 -0700 Subject: [PATCH 12/84] scope workflows --- .github/workflows/build-image.yml | 20 +++++++++---------- docs/hosting/checkmate-so/README.md | 13 +++++++----- docs/hosting/checkmate-so/docker-compose.yaml | 4 ++-- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml index 1356c8df5..f7e204159 100644 --- a/.github/workflows/build-image.yml +++ b/.github/workflows/build-image.yml @@ -2,9 +2,7 @@ name: Build image on: push: - # chore/simplify-deployment is temporary, for pre-merge testing of this - # workflow itself — remove before merge - branches: ["develop", "master", "demo", "chore/simplify-deployment"] + branches: ["develop", "master"] tags: ["v*"] workflow_dispatch: @@ -64,8 +62,8 @@ jobs: cache-from: type=gha cache-to: type=gha,mode=max - # The hosted demo/staging instances share one host and one compose file; a - # deploy recreates whichever service's image tag was just pushed. + # 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' @@ -81,13 +79,13 @@ jobs: cd checkmate git pull cd docs/hosting/checkmate-so - docker compose pull - docker compose up -d + docker compose pull checkmate-staging + docker compose up -d checkmate-staging docker system prune -af - deploy-demo: + deploy-prod: needs: build-and-push - if: github.ref == 'refs/heads/demo' + if: github.ref == 'refs/heads/master' runs-on: ubuntu-latest steps: - name: Deploy demo.checkmate.so @@ -100,6 +98,6 @@ jobs: cd checkmate git pull cd docs/hosting/checkmate-so - docker compose pull - docker compose up -d + docker compose pull checkmate-demo + docker compose up -d checkmate-demo docker system prune -af diff --git a/docs/hosting/checkmate-so/README.md b/docs/hosting/checkmate-so/README.md index d2a89cf09..2a7e9f648 100644 --- a/docs/hosting/checkmate-so/README.md +++ b/docs/hosting/checkmate-so/README.md @@ -12,13 +12,16 @@ 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: +and runs `docker compose pull && docker compose up -d ` in +this directory: -- push to `develop` → image tagged `:develop` → `checkmate-staging` recreated -- push to `demo` → image tagged `:demo` → `checkmate-demo` recreated +- 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; the deploy does `git pull` first so -compose/gateway changes land with the code. +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) diff --git a/docs/hosting/checkmate-so/docker-compose.yaml b/docs/hosting/checkmate-so/docker-compose.yaml index 65bee76d1..96849fecb 100644 --- a/docs/hosting/checkmate-so/docker-compose.yaml +++ b/docs/hosting/checkmate-so/docker-compose.yaml @@ -20,9 +20,9 @@ services: - /etc/letsencrypt:/etc/nginx/ssl:ro - ./gateway/nginx.conf:/etc/nginx/conf.d/default.conf:ro - # Demo: demo.checkmate.so ----- + # Prod: demo.checkmate.so ----- checkmate-demo: - image: ghcr.io/bluewave-labs/checkmate:demo + image: ghcr.io/bluewave-labs/checkmate:master container_name: checkmate-demo restart: always env_file: From 5feb666bdb033f241f95b74b9bb5edd4d8fcd571 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Thu, 9 Jul 2026 21:55:25 +0300 Subject: [PATCH 13/84] fix(status-page): inject sanitized customCSS on the public page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The customCSS field on status pages was persisted and validated end-to-end but never rendered on the public page, so operator-supplied styles had no effect. Inject the string as a } Date: Fri, 10 Jul 2026 00:06:31 +0300 Subject: [PATCH 14/84] fix(server): raise express.json body limit to 10mb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Express defaults to a 100kb JSON body, which is too small for realistic imports through POST /api/v1/monitors/import/json (backups with many monitors and their config easily exceed 100kb, returning "request entity too large"). Raise the parser cap to 10mb. That covers real-world import payloads while keeping the DoS surface bounded — a larger, streaming file-upload route for imports remains desirable as a follow-up. --- server/src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/app.ts b/server/src/app.ts index 2ed1c2f01..bf8c140c1 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -54,7 +54,7 @@ export const createApp = ({ app.use(express.static(frontendPath)); - app.use(express.json()); + app.use(express.json({ limit: "10mb" })); app.use(cookieParser()); app.use(sanitizeBody()); From 2810cd35625f861c7ed981097c22e6c10cfc7d00 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 10 Jul 2026 03:16:42 +0300 Subject: [PATCH 15/84] fix(infrastructure): trigger network "no traffic" notice on interface name only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The notice at the top of the network charts was firing whenever the aggregated per-second delta was zero across all interfaces. That is a false positive when Capture is running on a real host but the polling interval is >= 1 minute and the aggregation bucket is also 1 minute — first == last in the bucket, delta is zero, but the interface has plenty of real traffic in its cumulative counters. Base the notice on interface names instead: only show it when the ONLY interfaces Capture reports are loopback / container-bridge names (lo, lo0, docker0). Real NICs (enp0s6, eth0, wlan0, tunN, vethN, brN, etc.) keep the notice hidden even when the aggregation currently reports zero throughput. The underlying zero-throughput bug (single-sample buckets producing zero deltas) is tracked separately. --- .../Details/Components/ChartsNetwork.tsx | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/client/src/Pages/Infrastructure/Details/Components/ChartsNetwork.tsx b/client/src/Pages/Infrastructure/Details/Components/ChartsNetwork.tsx index c7ebac5be..dfb485a2b 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,7 +92,8 @@ export const InfraNetworkCharts = ({ () => getChartConfigs(theme, checks, t), [theme, checks, t] ); - const showNoTrafficNotice = chartConfigs.length > 0 && !hasAnyNetworkTraffic(checks); + const showNoTrafficNotice = + chartConfigs.length > 0 && onlyContainerVisibleInterfaces(checks); return ( From 9fa45c5871fc61f39246f19f28f28f6001d4d204 Mon Sep 17 00:00:00 2001 From: edobusy Date: Fri, 10 Jul 2026 23:46:49 +1000 Subject: [PATCH 16/84] docs(readme): add Italian translation --- README.md | 1 + docs/translations/README.ar.md | 1 + docs/translations/README.ca.md | 1 + docs/translations/README.cs.md | 1 + docs/translations/README.de.md | 1 + docs/translations/README.es.md | 1 + docs/translations/README.fi.md | 1 + docs/translations/README.fr.md | 1 + docs/translations/README.it.md | 199 ++++++++++++++++++++++++++++++ docs/translations/README.ja.md | 1 + docs/translations/README.pt-BR.md | 1 + docs/translations/README.ru.md | 1 + docs/translations/README.th.md | 1 + docs/translations/README.tr.md | 1 + docs/translations/README.uk.md | 1 + docs/translations/README.vi.md | 1 + docs/translations/README.zh-CN.md | 1 + docs/translations/README.zh-TW.md | 1 + 18 files changed, 216 insertions(+) create mode 100644 docs/translations/README.it.md diff --git a/README.md b/README.md index c6830f566..1e6e543e8 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ 🇪🇸 Español | 🇫🇮 Suomi | 🇫🇷 Français | + 🇮🇹 Italiano | 🇯🇵 日本語 | 🇧🇷 Português (Brasil) | 🇷🇺 Русский | diff --git a/docs/translations/README.ar.md b/docs/translations/README.ar.md index 81a0fc8d8..4997fc381 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) | 🇷🇺 Русский | diff --git a/docs/translations/README.ca.md b/docs/translations/README.ca.md index 94e46d49d..3552d6c4c 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) | 🇷🇺 Русский | diff --git a/docs/translations/README.cs.md b/docs/translations/README.cs.md index 3faa00334..13dd5d8b8 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) | 🇷🇺 Русский | diff --git a/docs/translations/README.de.md b/docs/translations/README.de.md index 59f897368..21f72165e 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) | 🇷🇺 Русский | diff --git a/docs/translations/README.es.md b/docs/translations/README.es.md index e72d67cc5..cbd8f4258 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) | 🇷🇺 Русский | diff --git a/docs/translations/README.fi.md b/docs/translations/README.fi.md index 91b91058b..cbfcb76cb 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) | 🇷🇺 Русский | diff --git a/docs/translations/README.fr.md b/docs/translations/README.fr.md index fc4341a28..209fc3dcc 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) | 🇷🇺 Русский | diff --git a/docs/translations/README.it.md b/docs/translations/README.it.md new file mode 100644 index 000000000..f97cb8420 --- /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..6534a3e3a 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) | 🇷🇺 Русский | diff --git a/docs/translations/README.pt-BR.md b/docs/translations/README.pt-BR.md index e96f7863e..6ddbe8367 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) | 🇷🇺 Русский | diff --git a/docs/translations/README.ru.md b/docs/translations/README.ru.md index 4f5d20725..1e22d0c9c 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) | 🇷🇺 Русский | diff --git a/docs/translations/README.th.md b/docs/translations/README.th.md index 0ea5de3fb..6e02f662e 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) | 🇷🇺 Русский | diff --git a/docs/translations/README.tr.md b/docs/translations/README.tr.md index 48c56f3b0..1e0b58416 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) | 🇷🇺 Русский | diff --git a/docs/translations/README.uk.md b/docs/translations/README.uk.md index 3f7b4630a..798cfd271 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) | 🇷🇺 Русский | diff --git a/docs/translations/README.vi.md b/docs/translations/README.vi.md index 13369f3d8..f800c77a6 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) | 🇷🇺 Русский | diff --git a/docs/translations/README.zh-CN.md b/docs/translations/README.zh-CN.md index 75ae7dc86..eb1e3c6f6 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) | 🇷🇺 Русский | diff --git a/docs/translations/README.zh-TW.md b/docs/translations/README.zh-TW.md index 8dfc605ec..c79944190 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) | 🇷🇺 Русский | From 9459edce004252cace4d7db35fe291a799fe49e9 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Sat, 11 Jul 2026 04:22:42 +0300 Subject: [PATCH 17/84] scope 10mb body limit to /monitors/import/json only Address review feedback: keep the global express.json() at the default 100kb and mount a route-scoped 10mb parser in front of it for /api/v1/monitors/import/json. Express matches middleware in registration order, so the import route uses the larger parser and every other endpoint keeps the tight default. No change in DoS surface for normal traffic. --- server/src/app.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/server/src/app.ts b/server/src/app.ts index bf8c140c1..12c88d5f6 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -54,7 +54,8 @@ export const createApp = ({ app.use(express.static(frontendPath)); - app.use(express.json({ limit: "10mb" })); + app.use("/api/v1/monitors/import/json", express.json({ limit: "10mb" })); + app.use(express.json()); app.use(cookieParser()); app.use(sanitizeBody()); From 77b817778b6dc24a7a47fcb6bad1febf7a43f6cc Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Sat, 11 Jul 2026 04:29:22 +0300 Subject: [PATCH 18/84] address review feedback: rename notice to match new trigger The notice now triggers on interface names (loopback / container-bridge only), not on zero traffic. Update the variable, i18n key, and English copy to match: - showNoTrafficNotice -> showLoopbackOnlyNotice - pages.infrastructure.charts.labels.netNoTraffic -> pages.infrastructure.charts.labels.loopbackOnlyNotice - English string rewritten to describe the loopback-only condition rather than "zero traffic on every interface" Other locales still hold the old string under the old key and will fall back to English until the translation pipeline picks up the new key. Tracked separately. --- .../Infrastructure/Details/Components/ChartsNetwork.tsx | 6 +++--- client/src/locales/en.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/client/src/Pages/Infrastructure/Details/Components/ChartsNetwork.tsx b/client/src/Pages/Infrastructure/Details/Components/ChartsNetwork.tsx index dfb485a2b..c84a327fa 100644 --- a/client/src/Pages/Infrastructure/Details/Components/ChartsNetwork.tsx +++ b/client/src/Pages/Infrastructure/Details/Components/ChartsNetwork.tsx @@ -92,14 +92,14 @@ export const InfraNetworkCharts = ({ () => getChartConfigs(theme, checks, t), [theme, checks, t] ); - const showNoTrafficNotice = + const showLoopbackOnlyNotice = chartConfigs.length > 0 && onlyContainerVisibleInterfaces(checks); return ( - {showNoTrafficNotice && ( + {showLoopbackOnlyNotice && ( - {t("pages.infrastructure.charts.labels.netNoTraffic")} + {t("pages.infrastructure.charts.labels.loopbackOnlyNotice")} )} Date: Mon, 13 Jul 2026 15:52:12 +0300 Subject: [PATCH 19/84] fix(status page): apply brand color via a dedicated brand token MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The status page appearance form saves a customer-picked brand color (`statusPage.color`), but `StatusPageThemeProvider` never consumed it — every rendered surface styled from `tokens.up`, so the picker had no visible effect on the public page. Rather than overriding `tokens.up` (the semantic OK/up status color, which would let a red or orange brand color render the "all operational" indicator in a red that reads as downtime), this introduces a separate `brand` / `brandStrong` / `brandSoft` token triple that is distinct from the up/OK color. ## Changes - `themes/tokens.ts`: add optional `brand` / `brandStrong` / `brandSoft` fields on `StatusPageThemeTokens`. Themes leave them undefined; the provider falls back to the theme's `up*` values so nothing regresses before individual themes opt in. - `themes/StatusPageThemeProvider.tsx`: add `brandColor` prop and a `resolveBrandTokens` helper. When a valid hex is provided it feeds `brand`, `brandStrong = darken(x, 0.2)`, `brandSoft = alpha(x, 0.15)` using MUI's shared `alpha`/`darken` (replaces the ad-hoc hex helpers). Invalid or empty values fall through cleanly. - `Status/index.tsx`: pass `brandColor={statusPage.color}` only at the public route. The admin preview provider wraps an iframe whose document renders itself, so passing the prop there had no effect and is dropped. - `themes/refined/styles.ts`: switch accent surfaces (hero logo, active chart-switch button, hardware pill, footer link) from `tokens.up` to `tokens.brand`. Uptime bars, status pills, and OK indicators stay on `tokens.up`. Other themes keep their current styling until a designer decides which surfaces should switch. ## Benefits - Brand Color picker actually changes the rendered accent surfaces. - The semantic "OK/up" status color is never repainted by the brand color, so operational-state indicators stay green regardless of the customer's brand. - Reuses MUI's `alpha` / `darken` helpers already used across the client; no divergent color utilities. - Small, incremental adoption: themes opt in at their own pace by reading `tokens.brand*` where they want the brand accent. --- client/src/Pages/StatusPage/Status/index.tsx | 1 + .../Status/themes/StatusPageThemeProvider.tsx | 38 ++++++++++++++++++- .../Status/themes/refined/styles.ts | 16 ++++---- .../Pages/StatusPage/Status/themes/tokens.ts | 8 ++++ 4 files changed, 54 insertions(+), 9 deletions(-) diff --git a/client/src/Pages/StatusPage/Status/index.tsx b/client/src/Pages/StatusPage/Status/index.tsx index 4fbf6179a..3c0408628 100644 --- a/client/src/Pages/StatusPage/Status/index.tsx +++ b/client/src/Pages/StatusPage/Status/index.tsx @@ -142,6 +142,7 @@ const StatusPageView = () => { theme={statusPage.theme} themeMode={statusPage.themeMode} timezone={statusPage.timezone} + brandColor={statusPage.color} paintBody > {customCss && } diff --git a/client/src/Pages/StatusPage/Status/themes/StatusPageThemeProvider.tsx b/client/src/Pages/StatusPage/Status/themes/StatusPageThemeProvider.tsx index f79991c3b..4d50dd3e6 100644 --- a/client/src/Pages/StatusPage/Status/themes/StatusPageThemeProvider.tsx +++ b/client/src/Pages/StatusPage/Status/themes/StatusPageThemeProvider.tsx @@ -6,6 +6,7 @@ import { useState, type ReactNode, } from "react"; +import { alpha, darken } from "@mui/material/styles"; import { DEFAULT_STATUS_PAGE_THEME, resolveStatusPageTheme, @@ -36,6 +37,15 @@ interface Props { theme?: StatusPageTheme; themeMode?: StatusPageThemeMode; timezone?: string; + /** + * Brand color picked in the status-page appearance settings. When + * provided, it feeds `tokens.brand` / `brandStrong` / `brandSoft`, + * which theme styles use for accent surfaces (hero, active tabs, + * links). It does NOT recolor the semantic `up`/OK status color, so + * a red or orange brand cannot make the "all operational" indicator + * signal downtime. + */ + brandColor?: string; /** * When true, paint the document body with the theme background so it * covers beyond the provider's wrapper (useful on the public route @@ -54,10 +64,32 @@ interface Props { children: ReactNode; } +const HEX_COLOR = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i; + +// Resolve the brand triple that theme styles read from. A customer's +// picked color (when valid) overrides the theme's default; otherwise +// the theme's own `brand*` values win, and finally we fall back to the +// `up*` triple so unmigrated theme styles have something to consume. +const resolveBrandTokens = ( + tokens: StatusPageThemeTokens, + brandColor: string | undefined +): StatusPageThemeTokens => { + 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; From 523bf2253c0fac3cddf6cfb5454c59989fcac99f Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Mon, 13 Jul 2026 16:10:04 +0300 Subject: [PATCH 20/84] feat(i18n): add Italian translation, document translation workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Changes - Add `client/src/locales/it.json` — Italian translation of all 874 UI keys, structure mirrors `en.json` exactly. Placeholders (`{{value}}`, etc.) and inline tag tokens (``, ``, ...) are preserved. Product/brand names, technical abbreviations, and placeholder-style examples (e.g. `you@example.com`) are kept in English. 116 leaves are intentionally kept verbatim in English (brand names, protocol names, sample values, port numbers, regex). - Register `it: "Italiano"` in `LanguageSelector`'s `languageNames` map so the dropdown shows the localised name; the selector already picks up any locale present in the i18n resources. - Fill in the two "How do I..." sections that CONTRIBUTING.md's table of contents already links to but did not have bodies: - "Improve the documentation?" — short guidance on doc PRs and keeping README translations in sync. - "Help with translations?" — describes the current per-language JSON workflow (source of truth: `en.json`; improve an existing language by editing its JSON; add a new one by adding a JSON file and registering the display name in `LanguageSelector`). Notes that the older PoEditor sync workflow is no longer authoritative. ## Benefits - Italian speakers can now use Checkmate in Italian. - Contributors who follow the "Help with translations?" link from the CONTRIBUTING TOC land on real, current guidance instead of a broken anchor. - Translation quality can be improved incrementally by anyone editing `it.json` directly; no external tooling required. --- CONTRIBUTING.md | 29 + .../Components/inputs/LanguageSelector.tsx | 1 + client/src/locales/it.json | 1662 +++++++++++++++++ 3 files changed, 1692 insertions(+) create mode 100644 client/src/locales/it.json diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c2e536da5..d19bd83be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -256,6 +256,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/client/src/Components/inputs/LanguageSelector.tsx b/client/src/Components/inputs/LanguageSelector.tsx index a83f3e0fa..116f45069 100644 --- a/client/src/Components/inputs/LanguageSelector.tsx +++ b/client/src/Components/inputs/LanguageSelector.tsx @@ -13,6 +13,7 @@ export const languageNames: Record = { ca: "Català", es: "Español", fr: "Français", + it: "Italiano", "pt-BR": "Português (BR)", cs: "Čeština", fi: "Suomi", diff --git a/client/src/locales/it.json b/client/src/locales/it.json new file mode 100644 index 000000000..4620284f7 --- /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": "Test", + "testNotifications": "Testa notifiche", + "toggleTheme": "Cambia 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 eliminare questo elemento?" + } + }, + "labels": { + "active": "Attivo", + "running": "In esecuzione", + "paused": "Sospeso", + "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": "Nessun elemento 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 ad esso." + }, + "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 ancora", + "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, 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": "Ancora nessuna verifica", + "description": "Una volta creato un monitor, ogni controllo 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 non attiva in questo intervallo di tempo", + "headers": { + "statusCode": "Codice di stato", + "location": "Posizione" + } + }, + "header": { + "title": "Verifiche", + "description": "Ogni controllo che Checkmate esegue sui tuoi monitor appare 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à sospeso", + "alreadyPaused_other": "Tutti i monitor selezionati sono già sospesi", + "alreadyRunning_one": "Il monitor selezionato è già in esecuzione", + "alreadyRunning_other": "Tutti i monitor selezionati sono già in esecuzione", + "paused_one": "{{count}} monitor sospeso", + "paused_other": "{{count}} monitor sospesi", + "resumed_one": "{{count}} monitor ripreso", + "resumed_other": "{{count}} monitor ripresi", + "partialFailure": "{{successCount}} monitor aggiornati, ma {{failedCount}} non sono stati sincronizzati con la coda dei job" + }, + "statBoxes": { + "activeFor": "Attivo da", + "certificateExpiry": "Scadenza certificato", + "lastCheck": "Ultima verifica", + "lastResponseTime": "Ultimo tempo di risposta", + "serviceIsDown": "Il servizio è non attivo" + }, + "status": { + "down": "non attivo", + "breached": "soglia superata", + "initializing": "inizializzazione", + "maintenance": "manutenzione", + "paused": "sospeso", + "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 body. Il matching avanzato è completamente supportato", + "HEAD": "HEAD recupera solo gli header della risposta per ridurre il carico sul target. Supporta il matching del codice di stato ma non il matching avanzato poiché non c'è un body." + } + }, + "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 — controlla la documentazione del tuo game server. 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": "Viene usata una finestra scorrevole per determinare quando un monitor va non attivo. Lo stato di un monitor cambierà 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 sono in salute", + "description": "Quando un monitor va non attivo o si riprende, l'incidente apparirà qui con tempistiche, dettagli del monitor e cronologia di 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 non attivi o si riprendono, gli incidenti appaiono qui con tempistiche, dettagli del monitor e cronologia di 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 infrastrutturale ancora" + }, + "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": "Ancora 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": "Ancora 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 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 in errore", + "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 prontezza", + "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": "% di memoria disponibile", + "allocatedPercentage": "% allocata", + "usedSPercentage": "% di 1s utilizzato dalla CPU", + "total": "Totale", + "used": "Utilizzato" + } + }, + "table": { + "headers": { + "host": "Host", + "processId": "ID processo", + "workerId": "ID worker", + "workerMode": "Modalità worker", + "lastSeenAt": "Ultima visto il", + "state": "Stato", + "timestamp": "Timestamp", + "level": "Livello", + "service": "Servizio", + "method": "Metodo", + "monitorId": "ID monitor", + "runCount": "Conteggio esecuzioni", + "failCount": "Conteggio", + "nextScheduledAt": "Prossima esecuzione il", + "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 ancora" + }, + "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": "Account SID", + "placeholderAccountSid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "optionAuthToken": "Auth token", + "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 ancora" + }, + "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 dalla modalità chiara a quella scura, cambia lingua o personalizza il tipo di visualizzazione dei grafici.", + "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": "Non può essere annullato" + } + }, + "retention": { + "title": "Conservazione verifiche", + "description": "Imposta per quanto tempo i dati delle verifiche vengono conservati prima di essere puliti 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. Queste 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": "Questo costruisce un trasporto SMTP per NodeMailer", + "descriptionTransportLink": "Vedi 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 usando SSL/TLS" + }, + "pool": { + "label": "Abilita 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 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": "Reset del sistema", + "description": "Rimuovi tutti i monitor dal tuo sistema.", + "dialog": { + "title": "Rimuovere tutti i monitor?", + "description": "Questa operazione non può essere annullata.", + "paragraph": "Anche tutta la cronologia delle verifiche, gli incidenti, le finestre di manutenzione pianificate e qualsiasi pagina di stato che faccia riferimento a questi monitor verranno rimossi. Esporta prima i tuoi monitor per mantenere 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 con successo", + "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": "Sospeso", + "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 sospesi", + "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 sospesi", + "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": "Report 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 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": "Bold", + "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 avvisi nel momento in cui qualcosa va non attivo.", + "title": "Nessun monitor uptime ancora" + }, + "header": { + "title": "Monitor uptime", + "description": "Controlla endpoint HTTP, ping, container e porte — e ricevi avvisi nel momento in cui qualcosa va non attivo." + } + } + } +} From 242e853bf00d12909740220fdb378d40c8b294c5 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Mon, 13 Jul 2026 16:18:24 +0300 Subject: [PATCH 21/84] =?UTF-8?q?i18n(it):=20review=20pass=20=E2=80=94=20f?= =?UTF-8?q?ix=20grammar,=20awkward=20phrasing,=20inconsistencies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 91 leaves refined after a native-Italian style pass. ## Categories of fixes - Grammar / gender agreement — e.g. `logs.table.headers.lastSeenAt` "Ultima visto il" → "Visto l'ultima volta"; heap gauge subject agreement in `logs.diagnostics.gauges.allocatedPercentage`. - Awkward phrasing / calques — `notFound.subtitle` drops stilted "ad esso"; `incidents.fallback.title` "in salute" → "funzionano correttamente" (was reading as if for a person, not systems); `settings.form.removeMonitors.title` "Reset del sistema" → "Reimposta il sistema"; "con successo" → "correttamente" on status-page delete confirmations. - Consistency — paused monitor status unified on "In pausa" across `common.labels.paused`, monitor status labels, status-bar keys, and the `bulkPause.*_one/other` copies. Button verbs "Sospendi"/"Riprendi" kept for brevity; status noun stays "In pausa". - Mistranslations — `buttons.testNotifications` "Testa" → "Prova"; `buttons.toggleTheme` "Cambia" → "Alterna"; Twilio "Auth token" now translated; Mongoose `readyState` clarified to "Stato di connessione". - Register — trimmed "Ancora" from several empty-state titles so they read like natural Italian empty states. ## Verification 874 leaves in EN and IT, 0 missing / 0 extra, all placeholders and inline tags preserved, no empty values. --- client/src/locales/it.json | 142 ++++++++++++++++++------------------- 1 file changed, 71 insertions(+), 71 deletions(-) diff --git a/client/src/locales/it.json b/client/src/locales/it.json index 4620284f7..cabeb94ef 100644 --- a/client/src/locales/it.json +++ b/client/src/locales/it.json @@ -46,9 +46,9 @@ "next": "Avanti", "back": "Indietro", "sendInvite": "Invia invito", - "test": "Test", - "testNotifications": "Testa notifiche", - "toggleTheme": "Cambia tema chiaro e scuro", + "test": "Prova", + "testNotifications": "Prova notifiche", + "toggleTheme": "Alterna tema chiaro e scuro", "flushQueue": "Svuota coda", "notFound": "Vai alla dashboard principale", "resetPassword": "Reimposta password", @@ -78,13 +78,13 @@ "dialogs": { "delete": { "description": "Questa azione non può essere annullata.", - "title": "Sei sicuro di voler eliminare questo elemento?" + "title": "Sei sicuro di voler eliminarlo?" } }, "labels": { "active": "Attivo", "running": "In esecuzione", - "paused": "Sospeso", + "paused": "In pausa", "idle": "Inattivo", "na": "N/A", "resolved": "Risolto", @@ -120,7 +120,7 @@ } }, "table": { - "empty": "Nessun elemento da mostrare", + "empty": "Non c'è ancora nulla da mostrare", "headers": { "actions": "Azioni", "dateTime": "Data e ora", @@ -229,7 +229,7 @@ "pages": { "notFound": { "title": "Oh no! Hai fatto cadere il tuo sushi!", - "subtitle": "L'URL non esiste, oppure non hai accesso ad esso." + "subtitle": "L'URL non esiste, oppure non hai accesso." }, "account": { "tabs": { @@ -275,7 +275,7 @@ }, "team": { "fallback": { - "title": "Nessun membro del team ancora", + "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": { @@ -318,7 +318,7 @@ "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." + "description": "Uptime in tempo reale, tempo di risposta e tracciamento degli incidenti — self-hosted e tuo per sempre." }, "common": { "passwordRules": { @@ -381,7 +381,7 @@ "register": { "title": "Bentornato su Checkmate!", "subtitle": "Registrati per iniziare", - "setupNotice": "Questa pagina è disponibile solo durante la configurazione iniziale. Una volta registrato un super amministratore, altri utenti possono essere invitati dalle impostazioni del team.", + "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": { @@ -402,8 +402,8 @@ }, "checks": { "fallback": { - "title": "Ancora nessuna verifica", - "description": "Una volta creato un monitor, ogni controllo di stato e tempo di risposta apparirà qui come una verifica." + "title": "Nessuna verifica", + "description": "Una volta creato un monitor, ogni sonda di stato e tempo di risposta apparirà qui come una verifica." }, "selects": { "monitor": { @@ -416,7 +416,7 @@ } }, "table": { - "empty": "Nessuna verifica non attiva in questo intervallo di tempo", + "empty": "Nessuna verifica fallita in questo intervallo di tempo", "headers": { "statusCode": "Codice di stato", "location": "Posizione" @@ -424,7 +424,7 @@ }, "header": { "title": "Verifiche", - "description": "Ogni controllo che Checkmate esegue sui tuoi monitor appare qui, con codice di stato e tempo di risposta." + "description": "Ogni sonda che Checkmate esegue sui tuoi monitor arriva qui, con codice di stato e tempo di risposta." } }, "common": { @@ -445,29 +445,29 @@ "selectMonitor": "Seleziona {{name}}" }, "bulkPause": { - "alreadyPaused_one": "Il monitor selezionato è già sospeso", - "alreadyPaused_other": "Tutti i monitor selezionati sono già sospesi", + "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 sospeso", - "paused_other": "{{count}} monitor sospesi", + "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 stati sincronizzati con la coda dei job" + "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": "Il servizio è non attivo" + "serviceIsDown": "Servizio non attivo" }, "status": { "down": "non attivo", "breached": "soglia superata", "initializing": "inizializzazione", "maintenance": "manutenzione", - "paused": "sospeso", + "paused": "in pausa", "total": "totale", "up": "attivo" }, @@ -493,8 +493,8 @@ "method": { "label": "Metodo di richiesta", "description": { - "GET": "GET recupera la risposta completa incluso il body. Il matching avanzato è completamente supportato", - "HEAD": "HEAD recupera solo gli header della risposta per ridurre il carico sul target. Supporta il matching del codice di stato ma non il matching avanzato poiché non c'è un body." + "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": { @@ -611,7 +611,7 @@ "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 — controlla la documentazione del tuo game server. Consulta la lista dei giochi supportati per i dettagli del protocollo.", + "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.", @@ -629,7 +629,7 @@ "title": "Impostazioni TLS/SSL" }, "incidents": { - "description": "Viene usata una finestra scorrevole per determinare quando un monitor va non attivo. Lo stato di un monitor cambierà solo quando la percentuale di verifiche nella finestra scorrevole raggiunge il valore specificato.", + "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" @@ -747,8 +747,8 @@ }, "incidents": { "fallback": { - "title": "Nessun incidente — tutti i sistemi sono in salute", - "description": "Quando un monitor va non attivo o si riprende, l'incidente apparirà qui con tempistiche, dettagli del monitor e cronologia di risoluzione." + "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": { @@ -833,7 +833,7 @@ }, "header": { "title": "Incidenti", - "description": "Quando i monitor vanno non attivi o si riprendono, gli incidenti appaiono qui con tempistiche, dettagli del monitor e cronologia di risoluzione." + "description": "Quando i monitor vanno giù o si ripristinano, gli incidenti appaiono qui con tempistiche, dettagli del monitor e cronologia della risoluzione." } }, "infrastructure": { @@ -890,7 +890,7 @@ "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 infrastrutturale ancora" + "title": "Nessun monitor infrastruttura" }, "header": { "title": "Monitor infrastruttura", @@ -903,7 +903,7 @@ "description": "Organizza i tuoi monitor con i tag" }, "fallback": { - "title": "Ancora nessun tag", + "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" }, @@ -930,7 +930,7 @@ }, "logs": { "fallback": { - "title": "Ancora nessun log", + "title": "Nessun log", "description": "Attività del server, avvisi ed errori appariranno qui man mano che Checkmate viene eseguito." }, "queueFallback": { @@ -946,7 +946,7 @@ "label": "Livello di log" }, "workers": "Worker", - "workersExplainers": "Mostra i processi worker. Gli ID worker sono troncati, passa il mouse sopra per l'ID completo", + "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", @@ -955,7 +955,7 @@ "metrics": { "jobs": "Job", "activeJobs": "Job attivi", - "failingJobs": "Job in errore", + "failingJobs": "Job non riusciti", "totalRuns": "Esecuzioni totali", "totalFailures": "Errori totali" }, @@ -986,7 +986,7 @@ "osMemoryLimit": "Limite memoria OS" }, "mongoDBStats": { - "readyState": "Stato di prontezza", + "readyState": "Stato di connessione", "readsPerSecond": "Letture/s", "insertsPerSecond": "Inserimenti/s", "updatesPerSecond": "Aggiornamenti/s", @@ -1010,9 +1010,9 @@ "heapUsage": "Utilizzo heap", "heapUtilization": "Utilizzo heap", "instantCpuUsage": "Utilizzo CPU istantaneo", - "availableMemoryPercentage": "% di memoria disponibile", - "allocatedPercentage": "% allocata", - "usedSPercentage": "% di 1s utilizzato dalla CPU", + "availableMemoryPercentage": "% memoria disponibile", + "allocatedPercentage": "% allocato", + "usedSPercentage": "% di 1s usato dalla CPU", "total": "Totale", "used": "Utilizzato" } @@ -1023,16 +1023,16 @@ "processId": "ID processo", "workerId": "ID worker", "workerMode": "Modalità worker", - "lastSeenAt": "Ultima visto il", + "lastSeenAt": "Visto l'ultima volta", "state": "Stato", "timestamp": "Timestamp", "level": "Livello", "service": "Servizio", "method": "Metodo", "monitorId": "ID monitor", - "runCount": "Conteggio esecuzioni", - "failCount": "Conteggio", - "nextScheduledAt": "Prossima esecuzione il", + "runCount": "Esecuzioni", + "failCount": "Errori", + "nextScheduledAt": "Prossima esecuzione", "lockedBy": "Bloccato da", "lastFinishedAt": "Ultimo completamento", "lastFailedAt": "Ultimo errore", @@ -1110,7 +1110,7 @@ "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 ancora" + "title": "Nessun canale di notifica" }, "form": { "accessToken": { @@ -1172,9 +1172,9 @@ "twilio": { "title": "Configurazione SMS Twilio", "description": "Configura Twilio per inviare notifiche SMS a un numero di telefono.", - "optionAccountSid": "Account SID", + "optionAccountSid": "SID account", "placeholderAccountSid": "ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", - "optionAuthToken": "Auth token", + "optionAuthToken": "Token di autenticazione", "placeholderAuthToken": "your_auth_token", "optionFromNumber": "Numero mittente (Twilio)", "placeholderFromNumber": "+15551234567", @@ -1229,7 +1229,7 @@ "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 ancora" + "title": "Nessun monitor PageSpeed" }, "header": { "title": "Monitor PageSpeed", @@ -1249,7 +1249,7 @@ }, "ui": { "title": "Aspetto", - "description": "Passa dalla modalità chiara a quella scura, cambia lingua o personalizza il tipo di visualizzazione dei grafici.", + "description": "Passa tra modalità chiara e scura, cambia lingua o personalizza il tipo di grafico.", "option": { "theme": { "label": "Modalità tema", @@ -1298,12 +1298,12 @@ }, "dialog": { "title": "Cancellare tutta la cronologia di monitoraggio?", - "description": "Non può essere annullato" + "description": "Questa azione non può essere annullata." } }, "retention": { - "title": "Conservazione verifiche", - "description": "Imposta per quanto tempo i dati delle verifiche vengono conservati prima di essere puliti automaticamente.", + "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)", @@ -1313,7 +1313,7 @@ }, "thresholds": { "title": "Soglie globali", - "description": "Imposta soglie globali di CPU, memoria, disco e temperatura per il monitoraggio infrastrutturale. Queste si applicano a tutti i monitor hardware a meno che non vengano sovrascritte.", + "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 (%)", @@ -1336,8 +1336,8 @@ "email": { "title": "Impostazioni email", "description": "Configura le impostazioni email per il tuo sistema. Vengono utilizzate per inviare notifiche e avvisi.", - "descriptionTransport": "Questo costruisce un trasporto SMTP per NodeMailer", - "descriptionTransportLink": "Vedi le specifiche qui", + "descriptionTransport": "Costruisce un trasporto SMTP per NodeMailer", + "descriptionTransportLink": "Consulta le specifiche qui", "option": { "host": { "label": "Host email - Hostname o indirizzo IP a cui connettersi", @@ -1373,19 +1373,19 @@ "placeholder": "localhost" }, "secure": { - "label": "Usa SSL (consigliato): Cripta la connessione usando SSL/TLS" + "label": "Usa SSL (consigliato): cripta la connessione con SSL/TLS" }, "pool": { - "label": "Abilita pooling delle connessioni: Riutilizza le connessioni esistenti per migliorare le prestazioni" + "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" + "label": "Disabilita STARTTLS: non usare TLS anche se il server lo supporta" }, "requireTLS": { - "label": "Forza STARTTLS: Richiedi l'aggiornamento TLS, fallisce se non supportato" + "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" + "label": "Rifiuta certificati non validi: rifiuta connessioni con certificati auto-firmati o non fidati" } } }, @@ -1394,12 +1394,12 @@ "description": "Aggiungi monitor di esempio a scopo dimostrativo." }, "removeMonitors": { - "title": "Reset del sistema", + "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": "Anche tutta la cronologia delle verifiche, gli incidenti, le finestre di manutenzione pianificate e qualsiasi pagina di stato che faccia riferimento a questi monitor verranno rimossi. Esporta prima i tuoi monitor per mantenere un backup dei loro dati." + "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": { @@ -1423,7 +1423,7 @@ } }, "statusPages": { - "deleteSuccess": "Pagina di stato eliminata con successo", + "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.", @@ -1438,7 +1438,7 @@ "down": "Non attivo", "breached": "Soglia superata", "maintenance": "Manutenzione", - "paused": "Sospeso", + "paused": "In pausa", "initializing": "Inizializzazione" }, "chart": { @@ -1463,20 +1463,20 @@ } }, "statusBar": { - "monitoringSummary": "Monitoraggio di {{count}} servizio · Aggiornamenti ogni 10 secondi", - "monitoringSummary_other": "Monitoraggio di {{count}} servizi · Aggiornamenti ogni 10 secondi", + "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 sospesi", + "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 sospesi", + "partiallyPaused": "Alcuni sistemi sono in pausa", "initializing": "Alcuni sistemi sono in inizializzazione", "noMonitors": "Nessun monitor è stato configurato", "unknown": "Impossibile determinare lo stato del sistema" @@ -1500,7 +1500,7 @@ "poweredBy": "Powered by" }, "editorial": { - "reportTitle": "Report di servizio", + "reportTitle": "Rapporto di servizio", "allUp": "Tutti i sistemi funzionano normalmente." }, "form": { @@ -1528,7 +1528,7 @@ "customDomain": { "label": "Dominio personalizzato", "placeholder": "status.example.com", - "helper": "Opzionale. Punta un record DNS a questa istanza Checkmate per servire la pagina di stato sul tuo dominio." + "helper": "Opzionale. Punta un record DNS a questa istanza di Checkmate per servire la pagina di stato sul tuo dominio." } } }, @@ -1579,7 +1579,7 @@ "description": "Livello SaaS con un punto di stato pulsante e schede in dissolvenza morbida." }, "bold": { - "name": "Bold", + "name": "Deciso", "description": "Caratteri grandi e spaziatura decisa. Ottimo per pagine focalizzate sul brand." }, "editorial": { @@ -1650,12 +1650,12 @@ }, "fallback": { "actionButton": "Crea un monitor", - "description": "Controlla endpoint HTTP, ping, container e porte — e ricevi avvisi nel momento in cui qualcosa va non attivo.", - "title": "Nessun monitor uptime ancora" + "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 avvisi nel momento in cui qualcosa va non attivo." + "description": "Controlla endpoint HTTP, ping, container e porte — e ricevi un avviso nel momento in cui qualcosa va giù." } } } From 5d55750b7b0381abeb16732e1671b06823dd17eb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 13 Jul 2026 09:38:55 -0700 Subject: [PATCH 22/84] update docs --- README.md | 21 +++++++++++++++++++++ docs/hosting/checkmate-so/README.md | 4 +++- 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 05eb688ff..2841149ca 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,27 @@ 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/docs/hosting/checkmate-so/README.md b/docs/hosting/checkmate-so/README.md index 2a7e9f648..0c06f6948 100644 --- a/docs/hosting/checkmate-so/README.md +++ b/docs/hosting/checkmate-so/README.md @@ -29,7 +29,9 @@ 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. + 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. From c1bcd9b206364660c75b43aa11058d9bfb7ec73d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 13 Jul 2026 09:47:45 -0700 Subject: [PATCH 23/84] add note to helm chart about migration --- charts/helm/checkmate/Chart.yaml | 4 ++++ 1 file changed, 4 insertions(+) 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) From 800080fd0633c28a352dfd16f75204e6c509cd9d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 13 Jul 2026 09:48:02 -0700 Subject: [PATCH 24/84] add log warnings about legacy env vars --- server/src/config/envValidation.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/src/config/envValidation.ts b/server/src/config/envValidation.ts index 0bfaa01ce..a769a28c7 100644 --- a/server/src/config/envValidation.ts +++ b/server/src/config/envValidation.ts @@ -69,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", From 9f79feec8ecdb5d4b67f61b4dd9aa17a6295baa5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 13 Jul 2026 10:10:28 -0700 Subject: [PATCH 25/84] add img.shields.io to csp directive --- server/src/app.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/app.ts b/server/src/app.ts index f195d8140..12e552406 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -76,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'"], }, From fd4edb72278c8d68bbb32c8b956f773828b7e1d0 Mon Sep 17 00:00:00 2001 From: edobusy Date: Tue, 14 Jul 2026 13:43:37 +1000 Subject: [PATCH 26/84] fix(docs): correct K8s install link path in translated READMEs The translated READMEs in docs/translations/ link to the Kubernetes installation guide with a path copied from the root README (./charts/helm/checkmate/INSTALLATION.md). Because these files live two levels below the repo root, that path resolves to docs/translations/charts/..., which does not exist, so the link is broken in all 17 translations. Fix the path to ../../charts/helm/checkmate/INSTALLATION.md so it resolves to the repo-root charts/ directory. The root README.md is correct as-is and is left unchanged. --- docs/translations/README.ar.md | 2 +- docs/translations/README.ca.md | 2 +- docs/translations/README.cs.md | 2 +- docs/translations/README.de.md | 2 +- docs/translations/README.es.md | 2 +- docs/translations/README.fi.md | 2 +- docs/translations/README.fr.md | 2 +- docs/translations/README.it.md | 2 +- docs/translations/README.ja.md | 2 +- docs/translations/README.pt-BR.md | 2 +- docs/translations/README.ru.md | 2 +- docs/translations/README.th.md | 2 +- docs/translations/README.tr.md | 2 +- docs/translations/README.uk.md | 2 +- docs/translations/README.vi.md | 2 +- docs/translations/README.zh-CN.md | 2 +- docs/translations/README.zh-TW.md | 2 +- 17 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/translations/README.ar.md b/docs/translations/README.ar.md index 4997fc381..8ff16e140 100644 --- a/docs/translations/README.ar.md +++ b/docs/translations/README.ar.md @@ -87,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 3552d6c4c..29e7e4107 100644 --- a/docs/translations/README.ca.md +++ b/docs/translations/README.ca.md @@ -85,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 13dd5d8b8..d662f0b70 100644 --- a/docs/translations/README.cs.md +++ b/docs/translations/README.cs.md @@ -85,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 21f72165e..faee4e44c 100644 --- a/docs/translations/README.de.md +++ b/docs/translations/README.de.md @@ -82,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 cbd8f4258..5dc04e269 100644 --- a/docs/translations/README.es.md +++ b/docs/translations/README.es.md @@ -82,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 cbfcb76cb..877130120 100644 --- a/docs/translations/README.fi.md +++ b/docs/translations/README.fi.md @@ -85,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 209fc3dcc..e78a6cf3c 100644 --- a/docs/translations/README.fr.md +++ b/docs/translations/README.fr.md @@ -82,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 index f97cb8420..1f03bd682 100644 --- a/docs/translations/README.it.md +++ b/docs/translations/README.it.md @@ -82,7 +82,7 @@ Le istruzioni per l'uso sono disponibili [qui](https://checkmate.so/docs). 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. +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 diff --git a/docs/translations/README.ja.md b/docs/translations/README.ja.md index 6534a3e3a..bbb446a51 100644 --- a/docs/translations/README.ja.md +++ b/docs/translations/README.ja.md @@ -82,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 6ddbe8367..ee35ca4f7 100644 --- a/docs/translations/README.pt-BR.md +++ b/docs/translations/README.pt-BR.md @@ -82,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 1e22d0c9c..d9b5dcb1c 100644 --- a/docs/translations/README.ru.md +++ b/docs/translations/README.ru.md @@ -82,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 6e02f662e..61b4953ca 100644 --- a/docs/translations/README.th.md +++ b/docs/translations/README.th.md @@ -85,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 1e0b58416..7e41aec43 100644 --- a/docs/translations/README.tr.md +++ b/docs/translations/README.tr.md @@ -82,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 798cfd271..7322afac7 100644 --- a/docs/translations/README.uk.md +++ b/docs/translations/README.uk.md @@ -85,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 f800c77a6..36bbac7ca 100644 --- a/docs/translations/README.vi.md +++ b/docs/translations/README.vi.md @@ -85,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 eb1e3c6f6..c3916509a 100644 --- a/docs/translations/README.zh-CN.md +++ b/docs/translations/README.zh-CN.md @@ -82,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 c79944190..cbf57b9fe 100644 --- a/docs/translations/README.zh-TW.md +++ b/docs/translations/README.zh-TW.md @@ -82,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 From 107b5cbd9672cbe01a66d23b2ed33542acb74d41 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 14 Jul 2026 09:24:14 -0700 Subject: [PATCH 27/84] delete incidents on all monitors deleted --- server/src/domain/monitors/monitor.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/domain/monitors/monitor.service.ts b/server/src/domain/monitors/monitor.service.ts index 455a1daff..1402cc453 100644 --- a/server/src/domain/monitors/monitor.service.ts +++ b/server/src/domain/monitors/monitor.service.ts @@ -555,6 +555,7 @@ export class MonitorService implements IMonitorService { await this.geoChecksRepository.deleteByMonitorId(monitor.id); await this.statusPagesRepository.removeMonitorFromStatusPages(monitor.id); await this.monitorStatsRepository.deleteByMonitorId(monitor.id); + await this.incidentsRepository.deleteByMonitorId(monitor.id, teamId); } catch (error: unknown) { this.logger.warn({ message: `Error deleting associated records for monitor ${monitor.id} with name ${monitor.name}`, From b088a8c878bee3f970af371811399549bf14bd68 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 14 Jul 2026 10:17:38 -0700 Subject: [PATCH 28/84] remove toEntityWithChecks --- .../monitors/monitor.repository.mongo.ts | 70 +------------------ 1 file changed, 2 insertions(+), 68 deletions(-) diff --git a/server/src/domain/monitors/monitor.repository.mongo.ts b/server/src/domain/monitors/monitor.repository.mongo.ts index cba542c96..6db51fb38 100644 --- a/server/src/domain/monitors/monitor.repository.mongo.ts +++ b/server/src/domain/monitors/monitor.repository.mongo.ts @@ -249,7 +249,7 @@ 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 => { @@ -525,6 +525,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { }; const toDateString = (value: Date | string): string => { + if (!value) return ""; return value instanceof Date ? value.toISOString() : value; }; @@ -581,73 +582,6 @@ 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; From 70f10a73d8669b17c82e5a28f8ef83c8d07300c8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 14 Jul 2026 10:46:25 -0700 Subject: [PATCH 29/84] remove duplicated code --- .../monitors/monitor.repository.mongo.ts | 127 ++++-------------- 1 file changed, 25 insertions(+), 102 deletions(-) diff --git a/server/src/domain/monitors/monitor.repository.mongo.ts b/server/src/domain/monitors/monitor.repository.mongo.ts index 6db51fb38..e7ccf56e9 100644 --- a/server/src/domain/monitors/monitor.repository.mongo.ts +++ b/server/src/domain/monitors/monitor.repository.mongo.ts @@ -48,19 +48,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,48 +81,28 @@ class MongoMonitorsRepository implements IMonitorsRepository { break; } } + return query; + }; + private uptimeStatsLookupStages: PipelineStage[] = [ + { $lookup: { from: "monitorstats", localField: "_id", foreignField: "monitorId", as: "stats" } }, + { $addFields: { uptimePercentage: { $arrayElemAt: ["$stats.uptimePercentage", 0] } } }, + { $project: { stats: 0 } }, + ]; + + findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise => { + const { page = 0, rowsPerPage = 0, field = "createdAt", order = "desc" } = config ?? {}; + const query = this.queryBuilder(config, teamId); const sort = { [field]: order === "asc" ? 1 : -1 } as const; const skip = Math.max(page, 0) * rowsPerPage; - const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(rowsPerPage); - 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 { page = 0, rowsPerPage = 0, field = "createdAt", order = "desc" } = 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 query = this.queryBuilder(config, teamId); const sort = { [field]: order === "asc" ? 1 : -1 } as const; const skip = Math.max(page, 0) * rowsPerPage; @@ -131,24 +111,7 @@ class MongoMonitorsRepository implements IMonitorsRepository { { $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, - }, - }, + ...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)); @@ -252,21 +195,8 @@ class MongoMonitorsRepository implements IMonitorsRepository { 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 }, { From 0810fcbb889489365034f6a98327fe4f8d90ebda Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 15 Jul 2026 09:18:35 -0700 Subject: [PATCH 30/84] refactor hardware aggregation pipeline --- .../checks/check.hardware.aggregation.ts | 137 ++++++++++++++ .../domain/checks/check.repository.mongo.ts | 171 +----------------- 2 files changed, 142 insertions(+), 166 deletions(-) create mode 100644 server/src/domain/checks/check.hardware.aggregation.ts diff --git a/server/src/domain/checks/check.hardware.aggregation.ts b/server/src/domain/checks/check.hardware.aggregation.ts new file mode 100644 index 000000000..176817315 --- /dev/null +++ b/server/src/domain/checks/check.hardware.aggregation.ts @@ -0,0 +1,137 @@ +import { CheckModel } from "@/domain/checks/check.model.js"; +import mongoose from "mongoose"; + +type DateRange = { start: Date; end: Date }; +type HardwareUpChecks = { totalChecks: number }; + +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) => { + 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.repository.mongo.ts b/server/src/domain/checks/check.repository.mongo.ts index 4f9ad6f4c..bfa8aa467 100644 --- a/server/src/domain/checks/check.repository.mongo.ts +++ b/server/src/domain/checks/check.repository.mongo.ts @@ -18,11 +18,11 @@ import mongoose from "mongoose"; import { getDateForRange } from "@/utils/dataUtils.js"; import { ILogger } from "@/utils/logger.js"; +import { getHardwareUpChecks, getHardwareStats, getHardwareTotalChecks } from "@/domain/checks/check.hardware.aggregation.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; @@ -540,9 +540,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 = { @@ -635,167 +635,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; From 51203909233c69e78e0a4eed7082d83c1368ce7f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 15 Jul 2026 09:23:16 -0700 Subject: [PATCH 31/84] aggregation->aggregations --- ...k.hardware.aggregation.ts => check.hardware.aggregations.ts} | 0 server/src/domain/checks/check.repository.mongo.ts | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename server/src/domain/checks/{check.hardware.aggregation.ts => check.hardware.aggregations.ts} (100%) diff --git a/server/src/domain/checks/check.hardware.aggregation.ts b/server/src/domain/checks/check.hardware.aggregations.ts similarity index 100% rename from server/src/domain/checks/check.hardware.aggregation.ts rename to server/src/domain/checks/check.hardware.aggregations.ts diff --git a/server/src/domain/checks/check.repository.mongo.ts b/server/src/domain/checks/check.repository.mongo.ts index bfa8aa467..3e8d1c52a 100644 --- a/server/src/domain/checks/check.repository.mongo.ts +++ b/server/src/domain/checks/check.repository.mongo.ts @@ -18,7 +18,7 @@ import mongoose from "mongoose"; import { getDateForRange } from "@/utils/dataUtils.js"; import { ILogger } from "@/utils/logger.js"; -import { getHardwareUpChecks, getHardwareStats, getHardwareTotalChecks } from "@/domain/checks/check.hardware.aggregation.js"; +import { getHardwareUpChecks, getHardwareStats, getHardwareTotalChecks } from "@/domain/checks/check.hardware.aggregations.js"; const SERVICE_NAME = "StatusService"; From bd13e9d9c801965a7d5c057cb6f34dfc2c117994 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 15 Jul 2026 10:02:36 -0700 Subject: [PATCH 32/84] add catchAsync to handle errors, remove all try/catch blocks from contorllers --- server/src/api/controllers/authController.ts | 415 ++++----- server/src/api/controllers/checkController.ts | 173 ++-- .../api/controllers/diagnosticController.ts | 25 +- .../src/api/controllers/geoCheckController.ts | 47 +- .../src/api/controllers/incidentController.ts | 147 ++-- .../src/api/controllers/inviteController.ts | 101 +-- server/src/api/controllers/logController.ts | 25 +- .../maintenanceWindowController.ts | 201 ++--- .../src/api/controllers/monitorController.ts | 791 ++++++++---------- .../api/controllers/notificationController.ts | 229 +++-- server/src/api/controllers/queueController.ts | 97 +-- .../src/api/controllers/settingsController.ts | 163 ++-- .../api/controllers/statusPageController.ts | 219 +++-- server/src/api/controllers/tagController.ts | 131 ++- server/src/utils/catchAsync.ts | 14 + 15 files changed, 1243 insertions(+), 1535 deletions(-) create mode 100644 server/src/utils/catchAsync.ts diff --git a/server/src/api/controllers/authController.ts b/server/src/api/controllers/authController.ts index 45f806a8c..1eb992529 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"; @@ -24,20 +25,20 @@ 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 { @@ -53,239 +54,183 @@ class AuthController implements IAuthController { 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..ccc91921d 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, @@ -14,11 +15,11 @@ 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 { @@ -33,109 +34,89 @@ class CheckController implements ICheckController { return CheckController.SERVICE_NAME; } - getChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = getChecksParamValidation.parse(req.params); - const validatedQuery = getChecksQueryValidation.parse(req.query); + getChecksByMonitor = catchAsync(async (req: Request, res: Response) => { + 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, - }); - } catch (error) { - next(error); - } - }; + return res.status(200).json({ + success: true, + msg: "Checks retrieved successfully", + data: result, + }); + }); - getChecksByTeam = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedQuery = getTeamChecksQueryValidation.parse(req.query); - const teamId = requireTeamId(req.user?.teamId); + getChecksByTeam = catchAsync(async (req: Request, res: Response) => { + 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, - }); - } catch (error) { - next(error); - } - }; + 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, + }); + }); - 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"; + getChecksSummaryByTeamId = catchAsync(async (req: Request, res: Response) => { + 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, - }); - } catch (error) { - next(error); - } - }; + const summary = await this.checkService.getChecksSummaryByTeamId({ teamId, dateRange }); + return res.status(200).json({ + success: true, + msg: "Checks summary retrieved successfully", + data: summary, + }); + }); - deleteChecks = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = deleteChecksParamValidation.parse(req.params); - const teamId = requireTeamId(req.user?.teamId); + deleteChecks = catchAsync(async (req: Request, res: Response) => { + 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 }, - }); - } catch (error) { - next(error); - } - }; + return res.status(200).json({ + success: true, + msg: "Checks deleted successfully", + data: { deletedCount }, + }); + }); - deleteChecksByTeamId = async (req: Request, res: Response, next: NextFunction) => { - try { - const teamId = requireTeamId(req.user?.teamId); + deleteChecksByTeamId = catchAsync(async (req: Request, res: Response) => { + 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/diagnosticController.ts b/server/src/api/controllers/diagnosticController.ts index efc383e1a..0864f7237 100755 --- a/server/src/api/controllers/diagnosticController.ts +++ b/server/src/api/controllers/diagnosticController.ts @@ -1,10 +1,11 @@ import { IDiagnosticService } from "@/domain/diagnostics/diagnostic.service.js"; -import { Request, Response, NextFunction } from "express"; +import { Request, Response, RequestHandler } from "express"; +import { catchAsync } from "@/utils/catchAsync.js"; const SERVICE_NAME = "diagnosticController"; export interface IDiagnosticController { - getSystemStats: (req: Request, res: Response, next: NextFunction) => Promise; + getSystemStats: RequestHandler; } class DiagnosticController implements IDiagnosticController { @@ -20,18 +21,14 @@ class DiagnosticController implements IDiagnosticController { 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..81082b6ed 100644 --- a/server/src/api/controllers/geoCheckController.ts +++ b/server/src/api/controllers/geoCheckController.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, getChecksQueryValidation } from "@/api/validation/checkValidation.js"; import type { IGeoChecksService } from "@/domain/geo-checks/geo-check.service.js"; import { requireTeamId } from "./controllerUtils.js"; @@ -6,7 +7,7 @@ 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; @@ -20,32 +21,28 @@ class GeoCheckController implements IGeoCheckController { return GeoCheckController.SERVICE_NAME; } - getGeoChecksByMonitor = async (req: Request, res: Response, next: NextFunction) => { - try { - const validatedParams = getChecksParamValidation.parse(req.params); - const validatedQuery = getChecksQueryValidation.parse(req.query); + getGeoChecksByMonitor = catchAsync(async (req: Request, res: Response) => { + 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..0efc1e0a7 100755 --- a/server/src/api/controllers/inviteController.ts +++ b/server/src/api/controllers/inviteController.ts @@ -1,13 +1,14 @@ -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 { @@ -21,62 +22,50 @@ class InviteController implements IInviteController { return InviteController.SERVICE_NAME; } - 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); - } - }; + 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, + }); + }); - 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); + 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); - 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, - }); - } catch (error) { - next(error); - } - }; + const inviteToken = await this.inviteService.sendInviteEmail({ + invite: inviteRequest, + firstName, + userRoles, + }); + return res.status(200).json({ + success: true, + msg: "Invite issued successfully", + data: inviteToken, + }); + }); - 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..f0449bbf2 100644 --- a/server/src/api/controllers/logController.ts +++ b/server/src/api/controllers/logController.ts @@ -1,10 +1,11 @@ import { ILogger } from "@/utils/logger.js"; -import { Request, Response, NextFunction } from "express"; +import { Request, Response, RequestHandler } from "express"; +import { catchAsync } from "@/utils/catchAsync.js"; const SERVICE_NAME = "LogController"; export interface ILogController { - getLogs: (req: Request, res: Response, next: NextFunction) => Promise; + getLogs: RequestHandler; } class LogController { @@ -17,17 +18,13 @@ class LogController { 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..e0f820a62 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, @@ -25,26 +26,26 @@ import { INotificationsService } from "@/domain/notifications/notification.servi 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; + getGroupsByTeamId: RequestHandler; + updateNotifications: RequestHandler; } class MonitorController implements IMonitorController { static SERVICE_NAME = SERVICE_NAME; @@ -61,430 +62,350 @@ class MonitorController implements IMonitorController { return MonitorController.SERVICE_NAME; } - 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); + 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); - return res.status(200).json({ - success: true, - msg: "SSL certificate retrieved successfully", - data: { - certificateDate: new Date(certificate.validTo), - }, - }); - } catch (error) { - next(error); + return res.status(200).json({ + success: true, + msg: "SSL certificate retrieved successfully", + data: { + certificateDate: new Date(certificate.validTo), + }, + }); + }); + + 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, + }); + }); + + getGroupsByTeamId = catchAsync(async (req: Request, res: Response) => { + const teamId = requireTeamId(req.user?.teamId); + const groups = await this.monitorService.getGroupsByTeamId({ teamId }); + + return res.status(200).json({ + msg: "Groups retrieved successfully", + data: groups, + }); + }); + + 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..72c1463f9 100755 --- a/server/src/api/controllers/queueController.ts +++ b/server/src/api/controllers/queueController.ts @@ -1,14 +1,15 @@ import { IJobScheduler } from "@/worker/worker.interface.js"; import { getQueueJobsQueryValidation } from "@/api/validation/queueValidation.js"; -import { Request, Response, NextFunction } from "express"; +import { Request, Response, RequestHandler } from "express"; +import { catchAsync } from "@/utils/catchAsync.js"; const SERVICE_NAME = "JobQueueController"; 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 { @@ -19,59 +20,43 @@ class JobQueueController implements IJobQueueController { return JobQueueController.SERVICE_NAME; } - 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); - } - }; + 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, + }); + }); - 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); - } - }; + 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 }, + }); + }); - 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); - } - }; + 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 }, + }); + }); - 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..06085c3f8 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"; @@ -10,9 +11,9 @@ 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 { @@ -50,94 +51,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..f75c5e7fb 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, @@ -16,12 +17,12 @@ 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 { @@ -35,132 +36,108 @@ class StatusPageController implements IStatusPageController { 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/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; +}; From 825a81d3264378286b0899639079f02c7ff4a94b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 15 Jul 2026 10:20:16 -0700 Subject: [PATCH 33/84] extract shared mongo mapper utils --- .../app-settings.repository.mongo.ts | 23 +++------- .../domain/checks/check.repository.mongo.ts | 15 +------ server/src/domain/dlq/dlq.repository.mongo.ts | 28 +++--------- .../geo-checks/geo-check.repository.mongo.ts | 15 +------ .../incidents/incident.repository.mongo.ts | 43 +++++++------------ .../domain/invites/invite.repository.mongo.ts | 26 +++-------- .../maintenance-window.repository.mongo.ts | 30 ++++--------- .../monitor-stats.repository.mongo.ts | 12 +----- .../monitors/monitor.repository.mongo.ts | 17 +------- .../notification.repository.mongo.ts | 9 +--- .../recovery-token.repository.mongo.ts | 23 +++------- .../status-page-repository.mongo.ts | 29 ++++--------- .../src/domain/tags/tag.repository.mongo.ts | 24 +++-------- .../src/domain/teams/team.repository.model.ts | 25 +++-------- .../src/domain/users/user.repository.mongo.ts | 23 +++------- server/src/utils/mongoMappers.ts | 15 +++++++ 16 files changed, 89 insertions(+), 268 deletions(-) create mode 100644 server/src/utils/mongoMappers.ts 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..191e6cfcd 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 => { 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/checks/check.repository.mongo.ts b/server/src/domain/checks/check.repository.mongo.ts index 3e8d1c52a..1433c950a 100644 --- a/server/src/domain/checks/check.repository.mongo.ts +++ b/server/src/domain/checks/check.repository.mongo.ts @@ -17,6 +17,7 @@ import { CheckModel, type CheckDocument } from "@/domain/checks/check.model.js"; import mongoose from "mongoose"; import { 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"; @@ -33,20 +34,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, diff --git a/server/src/domain/dlq/dlq.repository.mongo.ts b/server/src/domain/dlq/dlq.repository.mongo.ts index 4cf7bad85..bf02029b4 100644 --- a/server/src/domain/dlq/dlq.repository.mongo.ts +++ b/server/src/domain/dlq/dlq.repository.mongo.ts @@ -4,36 +4,22 @@ 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 => { 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), }; }; 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..e73056e95 100644 --- a/server/src/domain/geo-checks/geo-check.repository.mongo.ts +++ b/server/src/domain/geo-checks/geo-check.repository.mongo.ts @@ -5,6 +5,7 @@ import { GeoCheckMetadataDocument, GeoCheckModel, type GeoCheckDocument } from " import mongoose, { PipelineStage } from "mongoose"; import { getDateForRange } from "@/utils/dataUtils.js"; import { ILogger } from "@/utils/logger.js"; +import { toStringId, toDateString } from "@/utils/mongoMappers.js"; const SERVICE_NAME = "GeoChecksRepository"; @@ -17,20 +18,6 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { } 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), diff --git a/server/src/domain/incidents/incident.repository.mongo.ts b/server/src/domain/incidents/incident.repository.mongo.ts index 4cf410e88..35cd5d56e 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, @@ -48,20 +35,20 @@ class MongoIncidentsRepository implements IIncidentsRepository { protected 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), }; }; @@ -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/invites/invite.repository.mongo.ts b/server/src/domain/invites/invite.repository.mongo.ts index 8956a08b3..0c51147a1 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 => { 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/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/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.repository.mongo.ts b/server/src/domain/monitors/monitor.repository.mongo.ts index e7ccf56e9..28e4513f2 100644 --- a/server/src/domain/monitors/monitor.repository.mongo.ts +++ b/server/src/domain/monitors/monitor.repository.mongo.ts @@ -6,6 +6,7 @@ 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 }); @@ -440,18 +441,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 => { - if (!value) return ""; - return value instanceof Date ? value.toISOString() : value; - }; - const notificationIds = (doc.notifications ?? []).map((notification) => toStringId(notification)); const tagIds = (doc.tags ?? []).map((tag) => toStringId(tag)); return { @@ -506,10 +495,6 @@ class MongoMonitorsRepository implements IMonitorsRepository { }; 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/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/recovery-tokens/recovery-token.repository.mongo.ts b/server/src/domain/recovery-tokens/recovery-token.repository.mongo.ts index 1e6140a88..5ee7db994 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 => { 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/tags/tag.repository.mongo.ts b/server/src/domain/tags/tag.repository.mongo.ts index 5d7d8d0a2..51354b364 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 => { 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..22f171396 100644 --- a/server/src/domain/users/user.repository.mongo.ts +++ b/server/src/domain/users/user.repository.mongo.ts @@ -1,27 +1,14 @@ -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) { @@ -35,7 +22,7 @@ class MongoUsersRepository implements IUsersRepository { protected 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 +32,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), }; }; 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(); +}; From 0e52cb6231ab57b28e6ca5432c26ded7cc5fb5f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pomyka=C5=82a?= Date: Sat, 18 Jul 2026 20:22:00 +0200 Subject: [PATCH 34/84] Add polish --- client/src/locales/pl.json | 1663 ++++++++++++++++++++++++++++++++++++ 1 file changed, 1663 insertions(+) create mode 100644 client/src/locales/pl.json diff --git a/client/src/locales/pl.json b/client/src/locales/pl.json new file mode 100644 index 000000000..40b5c7c4f --- /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. Po utworzeniu bezpiecznie udostępnij dane logowania.", + "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": "Członek", + "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": "jedna mała litera" + }, + "match": { + "beginning": "Hasła", + "highlighted": "musi pasować" + }, + "number": { + "beginning": "Musi zawierać co najmniej", + "highlighted": "jedna cyfra" + }, + "special": { + "beginning": "Musi zawierać co najmniej", + "highlighted": "jeden znak specjalny (!?@#$%^&*()-_=+[]{}|;:'\",./\\~`)" + }, + "uppercase": { + "beginning": "Musi zawierać co najmniej", + "highlighted": "jedna wielka litera" + } + } + }, + "forgotPassword": { + "links": { + "login": { + "linkText": "zaloguj się", + "text": "Wróć do" + } + }, + "submit": "Wyślij prośbę o odzyskanie", + "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 superadministratora 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 sondy statusu i czasy odpowiedzi będą wyświetlane tutaj jako sprawdzenia.", + "title": "Brak jeszcze kontroli" + }, + "header": { + "description": "Każde sprawdzenie wykonywane przez Checkmate na Twoich monitorach pojawia się tutaj wraz z kodem statusu i czasem odpowiedzi.", + "title": "Kontrole" + }, + "selects": { + "monitor": { + "all": "Wszystkie monitory" + }, + "status": { + "all": "Wszystkie", + "down": "Niedostępny", + "up": "Działa" + } + }, + "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": "Konfiguruj", + "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 wstrzymany", + "paused_other": "Wstrzymano {{count}} monitorów", + "resumed_one": "{{count}} monitor 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 docelowego serwera. Obsługuje dopasowywanie kodu statusu, ale nie zaawansowane dopasowywanie, 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ę wyświetlaną.", + "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 czytelną nazwę wyświetlaną na pulpicie.", + "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 czytelną nazwę wyświetlaną na pulpicie.", + "port": "Wprowadź adres URL lub IP serwera, numer portu oraz czytelną nazwę wyświetlaną na pulpicie.", + "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": "Wyświetlana nazwa", + "placeholder": "np. Moja strona internetowa" + }, + "port": { + "label": "Port do monitorowania", + "placeholder": "80" + }, + "secret": { + "label": "Tajny klucz autoryzacji", + "placeholder": "Wprowadź swój klucz sekretu" + }, + "strategy": { + "desktop": "Komputer stacjonarny", + "label": "Strategia", + "mobile": "Mobilny" + }, + "url": { + "label": "URL", + "placeholder": "https://www.google.com" + }, + "wsUrl": { + "label": "Adres URL WebSocket", + "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 geograficznie rozproszone" + }, + "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 geograficznie rozproszone" + }, + "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 przesuwnym osiągnie określoną wartość.", + "option": { + "checks": { + "label": "Liczba sprawdzeń w oknie przesuwnym" + }, + "percentage": { + "label": "Jaki procent sprawdzeń w ruchomym oknie 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": "Śledź 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 okno konserwacji", + "description": "Zaplanuj planowaną przerwę, aby powiadomienia były wyciszone, a Twój zespół i użytkownicy wiedzieli, czego się spodziewać.", + "title": "Brak zaplanowanych okien konserwacyjnych" + }, + "form": { + "general": { + "description": "Ustaw nazwę i opcję powtarzania dla okna konserwacji.", + "option": { + "name": { + "label": "Nazwa", + "placeholder": "np. Cotygodniowa konserwacja" + }, + "repeat": { + "label": "Powtarzaj" + } + }, + "title": "Ustawienia ogólne" + }, + "startDate": { + "description": "Wybierz datę rozpoczęcia okna konserwacji.", + "option": { + "startDate": { + "label": "Data rozpoczęcia" + } + }, + "title": "Data rozpoczęcia" + }, + "startTime": { + "description": "Ustaw czas rozpoczęcia i czas trwania okna konserwacyjnego. Wszystkie wartości są w UTC", + "monitors": { + "description": "Monitory, do których należy zastosować okno konserwacji", + "option": { + "addMonitors": { + "label": "Dodaj monitry" + } + }, + "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": "Okna konserwacji" + }, + "table": { + "headers": { + "nextWindow": "Następne okno", + "repeat": "Powtórz" + } + } + }, + "notFound": { + "subtitle": "Adres URL nie istnieje lub nie masz do niego dostępu.", + "title": "O nie! Upuściłeś swoje sushi!" + }, + "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": "Śledź 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": "Śledź Core Web Vitals i wyniki Lighthouse w czasie, aby wykryć regresje, zanim zauważą je użytkownicy.", + "title": "Monitory PageSpeed" + }, + "strategy": { + "desktop": "Komputer stacjonarny", + "mobile": "Mobilne" + }, + "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": "Zasilane przez" + }, + "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 monitry 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": "Płaski, skoncentrowany i kompaktowy, zawierający tylko najważniejsze elementy.", + "name": "Minimalistyczny" + }, + "modern": { + "description": "Klasa SaaS 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": "Mapa cieplna czasu odpowiedzi", + "max": "maks. {{value}}ms" + }, + "chartTypeHeatmap": "Mapa ciepła", + "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 %" + } + } + } + } +} From 6e2a64fc9fa03c43626b714ba7db35a9a20a0313 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pomyka=C5=82a?= Date: Sat, 18 Jul 2026 20:57:50 +0200 Subject: [PATCH 35/84] Add Polish to language selector --- client/src/Components/inputs/LanguageSelector.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/client/src/Components/inputs/LanguageSelector.tsx b/client/src/Components/inputs/LanguageSelector.tsx index 116f45069..2798e0539 100644 --- a/client/src/Components/inputs/LanguageSelector.tsx +++ b/client/src/Components/inputs/LanguageSelector.tsx @@ -15,6 +15,7 @@ export const languageNames: Record = { fr: "Français", it: "Italiano", "pt-BR": "Português (BR)", + pl: "Polski", cs: "Čeština", fi: "Suomi", tr: "Türkçe", From cf451416b1385f74b61261920bb575a419f0ad1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pomyka=C5=82a?= Date: Sat, 18 Jul 2026 21:33:10 +0200 Subject: [PATCH 36/84] Update polish translations after review --- client/src/locales/pl.json | 112 ++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 56 deletions(-) diff --git a/client/src/locales/pl.json b/client/src/locales/pl.json index 40b5c7c4f..0fa50ea9f 100644 --- a/client/src/locales/pl.json +++ b/client/src/locales/pl.json @@ -275,7 +275,7 @@ }, "team": { "addMember": { - "description": "Utwórz nowe konto użytkownika. Po utworzeniu bezpiecznie udostępnij dane logowania.", + "description": "Utwórz nowe konto użytkownika. Następnie przekaż dane logowania w bezpieczny sposób.", "title": "Zarejestruj nowego członka zespołu" }, "fallback": { @@ -285,7 +285,7 @@ "filter": { "admin": "Administrator", "all": "Wszystkie role", - "member": "Członek", + "member": "Użytkownik", "placeholder": "Filtruj według roli" }, "invite": { @@ -339,7 +339,7 @@ }, "lowercase": { "beginning": "Musi zawierać co najmniej", - "highlighted": "jedna mała litera" + "highlighted": "jedną małą literę" }, "match": { "beginning": "Hasła", @@ -347,7 +347,7 @@ }, "number": { "beginning": "Musi zawierać co najmniej", - "highlighted": "jedna cyfra" + "highlighted": "jedną cyfrę" }, "special": { "beginning": "Musi zawierać co najmniej", @@ -355,7 +355,7 @@ }, "uppercase": { "beginning": "Musi zawierać co najmniej", - "highlighted": "jedna wielka litera" + "highlighted": "jedną wielką literę" } } }, @@ -366,7 +366,7 @@ "text": "Wróć do" } }, - "submit": "Wyślij prośbę o odzyskanie", + "submit": "Wyślij prośbę o odzyskanie hasła", "subtitle": "Nie martw się, wyślemy Ci instrukcje resetowania.", "title": "Zapomniałeś hasła?" }, @@ -386,7 +386,7 @@ "title": "Witamy ponownie w Checkmate!" }, "register": { - "setupNotice": "Ta strona jest dostępna tylko podczas początkowej konfiguracji. Po zarejestrowaniu superadministratora można zapraszać dodatkowych użytkowników z poziomu ustawień zespołu.", + "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!" @@ -398,12 +398,12 @@ }, "checks": { "fallback": { - "description": "Po utworzeniu monitora wszystkie sondy statusu i czasy odpowiedzi będą wyświetlane tutaj jako sprawdzenia.", - "title": "Brak jeszcze kontroli" + "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": "Kontrole" + "title": "Checki" }, "selects": { "monitor": { @@ -412,7 +412,7 @@ "status": { "all": "Wszystkie", "down": "Niedostępny", - "up": "Działa" + "up": "Dostępny" } }, "table": { @@ -427,8 +427,8 @@ "monitors": { "actions": { "bulkSelected_one": "Wybrano {{count}} monitor", - "bulkSelected_other": "Wybrano {{count}} monitorów", - "configure": "Konfiguruj", + "bulkSelected_other": "Wybrano {{count}} monitor(ów)", + "configure": "Skonfiguruj", "delete": "Usuń", "details": "Szczegóły", "generateToken": "Wygeneruj token", @@ -445,11 +445,11 @@ "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 wstrzymany", - "paused_other": "Wstrzymano {{count}} monitorów", - "resumed_one": "{{count}} monitor wznowiony", - "resumed_other": "Wznowiono {{count}} monitorów" + "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", @@ -510,7 +510,7 @@ "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 docelowego serwera. Obsługuje dopasowywanie kodu statusu, ale nie zaawansowane dopasowywanie, ponieważ nie ma treści odpowiedzi." + "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" } @@ -543,12 +543,12 @@ "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ę wyświetlaną.", + "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 czytelną nazwę wyświetlaną na pulpicie.", + "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 czytelną nazwę wyświetlaną na pulpicie.", - "port": "Wprowadź adres URL lub IP serwera, numer portu oraz czytelną nazwę wyświetlaną na pulpicie.", + "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": { @@ -588,7 +588,7 @@ "placeholder": "192.168.1.100 lub example.com" }, "name": { - "label": "Wyświetlana nazwa", + "label": "Nazwa", "placeholder": "np. Moja strona internetowa" }, "port": { @@ -596,20 +596,20 @@ "placeholder": "80" }, "secret": { - "label": "Tajny klucz autoryzacji", - "placeholder": "Wprowadź swój klucz sekretu" + "label": "Klucz autoryzacyjny", + "placeholder": "Wprowadź swój klucz" }, "strategy": { - "desktop": "Komputer stacjonarny", + "desktop": "Desktop", "label": "Strategia", - "mobile": "Mobilny" + "mobile": "Mobile" }, "url": { "label": "URL", "placeholder": "https://www.google.com" }, "wsUrl": { - "label": "Adres URL WebSocket", + "label": "WebSocket URL", "placeholder": "wss://example.com/socket" } }, @@ -619,7 +619,7 @@ "description": "Przeprowadzaj sprawdzenia z różnych lokalizacji geograficznych, aby monitorować globalną dostępność i wydajność.", "option": { "enabled": { - "label": "Włącz kontrole geograficznie rozproszone" + "label": "Włącz kontrole rozproszone geograficznie" }, "interval": { "label": "Interwał sprawdzania", @@ -643,7 +643,7 @@ "placeholder": "Wybierz lokalizacje" } }, - "title": "Kontrole geograficznie rozproszone" + "title": "Kontrole rozproszone geograficznie" }, "ignoreTls": { "description": "Skonfiguruj weryfikację certyfikatu TLS/SSL dla połączeń HTTPS.", @@ -655,13 +655,13 @@ "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 przesuwnym osiągnie określoną wartość.", + "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 przesuwnym" + "label": "Liczba sprawdzeń w oknie czasowym" }, "percentage": { - "label": "Jaki procent sprawdzeń w ruchomym oknie zakończy się niepowodzeniem/powodzeniem, zanim zmieni się status monitora?" + "label": "Jaki procent sprawdzeń w oknie czasowym zakończy się niepowodzeniem/powodzeniem, zanim zmieni się status monitora?" } }, "title": "Incydenty" @@ -868,7 +868,7 @@ } }, "header": { - "description": "Śledź wydajność serwera, identyfikuj wąskie gardła i zapewnij niezawodność dzięki monitorowaniu infrastruktury w czasie rzeczywistym.", + "description": "Kontroluj wydajność serwera, identyfikuj wąskie gardła i zapewnij niezawodność dzięki monitorowaniu infrastruktury w czasie rzeczywistym.", "title": "Monitory infrastruktury" }, "statBoxes": { @@ -1012,17 +1012,17 @@ }, "maintenanceWindow": { "fallback": { - "actionButton": "Utwórz okno konserwacji", + "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 konserwacyjnych" + "title": "Brak zaplanowanych okien serwisowych" }, "form": { "general": { - "description": "Ustaw nazwę i opcję powtarzania dla okna konserwacji.", + "description": "Ustaw nazwę i opcję powtarzania dla przerwy technicznej.", "option": { "name": { "label": "Nazwa", - "placeholder": "np. Cotygodniowa konserwacja" + "placeholder": "np. Cotygodniowa przerwa techniczna" }, "repeat": { "label": "Powtarzaj" @@ -1031,7 +1031,7 @@ "title": "Ustawienia ogólne" }, "startDate": { - "description": "Wybierz datę rozpoczęcia okna konserwacji.", + "description": "Wybierz datę rozpoczęcia przerwy technicznej.", "option": { "startDate": { "label": "Data rozpoczęcia" @@ -1040,12 +1040,12 @@ "title": "Data rozpoczęcia" }, "startTime": { - "description": "Ustaw czas rozpoczęcia i czas trwania okna konserwacyjnego. Wszystkie wartości są w UTC", + "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ć okno konserwacji", + "description": "Monitory, do których należy zastosować przerwę techniczną", "option": { "addMonitors": { - "label": "Dodaj monitry" + "label": "Dodaj monitory" } }, "title": "Monitory" @@ -1063,18 +1063,18 @@ }, "header": { "description": "Zaplanuj planowaną przerwę, aby powiadomienia były wyciszone, a Twój zespół i użytkownicy wiedzieli, czego się spodziewać.", - "title": "Okna konserwacji" + "title": "Przerwy techniczne" }, "table": { "headers": { - "nextWindow": "Następne okno", + "nextWindow": "Następna przerwa", "repeat": "Powtórz" } } }, "notFound": { "subtitle": "Adres URL nie istnieje lub nie masz do niego dostępu.", - "title": "O nie! Upuściłeś swoje sushi!" + "title": "Coś się popsuło i nie było mnie słychać..." }, "notifications": { "fallback": { @@ -1189,16 +1189,16 @@ }, "fallback": { "actionButton": "Utwórz monitor", - "description": "Śledź wskaźniki Core Web Vitals i wyniki Lighthouse w czasie, aby wykryć regresje zanim zrobią to użytkownicy.", + "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": "Śledź Core Web Vitals i wyniki Lighthouse w czasie, aby wykryć regresje, zanim zauważą je użytkownicy.", + "description": "Monitoruj Core Web Vitals i wyniki Lighthouse w czasie, aby wykryć regresje, zanim zauważą je użytkownicy.", "title": "Monitory PageSpeed" }, "strategy": { - "desktop": "Komputer stacjonarny", - "mobile": "Mobilne" + "desktop": "Desktop", + "mobile": "Mobile" }, "table": { "headers": { @@ -1410,7 +1410,7 @@ "title": "Brak stron statusu" }, "footer": { - "poweredBy": "Zasilane przez" + "poweredBy": "Powered by" }, "form": { "access": { @@ -1482,7 +1482,7 @@ "title": "Funkcje" }, "monitors": { - "description": "Wybierz monitry do wyświetlenia na swojej stronie statusu.", + "description": "Wybierz monitory do wyświetlenia na swojej stronie statusu.", "noMonitors": "Nie wybrano monitorów", "option": { "monitors": { @@ -1505,11 +1505,11 @@ "name": "Redakcyjny" }, "minimal": { - "description": "Płaski, skoncentrowany i kompaktowy, zawierający tylko najważniejsze elementy.", + "description": "Prosty i kompaktowy, zawierający tylko najważniejsze elementy.", "name": "Minimalistyczny" }, "modern": { - "description": "Klasa SaaS z pulsującą kropką statusu i delikatnie zanikającymi kartami.", + "description": "SaaS'owy z pulsującą kropką statusu i delikatnie zanikającymi kartami.", "name": "Nowoczesny" }, "refined": { @@ -1544,10 +1544,10 @@ "chart": { "avg": "śr. {{value}}ms", "downTooltip": "Niedostępny", - "heatmapAria": "Mapa cieplna czasu odpowiedzi", + "heatmapAria": "Heatmapa czasu odpowiedzi", "max": "maks. {{value}}ms" }, - "chartTypeHeatmap": "Mapa ciepła", + "chartTypeHeatmap": "Heatmapa", "chartTypeHistogram": "Histogram", "infrastructure": { "cpu": "CPU", From 924c862244c717b7e8bfceab9ab9e1cdebb681e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Pomyka=C5=82a?= Date: Sat, 18 Jul 2026 22:13:05 +0200 Subject: [PATCH 37/84] Format pl.json --- client/src/locales/pl.json | 3322 ++++++++++++++++++------------------ 1 file changed, 1661 insertions(+), 1661 deletions(-) diff --git a/client/src/locales/pl.json b/client/src/locales/pl.json index 0fa50ea9f..9a74b6cdb 100644 --- a/client/src/locales/pl.json +++ b/client/src/locales/pl.json @@ -1,1663 +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 %" - } - } - } - } + "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 %" + } + } + } + } } From 368595178d73868470121c8ca8cf375679e8a553 Mon Sep 17 00:00:00 2001 From: Osamaali313 Date: Sat, 18 Jul 2026 23:36:43 +0300 Subject: [PATCH 38/84] Fix missing "Poor" label on the average response-time gauge MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `RadialAvgResponse` maps the response-time palette to a qualitative word: const palette = getResponseTimeColor(avg); const msg: Record = { success: "Excellent", warning: "Average", danger: "Poor", }; ... {msg[palette]} But `getResponseTimeColor` only ever returns `"success" | "warning" | "error"` (MonitorUtils.ts) — never `"danger"`. So for the slow case `palette` is `"error"`, `msg["error"]` is `undefined`, and no label renders. The same component uses `theme.palette[palette].main`, which works precisely because `palette` is the real MUI key `"error"`; every other palette helper in MonitorUtils.ts also uses `"error"`, and `"danger"` appears nowhere else in the client. Any monitor whose average response time is >= 300 ms shows an empty label exactly where the "Poor" signal matters most. Use the `error` key. --- client/src/Components/monitors/charts/RadialAvgResponse.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ( From 3eadb99c5e4c68213358c6b499836b41c32d3b47 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 20 Jul 2026 10:01:48 -0700 Subject: [PATCH 39/84] protected -> private as no class inherits --- .../src/domain/app-settings/app-settings.repository.mongo.ts | 2 +- server/src/domain/dlq/dlq.repository.mongo.ts | 4 ++-- server/src/domain/incidents/incident.repository.mongo.ts | 4 ++-- server/src/domain/invites/invite.repository.mongo.ts | 2 +- .../src/domain/queue-workers/queue-worker.repository.mongo.ts | 2 +- .../domain/recovery-tokens/recovery-token.repository.mongo.ts | 2 +- server/src/domain/tags/tag.repository.mongo.ts | 2 +- server/src/domain/users/user.repository.mongo.ts | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) 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 191e6cfcd..d88141ce5 100644 --- a/server/src/domain/app-settings/app-settings.repository.mongo.ts +++ b/server/src/domain/app-settings/app-settings.repository.mongo.ts @@ -5,7 +5,7 @@ import { AppSettingsModel, type AppSettingsDocument } from "@/domain/app-setting import { toStringId, toDateString } from "@/utils/mongoMappers.js"; class MongoSettingsRepository implements ISettingsRepository { - protected toEntity = (doc: AppSettingsDocument): Settings => { + private toEntity = (doc: AppSettingsDocument): Settings => { return { id: toStringId(doc._id), checkTTL: doc.checkTTL, diff --git a/server/src/domain/dlq/dlq.repository.mongo.ts b/server/src/domain/dlq/dlq.repository.mongo.ts index bf02029b4..c30ccb261 100644 --- a/server/src/domain/dlq/dlq.repository.mongo.ts +++ b/server/src/domain/dlq/dlq.repository.mongo.ts @@ -6,7 +6,7 @@ import mongoose from "mongoose"; import { AppError } from "@/utils/AppError.js"; import { toStringId, toDateString } from "@/utils/mongoMappers.js"; class MongoDLQRepository implements IDLQRepository { - protected toEntity = (doc: DLQItemDocument): DLQItem => { + private toEntity = (doc: DLQItemDocument): DLQItem => { return { id: toStringId(doc._id), type: doc.type, @@ -23,7 +23,7 @@ class MongoDLQRepository implements IDLQRepository { }; }; - protected mapDocuments = (documents: DLQItemDocument[] | DLQItemDocument | null): DLQItem[] => { + private mapDocuments = (documents: DLQItemDocument[] | DLQItemDocument | null): DLQItem[] => { if (!documents) { return []; } diff --git a/server/src/domain/incidents/incident.repository.mongo.ts b/server/src/domain/incidents/incident.repository.mongo.ts index 35cd5d56e..0123ef7c8 100644 --- a/server/src/domain/incidents/incident.repository.mongo.ts +++ b/server/src/domain/incidents/incident.repository.mongo.ts @@ -33,7 +33,7 @@ class MongoIncidentsRepository implements IIncidentsRepository { return matchStage; } - protected toEntity = (doc: IncidentDocument): Incident => { + private toEntity = (doc: IncidentDocument): Incident => { return { id: toStringId(doc._id), monitorId: toStringId(doc.monitorId), @@ -52,7 +52,7 @@ class MongoIncidentsRepository implements IIncidentsRepository { }; }; - protected mapDocuments = (documents: IncidentDocument[] | IncidentDocument | null): Incident[] => { + private mapDocuments = (documents: IncidentDocument[] | IncidentDocument | null): Incident[] => { if (!documents) { return []; } diff --git a/server/src/domain/invites/invite.repository.mongo.ts b/server/src/domain/invites/invite.repository.mongo.ts index 0c51147a1..78c666d52 100644 --- a/server/src/domain/invites/invite.repository.mongo.ts +++ b/server/src/domain/invites/invite.repository.mongo.ts @@ -6,7 +6,7 @@ import crypto from "crypto"; import { toStringId, toDateString } from "@/utils/mongoMappers.js"; class MongoInvitesRepository implements IInvitesRepository { - protected toEntity = (doc: InviteDocument): Invite => { + private toEntity = (doc: InviteDocument): Invite => { return { id: toStringId(doc._id), email: doc.email, 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 5ee7db994..02ba3089b 100644 --- a/server/src/domain/recovery-tokens/recovery-token.repository.mongo.ts +++ b/server/src/domain/recovery-tokens/recovery-token.repository.mongo.ts @@ -10,7 +10,7 @@ const SERVICE_NAME = "MongoRecoveryTokensRepository"; class MongoRecoveryTokensRepository implements IRecoveryTokensRepository { static SERVICE_NAME = SERVICE_NAME; - protected toEntity = (doc: RecoveryTokenDocument): RecoveryToken => { + private toEntity = (doc: RecoveryTokenDocument): RecoveryToken => { return { id: toStringId(doc._id), email: doc.email, diff --git a/server/src/domain/tags/tag.repository.mongo.ts b/server/src/domain/tags/tag.repository.mongo.ts index 51354b364..6345f23da 100644 --- a/server/src/domain/tags/tag.repository.mongo.ts +++ b/server/src/domain/tags/tag.repository.mongo.ts @@ -9,7 +9,7 @@ const SERVICE_NAME = "TagsRepository"; class MongoTagsRepository implements ITagsRepository { static SERVICE_NAME = SERVICE_NAME; - protected toEntity = (doc: TagDocument): Tag => { + private toEntity = (doc: TagDocument): Tag => { return { id: toStringId(doc._id), teamId: toStringId(doc.teamId), diff --git a/server/src/domain/users/user.repository.mongo.ts b/server/src/domain/users/user.repository.mongo.ts index 22f171396..b137c5f4c 100644 --- a/server/src/domain/users/user.repository.mongo.ts +++ b/server/src/domain/users/user.repository.mongo.ts @@ -20,7 +20,7 @@ class MongoUsersRepository implements IUsersRepository { }; }; - protected toEntity = (doc: UserDocument): User => { + private toEntity = (doc: UserDocument): User => { return { id: toStringId(doc._id), firstName: doc.firstName, From d485adbb5479ce3e13d985b38828015b0fa0cf2e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 20 Jul 2026 10:04:55 -0700 Subject: [PATCH 40/84] narrow mapDocument types --- server/src/domain/dlq/dlq.repository.mongo.ts | 2 +- server/src/domain/incidents/incident.repository.mongo.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/domain/dlq/dlq.repository.mongo.ts b/server/src/domain/dlq/dlq.repository.mongo.ts index c30ccb261..63fec34c5 100644 --- a/server/src/domain/dlq/dlq.repository.mongo.ts +++ b/server/src/domain/dlq/dlq.repository.mongo.ts @@ -23,7 +23,7 @@ class MongoDLQRepository implements IDLQRepository { }; }; - private mapDocuments = (documents: DLQItemDocument[] | DLQItemDocument | null): DLQItem[] => { + private mapDocuments = (documents: DLQItemDocument[]) => { if (!documents) { return []; } diff --git a/server/src/domain/incidents/incident.repository.mongo.ts b/server/src/domain/incidents/incident.repository.mongo.ts index 0123ef7c8..61cea96b8 100644 --- a/server/src/domain/incidents/incident.repository.mongo.ts +++ b/server/src/domain/incidents/incident.repository.mongo.ts @@ -52,7 +52,7 @@ class MongoIncidentsRepository implements IIncidentsRepository { }; }; - private mapDocuments = (documents: IncidentDocument[] | IncidentDocument | null): Incident[] => { + private mapDocuments = (documents: IncidentDocument[]): Incident[] => { if (!documents) { return []; } From 4d9e5821a8b3fb28bdb0664dbd93ed748f8e72a2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 20 Jul 2026 10:21:43 -0700 Subject: [PATCH 41/84] purge unused groups route, update tests --- server/src/api/controllers/monitorController.ts | 11 ----------- server/src/api/routes/monitorRoute.ts | 1 - .../domain/monitors/monitor.repository.interface.ts | 1 - .../src/domain/monitors/monitor.repository.mongo.ts | 8 -------- server/src/domain/monitors/monitor.service.ts | 5 ----- server/test/helpers/InMemoryMonitorsRepository.ts | 4 ---- server/test/unit/services/monitorService.test.ts | 13 ------------- 7 files changed, 43 deletions(-) diff --git a/server/src/api/controllers/monitorController.ts b/server/src/api/controllers/monitorController.ts index e0f820a62..2ffb18802 100644 --- a/server/src/api/controllers/monitorController.ts +++ b/server/src/api/controllers/monitorController.ts @@ -44,7 +44,6 @@ export interface IMonitorController { getMonitorsWithChecksByTeamId: RequestHandler; exportMonitorsToJSON: RequestHandler; getAllGames: RequestHandler; - getGroupsByTeamId: RequestHandler; updateNotifications: RequestHandler; } class MonitorController implements IMonitorController { @@ -365,16 +364,6 @@ class MonitorController implements IMonitorController { }); }); - getGroupsByTeamId = catchAsync(async (req: Request, res: Response) => { - const teamId = requireTeamId(req.user?.teamId); - const groups = await this.monitorService.getGroupsByTeamId({ teamId }); - - return res.status(200).json({ - msg: "Groups retrieved successfully", - data: groups, - }); - }); - updateNotifications = catchAsync(async (req: Request, res: Response) => { updateNotificationsValidation.parse(req.body); diff --git a/server/src/api/routes/monitorRoute.ts b/server/src/api/routes/monitorRoute.ts index 29834ce70..74a00637d 100644 --- a/server/src/api/routes/monitorRoute.ts +++ b/server/src/api/routes/monitorRoute.ts @@ -15,7 +15,6 @@ class MonitorRoutes { // 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); diff --git a/server/src/domain/monitors/monitor.repository.interface.ts b/server/src/domain/monitors/monitor.repository.interface.ts index 46f3df396..a710e366f 100644 --- a/server/src/domain/monitors/monitor.repository.interface.ts +++ b/server/src/domain/monitors/monitor.repository.interface.ts @@ -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 28e4513f2..96db34c15 100644 --- a/server/src/domain/monitors/monitor.repository.mongo.ts +++ b/server/src/domain/monitors/monitor.repository.mongo.ts @@ -382,14 +382,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 } }); }; diff --git a/server/src/domain/monitors/monitor.service.ts b/server/src/domain/monitors/monitor.service.ts index 1402cc453..66684c2ac 100644 --- a/server/src/domain/monitors/monitor.service.ts +++ b/server/src/domain/monitors/monitor.service.ts @@ -72,7 +72,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; @@ -422,10 +421,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); diff --git a/server/test/helpers/InMemoryMonitorsRepository.ts b/server/test/helpers/InMemoryMonitorsRepository.ts index 45db5b3f7..09162cfd2 100644 --- a/server/test/helpers/InMemoryMonitorsRepository.ts +++ b/server/test/helpers/InMemoryMonitorsRepository.ts @@ -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/unit/services/monitorService.test.ts b/server/test/unit/services/monitorService.test.ts index 65d6923a6..6a1cf8343 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(), @@ -746,18 +745,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(); From 36434c11601a115c32b8b1dd0c78ff8bc6336c22 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 20 Jul 2026 11:25:39 -0700 Subject: [PATCH 42/84] convert router classes to exported functions --- server/src/api/routes/authRoute.ts | 48 ----------- server/src/api/routes/authRoutes.ts | 27 ++++++ server/src/api/routes/checkRoute.ts | 29 ------- server/src/api/routes/checkRoutes.ts | 14 +++ server/src/api/routes/diagnosticRoute.ts | 24 ------ server/src/api/routes/diagnosticRoutes.ts | 9 ++ server/src/api/routes/geoCheckRoutes.ts | 24 ++---- server/src/api/routes/incidentRoute.ts | 30 ------- server/src/api/routes/incidentRoutes.ts | 12 +++ server/src/api/routes/inviteRoute.ts | 26 ------ server/src/api/routes/inviteRoutes.ts | 11 +++ server/src/api/routes/logRoutes.ts | 24 ++---- .../src/api/routes/maintenanceWindowRoute.ts | 29 ------- .../src/api/routes/maintenanceWindowRoutes.ts | 15 ++++ server/src/api/routes/monitorRoute.ts | 63 -------------- server/src/api/routes/monitorRoutes.ts | 50 +++++++++++ server/src/api/routes/notificationRoute.ts | 31 ------- server/src/api/routes/notificationRoutes.ts | 14 +++ server/src/api/routes/queueRoute.ts | 26 ------ server/src/api/routes/queueRoutes.ts | 12 +++ server/src/api/routes/settingsRoute.ts | 26 ------ server/src/api/routes/settingsRoutes.ts | 11 +++ server/src/api/routes/statusPageRoute.ts | 32 ------- server/src/api/routes/statusPageRoutes.ts | 19 +++++ server/src/api/routes/tagRoutes.ts | 34 ++------ server/src/config/routes.ts | 85 +++++++++---------- 26 files changed, 256 insertions(+), 469 deletions(-) delete mode 100644 server/src/api/routes/authRoute.ts create mode 100644 server/src/api/routes/authRoutes.ts delete mode 100644 server/src/api/routes/checkRoute.ts create mode 100644 server/src/api/routes/checkRoutes.ts delete mode 100644 server/src/api/routes/diagnosticRoute.ts create mode 100644 server/src/api/routes/diagnosticRoutes.ts delete mode 100644 server/src/api/routes/incidentRoute.ts create mode 100644 server/src/api/routes/incidentRoutes.ts delete mode 100644 server/src/api/routes/inviteRoute.ts create mode 100644 server/src/api/routes/inviteRoutes.ts delete mode 100644 server/src/api/routes/maintenanceWindowRoute.ts create mode 100644 server/src/api/routes/maintenanceWindowRoutes.ts delete mode 100644 server/src/api/routes/monitorRoute.ts create mode 100644 server/src/api/routes/monitorRoutes.ts delete mode 100644 server/src/api/routes/notificationRoute.ts create mode 100644 server/src/api/routes/notificationRoutes.ts delete mode 100644 server/src/api/routes/queueRoute.ts create mode 100644 server/src/api/routes/queueRoutes.ts delete mode 100644 server/src/api/routes/settingsRoute.ts create mode 100644 server/src/api/routes/settingsRoutes.ts delete mode 100644 server/src/api/routes/statusPageRoute.ts create mode 100644 server/src/api/routes/statusPageRoutes.ts 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 74a00637d..000000000 --- a/server/src/api/routes/monitorRoute.ts +++ /dev/null @@ -1,63 +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); - - // 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/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); }; From 5d3dc03c89005054ae0d1f1e70ffcb12f2d9954b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 21 Jul 2026 10:02:20 -0700 Subject: [PATCH 43/84] add a provider registry to notificaiton service, replace positional args and switch statements --- .../notifications/notification.service.ts | 146 ++++++------------ 1 file changed, 50 insertions(+), 96 deletions(-) diff --git a/server/src/domain/notifications/notification.service.ts b/server/src/domain/notifications/notification.service.ts index 7fc6e3543..59d36d928 100644 --- a/server/src/domain/notifications/notification.service.ts +++ b/server/src/domain/notifications/notification.service.ts @@ -9,6 +9,10 @@ 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"; +import { no } from "zod/v4/locales"; + +export type NotificationProviderRegistry = Record; export interface INotificationsService { createNotification: (notificationData: Partial, userId: string, teamId: string) => Promise; @@ -29,52 +33,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 +78,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 +125,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[]) => { From 57e008c7ba4d75c836cf1a647cb473ff38d6221f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 21 Jul 2026 10:05:17 -0700 Subject: [PATCH 44/84] update services config --- server/src/config/services.shared.ts | 33 ++++++++++++++++------------ 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/server/src/config/services.shared.ts b/server/src/config/services.shared.ts index 49b73f52e..72fe3fcf4 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"; @@ -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, From bb374db463a158f2f6c60f903dea7bd5cc4bd8d7 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 21 Jul 2026 10:07:09 -0700 Subject: [PATCH 45/84] remove stray import --- server/src/domain/notifications/notification.service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/server/src/domain/notifications/notification.service.ts b/server/src/domain/notifications/notification.service.ts index 59d36d928..30141c3ea 100644 --- a/server/src/domain/notifications/notification.service.ts +++ b/server/src/domain/notifications/notification.service.ts @@ -10,7 +10,6 @@ import type { ISettingsService } from "@/domain/app-settings/app-settings.servic 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"; -import { no } from "zod/v4/locales"; export type NotificationProviderRegistry = Record; From aad03b787ff927d56e02855483e6ed11dad9f2f2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 21 Jul 2026 10:07:18 -0700 Subject: [PATCH 46/84] update tests --- .../services/notificationsService.test.ts | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/server/test/unit/services/notificationsService.test.ts b/server/test/unit/services/notificationsService.test.ts index 8ba8b34aa..6fbbfa4a1 100644 --- a/server/test/unit/services/notificationsService.test.ts +++ b/server/test/unit/services/notificationsService.test.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 }; }; From 6e047d2cdb20d5e96a95524dcd1ec18f4f05ce41 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 21 Jul 2026 10:25:50 -0700 Subject: [PATCH 47/84] convert positional args to config obj --- server/src/worker/worker.db-queue.ts | 106 +++++++++++++-------------- 1 file changed, 50 insertions(+), 56 deletions(-) diff --git a/server/src/worker/worker.db-queue.ts b/server/src/worker/worker.db-queue.ts index 3ea2fddb8..50e41a4f1 100644 --- a/server/src/worker/worker.db-queue.ts +++ b/server/src/worker/worker.db-queue.ts @@ -31,6 +31,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 +64,43 @@ 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; + + 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; } 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 - ); + static async create(dependencies: DBQueueWorkerDependencies): Promise { + const instance = new DBQueueWorker(dependencies); await instance.init(); return instance; } From 936d9cd9a0144864250349a13b523179d23847e3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 21 Jul 2026 10:26:02 -0700 Subject: [PATCH 48/84] update call site --- server/src/config/services.worker.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) 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 }; }; From c567e4be5e59dd5deca9d32083c347f136c52ae3 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 21 Jul 2026 10:26:10 -0700 Subject: [PATCH 49/84] Update tests --- .../test/unit/services/dbQueueWorker.test.ts | 48 +++++-------------- 1 file changed, 12 insertions(+), 36 deletions(-) diff --git a/server/test/unit/services/dbQueueWorker.test.ts b/server/test/unit/services/dbQueueWorker.test.ts index 68df5f02a..4956e474c 100644 --- a/server/test/unit/services/dbQueueWorker.test.ts +++ b/server/test/unit/services/dbQueueWorker.test.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); From 8e600e9a87e783d2a584e69c5e5116b9c5a6907f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 21 Jul 2026 10:48:18 -0700 Subject: [PATCH 50/84] remove inline enum definitions --- server/src/api/validation/checkValidation.ts | 15 ++++++++------- server/src/api/validation/incidentValidation.ts | 8 +++++--- .../api/validation/maintenanceWindowValidation.ts | 3 ++- server/src/api/validation/monitorValidation.ts | 13 +++++++------ server/src/api/validation/statusPageValidation.ts | 3 ++- server/src/domain/incidents/incident.type.ts | 4 ++-- server/src/types/query.ts | 8 ++++++++ 7 files changed, 34 insertions(+), 20 deletions(-) create mode 100644 server/src/types/query.ts diff --git a/server/src/api/validation/checkValidation.ts b/server/src/api/validation/checkValidation.ts index db0978357..d041c9e8f 100644 --- a/server/src/api/validation/checkValidation.ts +++ b/server/src/api/validation/checkValidation.ts @@ -2,6 +2,7 @@ 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 { 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..1ed9dca84 100644 --- a/server/src/api/validation/monitorValidation.ts +++ b/server/src/api/validation/monitorValidation.ts @@ -11,6 +11,7 @@ import { MonitorTypes, PageSpeedStrategies, } from "@/domain/monitors/monitor.types.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,7 +196,7 @@ export const getUptimeDetailsByIdParamValidation = z.object({ }); export const getUptimeDetailsByIdQueryValidation = z.object({ - dateRange: z.enum(["recent", "hour", "day", "week", "month", "all"]), + dateRange: z.enum(DateRanges), normalize: booleanCoercion.optional(), }); @@ -206,7 +207,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 +264,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/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/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]; From ed1ef56184b0bef2264196dbe938ccdb25227c3f Mon Sep 17 00:00:00 2001 From: Iven Schlenther Date: Wed, 22 Jul 2026 11:27:38 +0200 Subject: [PATCH 51/84] - Add `sx={{ overflowWrap: "anywhere" }}` to Typography in BaseChart for improved text wrapping. - Adjust `alignItems` to `stretch` and add `maxWidth` (260) to Gauge components in Infrastructure Details page. - Update German translations for memory titles, temperatures, and labels in locales file. --- client/src/Components/design-elements/BaseChart.tsx | 1 + .../Pages/Infrastructure/Details/Components/Gauges.tsx | 5 ++++- client/src/locales/de.json | 8 ++++---- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/client/src/Components/design-elements/BaseChart.tsx b/client/src/Components/design-elements/BaseChart.tsx index c6d8298a7..ddfd82a3e 100644 --- a/client/src/Components/design-elements/BaseChart.tsx +++ b/client/src/Components/design-elements/BaseChart.tsx @@ -40,6 +40,7 @@ export const BaseChart = ({ {title} diff --git a/client/src/Pages/Infrastructure/Details/Components/Gauges.tsx b/client/src/Pages/Infrastructure/Details/Components/Gauges.tsx index 04c87ca95..e2b053aef 100644 --- a/client/src/Pages/Infrastructure/Details/Components/Gauges.tsx +++ b/client/src/Pages/Infrastructure/Details/Components/Gauges.tsx @@ -25,10 +25,11 @@ export const InfraDetailsGauges = ({ Date: Wed, 22 Jul 2026 11:33:06 +0200 Subject: [PATCH 52/84] Update English and German translations for "temp" label to "CPU temperature" in locales files. --- client/src/locales/de.json | 2 +- client/src/locales/en.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/client/src/locales/de.json b/client/src/locales/de.json index 29930bdfc..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": "Temperatur" + "temp": "CPU-Temperatur" } }, "gauges": { diff --git a/client/src/locales/en.json b/client/src/locales/en.json index 2adde28ec..d940f95fc 100644 --- a/client/src/locales/en.json +++ b/client/src/locales/en.json @@ -845,7 +845,7 @@ "netBytesRecv": "{{name}} - Bytes Received", "netBytesSent": "{{name}} - Bytes Sent", "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": "Temp" + "temp": "CPU temperature" } }, "gauges": { From d53785b768a69cb97dfd71e0d0f3b51ec36f83d0 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jul 2026 10:03:48 -0700 Subject: [PATCH 53/84] remove unused utilities, fix delete profile image issue --- server/src/api/controllers/controllerUtils.ts | 18 ------------------ server/src/api/validation/userValidation.ts | 3 ++- .../src/domain/users/user.repository.mongo.ts | 4 +--- server/src/utils/utils.ts | 17 ----------------- 4 files changed, 3 insertions(+), 39 deletions(-) delete mode 100755 server/src/utils/utils.ts diff --git a/server/src/api/controllers/controllerUtils.ts b/server/src/api/controllers/controllerUtils.ts index 45bf3fa9f..be3f46533 100755 --- a/server/src/api/controllers/controllerUtils.ts +++ b/server/src/api/controllers/controllerUtils.ts @@ -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/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/domain/users/user.repository.mongo.ts b/server/src/domain/users/user.repository.mongo.ts index b137c5f4c..af4b78b26 100644 --- a/server/src/domain/users/user.repository.mongo.ts +++ b/server/src/domain/users/user.repository.mongo.ts @@ -2,7 +2,6 @@ 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"; @@ -87,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/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]; -}; From c961f5292dbf3bf1173ee535ce6e7c1129fdbbbb Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jul 2026 10:05:36 -0700 Subject: [PATCH 54/84] fix signature, add tests --- server/src/domain/users/user.service.ts | 4 ++-- .../unit/validation/userValidation.test.ts | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/server/src/domain/users/user.service.ts b/server/src/domain/users/user.service.ts index 7bdb9677f..78a884a51 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 @@ -242,7 +242,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/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); + }); }); From 586e79efb284ac7a88574a11addbfb99b5dab520 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jul 2026 10:13:37 -0700 Subject: [PATCH 55/84] remove unused getters --- server/src/api/controllers/authController.ts | 4 ---- server/src/api/controllers/checkController.ts | 5 ----- server/src/api/controllers/diagnosticController.ts | 4 ---- server/src/api/controllers/geoCheckController.ts | 4 ---- server/src/api/controllers/inviteController.ts | 4 ---- server/src/api/controllers/logController.ts | 3 --- server/src/api/controllers/monitorController.ts | 4 ---- server/src/api/controllers/queueController.ts | 4 ---- server/src/api/controllers/settingsController.ts | 5 ----- server/src/api/controllers/statusPageController.ts | 5 ----- server/src/db/db.mongo.ts | 4 ---- .../src/domain/app-settings/app-settings.service.ts | 5 ----- server/src/domain/checks/check.service.ts | 4 ---- server/src/domain/diagnostics/diagnostic.service.ts | 4 ---- server/src/domain/geo-checks/geo-check.service.ts | 5 ----- server/src/domain/incidents/incident.service.ts | 4 ---- server/src/domain/invites/invite.service.ts | 4 ---- .../maintenance-windows/maintenance-window.service.ts | 4 ---- server/src/domain/monitors/monitor.service.ts | 6 ------ server/src/domain/users/user.service.ts | 4 ---- server/src/service/bufferService.ts | 4 ---- server/src/service/emailService.ts | 4 ---- server/src/service/globalPingService.ts | 5 ----- server/src/service/networkService.ts | 5 ----- server/src/service/statusService.ts | 4 ---- server/src/utils/logger.ts | 5 ----- server/src/worker/worker.db-queue.ts | 4 ---- server/src/worker/worker.helper.ts | 5 ----- server/src/worker/worker.interface.ts | 2 -- server/src/worker/worker.job-scheduler.ts | 3 --- server/test/unit/services/bufferService.test.ts | 11 ----------- server/test/unit/services/checkService.test.ts | 7 ------- server/test/unit/services/diagnosticService.test.ts | 11 ----------- server/test/unit/services/emailService.test.ts | 5 ----- server/test/unit/services/geoChecksService.test.ts | 11 ----------- server/test/unit/services/globalPingService.test.ts | 11 ----------- server/test/unit/services/incidentService.test.ts | 7 ------- server/test/unit/services/inviteService.test.ts | 11 ----------- .../unit/services/maintenanceWindowService.test.ts | 11 ----------- server/test/unit/services/monitorService.test.ts | 7 ------- server/test/unit/services/networkService.test.ts | 11 ----------- server/test/unit/services/settingsService.test.ts | 11 ----------- server/test/unit/services/statusService.test.ts | 7 ------- server/test/unit/services/userService.test.ts | 7 ------- server/test/unit/services/workerHelper.test.ts | 7 ------- 45 files changed, 262 deletions(-) diff --git a/server/src/api/controllers/authController.ts b/server/src/api/controllers/authController.ts index 1eb992529..59afd4cc8 100755 --- a/server/src/api/controllers/authController.ts +++ b/server/src/api/controllers/authController.ts @@ -50,10 +50,6 @@ class AuthController implements IAuthController { this.userService = userService; } - get serviceName() { - return AuthController.SERVICE_NAME; - } - registerUser = catchAsync(async (req: Request, res: Response) => { const newUser = req.body.user; const newUserToken = req.body.token; diff --git a/server/src/api/controllers/checkController.ts b/server/src/api/controllers/checkController.ts index ccc91921d..525a00080 100755 --- a/server/src/api/controllers/checkController.ts +++ b/server/src/api/controllers/checkController.ts @@ -14,7 +14,6 @@ import { requireTeamId } from "@/api/controllers/controllerUtils.js"; const SERVICE_NAME = "checkController"; export interface ICheckController { - serviceName: string; getChecksByMonitor: RequestHandler; getChecksByTeam: RequestHandler; getChecksSummaryByTeamId: RequestHandler; @@ -30,10 +29,6 @@ class CheckController implements ICheckController { 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); diff --git a/server/src/api/controllers/diagnosticController.ts b/server/src/api/controllers/diagnosticController.ts index 0864f7237..382b4349a 100755 --- a/server/src/api/controllers/diagnosticController.ts +++ b/server/src/api/controllers/diagnosticController.ts @@ -17,10 +17,6 @@ class DiagnosticController implements IDiagnosticController { this.diagnosticService = diagnosticService; } - get serviceName() { - return DiagnosticController.SERVICE_NAME; - } - getSystemStats = catchAsync(async (req: Request, res: Response) => { const diagnostics = await this.diagnosticService.getSystemStats(); return res.status(200).json({ diff --git a/server/src/api/controllers/geoCheckController.ts b/server/src/api/controllers/geoCheckController.ts index 81082b6ed..0c7f62260 100644 --- a/server/src/api/controllers/geoCheckController.ts +++ b/server/src/api/controllers/geoCheckController.ts @@ -17,10 +17,6 @@ class GeoCheckController implements IGeoCheckController { 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); diff --git a/server/src/api/controllers/inviteController.ts b/server/src/api/controllers/inviteController.ts index 0efc1e0a7..5af172dfb 100755 --- a/server/src/api/controllers/inviteController.ts +++ b/server/src/api/controllers/inviteController.ts @@ -18,10 +18,6 @@ class InviteController implements IInviteController { 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); diff --git a/server/src/api/controllers/logController.ts b/server/src/api/controllers/logController.ts index f0449bbf2..bbbeef2a9 100644 --- a/server/src/api/controllers/logController.ts +++ b/server/src/api/controllers/logController.ts @@ -14,9 +14,6 @@ class LogController { constructor(logger: ILogger) { this.logger = logger; } - get serviceName() { - return LogController.SERVICE_NAME; - } getLogs = catchAsync(async (req: Request, res: Response) => { const logs = this.logger.getLogs(); diff --git a/server/src/api/controllers/monitorController.ts b/server/src/api/controllers/monitorController.ts index 2ffb18802..f81e64f4a 100644 --- a/server/src/api/controllers/monitorController.ts +++ b/server/src/api/controllers/monitorController.ts @@ -57,10 +57,6 @@ 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); diff --git a/server/src/api/controllers/queueController.ts b/server/src/api/controllers/queueController.ts index 72c1463f9..09e0474e5 100755 --- a/server/src/api/controllers/queueController.ts +++ b/server/src/api/controllers/queueController.ts @@ -16,10 +16,6 @@ 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({ diff --git a/server/src/api/controllers/settingsController.ts b/server/src/api/controllers/settingsController.ts index 06085c3f8..a3237d92b 100644 --- a/server/src/api/controllers/settingsController.ts +++ b/server/src/api/controllers/settingsController.ts @@ -10,7 +10,6 @@ import { Settings } from "@/domain/app-settings/app-settings.type.js"; const SERVICE_NAME = "SettingsController"; export interface ISettingsController { - serviceName: string; getAppSettings: RequestHandler; updateAppSettings: RequestHandler; sendTestEmail: RequestHandler; @@ -25,10 +24,6 @@ class SettingsController implements ISettingsController { this.emailService = emailService; } - get serviceName() { - return SettingsController.SERVICE_NAME; - } - buildAppSettings = (dbSettings: Settings) => { const sanitizedSettings: Record = { ...dbSettings }; delete sanitizedSettings.version; diff --git a/server/src/api/controllers/statusPageController.ts b/server/src/api/controllers/statusPageController.ts index f75c5e7fb..5ba1cf527 100644 --- a/server/src/api/controllers/statusPageController.ts +++ b/server/src/api/controllers/statusPageController.ts @@ -16,7 +16,6 @@ import { resolveStatusPageDomainFromRequest } from "@/utils/statusPageDomain.js" const SERVICE_NAME = "statusPageController"; export interface IStatusPageController { - readonly serviceName: string; createStatusPage: RequestHandler; updateStatusPage: RequestHandler; getStatusPageByUrl: RequestHandler; @@ -32,10 +31,6 @@ class StatusPageController implements IStatusPageController { this.statusPageService = statusPageService; } - get serviceName() { - return StatusPageController.SERVICE_NAME; - } - createStatusPage = catchAsync(async (req: Request, res: Response) => { createStatusPageBodyValidation.parse(req.body); if (req.file) { 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/domain/app-settings/app-settings.service.ts b/server/src/domain/app-settings/app-settings.service.ts index 92a54702e..12f75d1c8 100755 --- a/server/src/domain/app-settings/app-settings.service.ts +++ b/server/src/domain/app-settings/app-settings.service.ts @@ -20,7 +20,6 @@ export type EnvConfig = { }; export interface ISettingsService { - readonly serviceName: string; loadSettings(): EnvConfig; getSettings(): EnvConfig; areStatusPageThemesEnabled(): boolean; @@ -57,10 +56,6 @@ export class SettingsService implements ISettingsService { this.settingsRepository = settingsRepository; } - get serviceName() { - return SettingsService.SERVICE_NAME; - } - loadSettings() { return this.settings; } diff --git a/server/src/domain/checks/check.service.ts b/server/src/domain/checks/check.service.ts index ca7fca457..f3c18ec91 100644 --- a/server/src/domain/checks/check.service.ts +++ b/server/src/domain/checks/check.service.ts @@ -50,10 +50,6 @@ export class CheckService implements ICheckService { this.checksRepository = checksRepository; } - get serviceName() { - return CheckService.SERVICE_NAME; - } - createChecks = async (checks: Check[]) => { return this.checksRepository.createChecks(checks); }; 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/geo-checks/geo-check.service.ts b/server/src/domain/geo-checks/geo-check.service.ts index 0ace565fc..040383811 100644 --- a/server/src/domain/geo-checks/geo-check.service.ts +++ b/server/src/domain/geo-checks/geo-check.service.ts @@ -11,7 +11,6 @@ import { AppError } from "@/utils/AppError.js"; const SERVICE_NAME = "GeoChecksService"; export interface IGeoChecksService { - readonly serviceName: string; buildGeoCheck(monitor: Monitor): Promise; createGeoChecks(geoChecks: GeoCheck[]): Promise; getGeoChecksByMonitor(args: { @@ -50,10 +49,6 @@ export class GeoChecksService implements IGeoChecksService { this.monitorsRepository = monitorsRepository; } - get serviceName() { - return GeoChecksService.SERVICE_NAME; - } - async buildGeoCheck(monitor: Monitor): Promise { try { if (!monitor.url) { diff --git a/server/src/domain/incidents/incident.service.ts b/server/src/domain/incidents/incident.service.ts index 311fad567..98f092088 100644 --- a/server/src/domain/incidents/incident.service.ts +++ b/server/src/domain/incidents/incident.service.ts @@ -57,10 +57,6 @@ export class IncidentService implements IIncidentService { this.notificationMessageBuilder = notificationMessageBuilder; } - get serviceName() { - return IncidentService.SERVICE_NAME; - } - handleIncident = async ( monitor: Monitor, code: number, 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.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/monitors/monitor.service.ts b/server/src/domain/monitors/monitor.service.ts index 66684c2ac..e7a79a161 100644 --- a/server/src/domain/monitors/monitor.service.ts +++ b/server/src/domain/monitors/monitor.service.ts @@ -31,8 +31,6 @@ const isUptimeChecksResult = (result: UptimeChecksResult | HardwareChecksResult supportsUptimeDetails(result.monitorType); export interface IMonitorService { - readonly serviceName: string; - // create createMonitor(teamId: string, userId: string, body: Partial): Promise; createMonitors(monitors: Array): Promise; @@ -135,10 +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)), diff --git a/server/src/domain/users/user.service.ts b/server/src/domain/users/user.service.ts index 78a884a51..f3db2f3b5 100644 --- a/server/src/domain/users/user.service.ts +++ b/server/src/domain/users/user.service.ts @@ -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 }); }; 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..f057e955a 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 { diff --git a/server/src/service/globalPingService.ts b/server/src/service/globalPingService.ts index cebd95408..4b0257df4 100644 --- a/server/src/service/globalPingService.ts +++ b/server/src/service/globalPingService.ts @@ -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/networkService.ts b/server/src/service/networkService.ts index 3470c6086..632a03141 100644 --- a/server/src/service/networkService.ts +++ b/server/src/service/networkService.ts @@ -8,7 +8,6 @@ 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..6435958cb 100755 --- a/server/src/service/statusService.ts +++ b/server/src/service/statusService.ts @@ -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/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/worker/worker.db-queue.ts b/server/src/worker/worker.db-queue.ts index 50e41a4f1..9420660c7 100644 --- a/server/src/worker/worker.db-queue.ts +++ b/server/src/worker/worker.db-queue.ts @@ -95,10 +95,6 @@ export class DBQueueWorker extends JobScheduler implements IQueueWorker { this.isDbConnected = dependencies.isDbConnected; } - get serviceName() { - return DBQueueWorker.SERVICE_NAME; - } - static async create(dependencies: DBQueueWorkerDependencies): Promise { const instance = new DBQueueWorker(dependencies); await instance.init(); 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..e70af5f35 100644 --- a/server/src/worker/worker.interface.ts +++ b/server/src/worker/worker.interface.ts @@ -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..0f04a39b7 100644 --- a/server/src/worker/worker.job-scheduler.ts +++ b/server/src/worker/worker.job-scheduler.ts @@ -26,9 +26,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 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/checkService.test.ts b/server/test/unit/services/checkService.test.ts index 104ccbaa2..587731951 100644 --- a/server/test/unit/services/checkService.test.ts +++ b/server/test/unit/services/checkService.test.ts @@ -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", () => { 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..c49d525b1 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 ────────────────────────────────────────────── diff --git a/server/test/unit/services/geoChecksService.test.ts b/server/test/unit/services/geoChecksService.test.ts index 8e50fa3e8..69bfff730 100644 --- a/server/test/unit/services/geoChecksService.test.ts +++ b/server/test/unit/services/geoChecksService.test.ts @@ -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..27b2736df 100644 --- a/server/test/unit/services/incidentService.test.ts +++ b/server/test/unit/services/incidentService.test.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/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 6a1cf8343..2b6f72d1d 100644 --- a/server/test/unit/services/monitorService.test.ts +++ b/server/test/unit/services/monitorService.test.ts @@ -111,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(); diff --git a/server/test/unit/services/networkService.test.ts b/server/test/unit/services/networkService.test.ts index 268b52e3a..0943b1f22 100644 --- a/server/test/unit/services/networkService.test.ts +++ b/server/test/unit/services/networkService.test.ts @@ -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/settingsService.test.ts b/server/test/unit/services/settingsService.test.ts index 43ea7fe18..e46472fac 100644 --- a/server/test/unit/services/settingsService.test.ts +++ b/server/test/unit/services/settingsService.test.ts @@ -56,17 +56,6 @@ const createService = (envOverrides?: Partial) => { // ── Tests ──────────────────────────────────────────────────────────────────── describe("SettingsService", () => { - describe("serviceName", () => { - it("returns SettingsService from static property", () => { - expect(SettingsService.SERVICE_NAME).toBe("SettingsService"); - }); - - it("returns SettingsService from instance getter", () => { - const { service } = createService(); - expect(service.serviceName).toBe("SettingsService"); - }); - }); - // ── loadSettings ──────────────────────────────────────────────────────── describe("loadSettings", () => { diff --git a/server/test/unit/services/statusService.test.ts b/server/test/unit/services/statusService.test.ts index 5712de301..4f4c0264a 100644 --- a/server/test/unit/services/statusService.test.ts +++ b/server/test/unit/services/statusService.test.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", () => { From 4949acac1cace44fe453d9d6630abbe74533f5ad Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jul 2026 10:26:00 -0700 Subject: [PATCH 56/84] remove unused email templates, remove duplicate constants --- .../app-settings/app-settings.service.ts | 5 -- .../queue-workers/queue-worker.model.ts | 3 +- server/src/index.ts | 2 +- server/src/service/emailService.ts | 1 - server/src/templates/addReview.mjml | 58 ---------------- server/src/templates/noIncidentsThisWeek.mjml | 66 ------------------- server/src/worker/worker.db-queue.ts | 5 +- server/src/worker/worker.job-scheduler.ts | 5 +- 8 files changed, 7 insertions(+), 138 deletions(-) delete mode 100755 server/src/templates/addReview.mjml delete mode 100755 server/src/templates/noIncidentsThisWeek.mjml diff --git a/server/src/domain/app-settings/app-settings.service.ts b/server/src/domain/app-settings/app-settings.service.ts index 12f75d1c8..2df2b7512 100755 --- a/server/src/domain/app-settings/app-settings.service.ts +++ b/server/src/domain/app-settings/app-settings.service.ts @@ -20,7 +20,6 @@ export type EnvConfig = { }; export interface ISettingsService { - loadSettings(): EnvConfig; getSettings(): EnvConfig; areStatusPageThemesEnabled(): boolean; getDBSettings(): Promise; @@ -56,10 +55,6 @@ export class SettingsService implements ISettingsService { this.settingsRepository = settingsRepository; } - loadSettings() { - return this.settings; - } - getSettings() { return this.settings; } 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/index.ts b/server/src/index.ts index b5e870ded..68ef00a1f 100755 --- a/server/src/index.ts +++ b/server/src/index.ts @@ -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({ diff --git a/server/src/service/emailService.ts b/server/src/service/emailService.ts index f057e955a..26b89f84b 100755 --- a/server/src/service/emailService.ts +++ b/server/src/service/emailService.ts @@ -75,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/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/worker/worker.db-queue.ts b/server/src/worker/worker.db-queue.ts index 9420660c7..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; diff --git a/server/src/worker/worker.job-scheduler.ts b/server/src/worker/worker.job-scheduler.ts index 0f04a39b7..433e69955 100644 --- a/server/src/worker/worker.job-scheduler.ts +++ b/server/src/worker/worker.job-scheduler.ts @@ -5,13 +5,12 @@ import { Monitor, supportsGeoCheck } from "@/domain/monitors/monitor.types.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; From 80d4e818f1beb8e35d840194bcd473a8ea4d6fea Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jul 2026 10:36:16 -0700 Subject: [PATCH 57/84] remove unused static vars --- server/src/api/controllers/authController.ts | 4 ---- server/src/api/controllers/checkController.ts | 4 ---- .../api/controllers/diagnosticController.ts | 4 ---- .../src/api/controllers/geoCheckController.ts | 4 ---- .../src/api/controllers/inviteController.ts | 2 -- server/src/api/controllers/logController.ts | 3 --- .../src/api/controllers/monitorController.ts | 4 ---- server/src/api/controllers/queueController.ts | 3 --- .../src/api/controllers/settingsController.ts | 3 --- .../api/controllers/statusPageController.ts | 3 --- .../test/unit/services/emailService.test.ts | 8 ++++---- .../unit/services/settingsService.test.ts | 20 +++++-------------- 12 files changed, 9 insertions(+), 53 deletions(-) diff --git a/server/src/api/controllers/authController.ts b/server/src/api/controllers/authController.ts index 59afd4cc8..ab8d1fc61 100755 --- a/server/src/api/controllers/authController.ts +++ b/server/src/api/controllers/authController.ts @@ -22,8 +22,6 @@ import { } from "@/api/validation/userValidation.js"; import { IUserService } from "@/domain/users/user.service.js"; -const SERVICE_NAME = "authController"; - export interface IAuthController { registerUser: RequestHandler; createUser: RequestHandler; @@ -42,8 +40,6 @@ export interface IAuthController { } class AuthController implements IAuthController { - static SERVICE_NAME = SERVICE_NAME; - private userService: IUserService; constructor(userService: IUserService) { diff --git a/server/src/api/controllers/checkController.ts b/server/src/api/controllers/checkController.ts index 525a00080..7b69c7047 100755 --- a/server/src/api/controllers/checkController.ts +++ b/server/src/api/controllers/checkController.ts @@ -11,8 +11,6 @@ import { import { ICheckService } from "@/domain/checks/check.service.js"; import { requireTeamId } from "@/api/controllers/controllerUtils.js"; -const SERVICE_NAME = "checkController"; - export interface ICheckController { getChecksByMonitor: RequestHandler; getChecksByTeam: RequestHandler; @@ -22,8 +20,6 @@ export interface ICheckController { } class CheckController implements ICheckController { - static SERVICE_NAME = SERVICE_NAME; - private checkService: ICheckService; constructor(checkService: ICheckService) { this.checkService = checkService; diff --git a/server/src/api/controllers/diagnosticController.ts b/server/src/api/controllers/diagnosticController.ts index 382b4349a..9d70304d4 100755 --- a/server/src/api/controllers/diagnosticController.ts +++ b/server/src/api/controllers/diagnosticController.ts @@ -2,15 +2,11 @@ import { IDiagnosticService } from "@/domain/diagnostics/diagnostic.service.js"; import { Request, Response, RequestHandler } from "express"; import { catchAsync } from "@/utils/catchAsync.js"; -const SERVICE_NAME = "diagnosticController"; - export interface IDiagnosticController { getSystemStats: RequestHandler; } class DiagnosticController implements IDiagnosticController { - static SERVICE_NAME = SERVICE_NAME; - private diagnosticService: IDiagnosticService; constructor(diagnosticService: IDiagnosticService) { diff --git a/server/src/api/controllers/geoCheckController.ts b/server/src/api/controllers/geoCheckController.ts index 0c7f62260..24caef382 100644 --- a/server/src/api/controllers/geoCheckController.ts +++ b/server/src/api/controllers/geoCheckController.ts @@ -4,14 +4,10 @@ import { getChecksParamValidation, getChecksQueryValidation } from "@/api/valida import type { IGeoChecksService } from "@/domain/geo-checks/geo-check.service.js"; import { requireTeamId } from "./controllerUtils.js"; -const SERVICE_NAME = "geoCheckController"; - export interface IGeoCheckController { getGeoChecksByMonitor: RequestHandler; } class GeoCheckController implements IGeoCheckController { - static SERVICE_NAME = SERVICE_NAME; - private geoChecksService: IGeoChecksService; constructor(geoChecksService: IGeoChecksService) { this.geoChecksService = geoChecksService; diff --git a/server/src/api/controllers/inviteController.ts b/server/src/api/controllers/inviteController.ts index 5af172dfb..511145b7b 100755 --- a/server/src/api/controllers/inviteController.ts +++ b/server/src/api/controllers/inviteController.ts @@ -3,7 +3,6 @@ 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: RequestHandler; @@ -12,7 +11,6 @@ export interface IInviteController { } class InviteController implements IInviteController { - static SERVICE_NAME = SERVICE_NAME; private inviteService: IInviteService; constructor(inviteService: IInviteService) { this.inviteService = inviteService; diff --git a/server/src/api/controllers/logController.ts b/server/src/api/controllers/logController.ts index bbbeef2a9..316d03788 100644 --- a/server/src/api/controllers/logController.ts +++ b/server/src/api/controllers/logController.ts @@ -2,14 +2,11 @@ import { ILogger } from "@/utils/logger.js"; import { Request, Response, RequestHandler } from "express"; import { catchAsync } from "@/utils/catchAsync.js"; -const SERVICE_NAME = "LogController"; - export interface ILogController { getLogs: RequestHandler; } class LogController { - static SERVICE_NAME = SERVICE_NAME; private logger: ILogger; constructor(logger: ILogger) { this.logger = logger; diff --git a/server/src/api/controllers/monitorController.ts b/server/src/api/controllers/monitorController.ts index f81e64f4a..664a78f45 100644 --- a/server/src/api/controllers/monitorController.ts +++ b/server/src/api/controllers/monitorController.ts @@ -23,8 +23,6 @@ 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: RequestHandler; getUptimeDetailsById: RequestHandler; @@ -47,8 +45,6 @@ export interface IMonitorController { updateNotifications: RequestHandler; } class MonitorController implements IMonitorController { - static SERVICE_NAME = SERVICE_NAME; - private monitorService: IMonitorService; private notificationsService: INotificationsService; diff --git a/server/src/api/controllers/queueController.ts b/server/src/api/controllers/queueController.ts index 09e0474e5..03204167c 100755 --- a/server/src/api/controllers/queueController.ts +++ b/server/src/api/controllers/queueController.ts @@ -3,8 +3,6 @@ import { getQueueJobsQueryValidation } from "@/api/validation/queueValidation.js import { Request, Response, RequestHandler } from "express"; import { catchAsync } from "@/utils/catchAsync.js"; -const SERVICE_NAME = "JobQueueController"; - export interface IJobQueueController { getMetrics: RequestHandler; getJobs: RequestHandler; @@ -13,7 +11,6 @@ export interface IJobQueueController { } class JobQueueController implements IJobQueueController { - static SERVICE_NAME = SERVICE_NAME; constructor(private scheduler: IJobScheduler) {} getMetrics = catchAsync(async (req: Request, res: Response) => { diff --git a/server/src/api/controllers/settingsController.ts b/server/src/api/controllers/settingsController.ts index a3237d92b..3cb032e93 100644 --- a/server/src/api/controllers/settingsController.ts +++ b/server/src/api/controllers/settingsController.ts @@ -7,8 +7,6 @@ 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 { getAppSettings: RequestHandler; updateAppSettings: RequestHandler; @@ -16,7 +14,6 @@ export interface ISettingsController { } class SettingsController implements ISettingsController { - static SERVICE_NAME = SERVICE_NAME; private settingsService: ISettingsService; private emailService: IEmailService; constructor(settingsService: ISettingsService, emailService: IEmailService) { diff --git a/server/src/api/controllers/statusPageController.ts b/server/src/api/controllers/statusPageController.ts index 5ba1cf527..038ddb423 100644 --- a/server/src/api/controllers/statusPageController.ts +++ b/server/src/api/controllers/statusPageController.ts @@ -13,8 +13,6 @@ 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 { createStatusPage: RequestHandler; updateStatusPage: RequestHandler; @@ -25,7 +23,6 @@ export interface IStatusPageController { } class StatusPageController implements IStatusPageController { - static SERVICE_NAME = SERVICE_NAME; private statusPageService: IStatusPageService; constructor(statusPageService: IStatusPageService) { this.statusPageService = statusPageService; diff --git a/server/test/unit/services/emailService.test.ts b/server/test/unit/services/emailService.test.ts index c49d525b1..971c712da 100644 --- a/server/test/unit/services/emailService.test.ts +++ b/server/test/unit/services/emailService.test.ts @@ -90,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", () => { @@ -141,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/settingsService.test.ts b/server/test/unit/services/settingsService.test.ts index e46472fac..063a64f6d 100644 --- a/server/test/unit/services/settingsService.test.ts +++ b/server/test/unit/services/settingsService.test.ts @@ -56,12 +56,12 @@ const createService = (envOverrides?: Partial) => { // ── Tests ──────────────────────────────────────────────────────────────────── describe("SettingsService", () => { - // ── loadSettings ──────────────────────────────────────────────────────── + // ── getSettings ───────────────────────────────────────────────────────── - 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", @@ -82,7 +82,7 @@ describe("SettingsService", () => { CLIENT_CONFIG_LOG_LEVEL: "warn", }); - expect(service.loadSettings().clientConfig).toEqual({ + expect(service.getSettings().clientConfig).toEqual({ apiBaseUrl: "https://api.example.com/api/v1", logLevel: "warn", }); @@ -90,23 +90,13 @@ describe("SettingsService", () => { 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", () => { From e1c89cd0939287d685468ee2b0a6ca8082061c79 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 22 Jul 2026 11:00:59 -0700 Subject: [PATCH 58/84] dedupe normalization --- server/src/utils/dataUtils.ts | 81 ++++-------- server/test/unit/utils/dataUtils.test.ts | 159 +++++++++++++++++++++++ 2 files changed, 186 insertions(+), 54 deletions(-) create mode 100644 server/test/unit/utils/dataUtils.test.ts diff --git a/server/src/utils/dataUtils.ts b/server/src/utils/dataUtils.ts index 7b46619e8..b264f85b1 100755 --- a/server/src/utils/dataUtils.ts +++ b/server/src/utils/dataUtils.ts @@ -20,72 +20,45 @@ export const getDateForRange = (dateRange: string): Date | undefined => { } }; -const calculatePercentile = (arr: T[], percentile: number): number => { - const sorted = arr.slice().sort((a, b) => a.responseTime - b.responseTime); +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]!.responseTime; - return sorted[lower]!.responseTime * (1 - weight) + sorted[upper]!.responseTime * weight; + if (upper >= sorted.length) return value(sorted[lower]!); + return value(sorted[lower]!) * (1 - weight) + value(sorted[upper]!) * weight; }; -const calculatePercentileUptimeDetails = (arr: GroupedCheck[], percentile: number): number => { - const sorted = arr.slice().sort((a, b) => a.avgResponseTime - b.avgResponseTime); - 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; +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 }; - }); + 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), + })); }; 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.avgResponseTime === "number" ? check.avgResponseTime : 0); + if (checks.length <= 1) { + return checks.map((check) => ({ ...check, originalAvgResponseTime: original(check) })); } + const min = percentileBy(checks, 0, (c) => c.avgResponseTime); + const max = percentileBy(checks, 95, (c) => c.avgResponseTime); + return checks.map((check) => ({ + ...check, + avgResponseTime: rescale(check.avgResponseTime, min, max, rangeMin, rangeMax), + originalAvgResponseTime: original(check), + })); }; diff --git a/server/test/unit/utils/dataUtils.test.ts b/server/test/unit/utils/dataUtils.test.ts new file mode 100644 index 000000000..f4893fe03 --- /dev/null +++ b/server/test/unit/utils/dataUtils.test.ts @@ -0,0 +1,159 @@ +import { describe, expect, it } from "@jest/globals"; +import { getDateForRange, NormalizeData, NormalizeDataUptimeDetails } from "../../../src/utils/dataUtils.ts"; +import type { GroupedCheck } from "../../../src/domain/checks/check.type.ts"; + +// ── Helpers ────────────────────────────────────────────────────────────────── + +const makeCheck = (responseTime: number, overrides?: Record) => ({ + id: `check-${responseTime}`, + status: true, + responseTime, + ...overrides, +}); + +const makeGrouped = (avgResponseTime: number, overrides?: Partial): GroupedCheck => ({ + bucketDate: `bucket-${avgResponseTime}`, + avgResponseTime, + totalChecks: 5, + ...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 string); + const after = Date.now(); + + expect(result).toBeInstanceOf(Date); + expect(result!.getTime()).toBeGreaterThanOrEqual(before - offsetMs); + expect(result!.getTime()).toBeLessThanOrEqual(after - offsetMs); + }); + + it("returns undefined for 'all'", () => { + expect(getDateForRange("all")).toBeUndefined(); + }); + + it("returns undefined for unknown ranges", () => { + expect(getDateForRange("fortnight")).toBeUndefined(); + }); +}); + +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); + }); + }); +}); + +describe("NormalizeDataUptimeDetails", () => { + it("rescales avgResponseTime into [rangeMin, rangeMax] anchored to the p0–p95 window", () => { + const checks = [100, 200, 300, 400, 500].map((avg) => makeGrouped(avg)); + + const result = NormalizeDataUptimeDetails(checks, 10, 100); + + expect(result[0]!.avgResponseTime).toBe(10); + expect(result[1]!.avgResponseTime).toBeCloseTo(33.6842, 4); + expect(result[4]!.avgResponseTime).toBe(100); + }); + + it("preserves the raw value in originalAvgResponseTime and spreads other fields", () => { + const checks = [makeGrouped(100), makeGrouped(500)]; + + const result = NormalizeDataUptimeDetails(checks, 10, 100); + + expect(result[0]!.originalAvgResponseTime).toBe(100); + expect(result[1]!.originalAvgResponseTime).toBe(500); + expect(result[0]).toMatchObject({ bucketDate: "bucket-100", totalChecks: 5 }); + }); + + it("passes a single bucket through, only adding originalAvgResponseTime", () => { + const result = NormalizeDataUptimeDetails([makeGrouped(250)], 10, 100); + + expect(result).toEqual([{ bucketDate: "bucket-250", avgResponseTime: 250, totalChecks: 5, originalAvgResponseTime: 250 }]); + }); + + it("returns an empty array for empty input", () => { + expect(NormalizeDataUptimeDetails([], 10, 100)).toEqual([]); + }); + + it("records originalAvgResponseTime as 0 for a null avgResponseTime ($avg over an empty bucket)", () => { + const result = NormalizeDataUptimeDetails([makeGrouped(null as unknown as number)], 10, 100); + + expect(result[0]!.originalAvgResponseTime).toBe(0); + }); + + it("produces NaN when all values are equal (division by zero — known edge case)", () => { + const result = NormalizeDataUptimeDetails([makeGrouped(50), makeGrouped(50)], 10, 100); + + result.forEach((check) => { + expect(check.avgResponseTime).toBeNaN(); + expect(check.originalAvgResponseTime).toBe(50); + }); + }); +}); From 4a90ab8e781f010c5088490a07673e2ab53bcb40 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 23 Jul 2026 09:36:06 -0700 Subject: [PATCH 59/84] extract pagination --- .../monitors/monitor.repository.mongo.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/server/src/domain/monitors/monitor.repository.mongo.ts b/server/src/domain/monitors/monitor.repository.mongo.ts index 96db34c15..d9e35855c 100644 --- a/server/src/domain/monitors/monitor.repository.mongo.ts +++ b/server/src/domain/monitors/monitor.repository.mongo.ts @@ -91,27 +91,26 @@ class MongoMonitorsRepository implements IMonitorsRepository { { $project: { stats: 0 } }, ]; - findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise => { + 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 = { [field]: order === "asc" ? 1 : -1 } as const; - const skip = Math.max(page, 0) * rowsPerPage; - const documents = await MonitorModel.find(query).sort(sort).skip(skip).limit(rowsPerPage); + 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, field = "createdAt", order = "desc" } = config ?? {}; - + const { sort, skip, limit } = this.pageOptions(config); const query = this.queryBuilder(config, teamId); - const sort = { [field]: order === "asc" ? 1 : -1 } as const; - const skip = Math.max(page, 0) * rowsPerPage; - const pipeline: PipelineStage[] = [ { $match: query }, { $sort: sort }, - ...(rowsPerPage ? [{ $skip: skip }, { $limit: rowsPerPage }] : []), + ...(limit ? [{ $skip: skip }, { $limit: limit }] : []), ...this.uptimeStatsLookupStages, ]; From 3af00281c48a26a5716c751a350106bbcc9e3d60 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 23 Jul 2026 10:25:24 -0700 Subject: [PATCH 60/84] extract date range calculation for conisistency --- .../checks/check.repository.interface.ts | 11 ++-- .../domain/checks/check.repository.mongo.ts | 39 +++++------ server/src/domain/checks/check.service.ts | 13 ++-- .../geo-check.repository.interface.ts | 11 +--- .../geo-checks/geo-check.repository.mongo.ts | 29 ++++----- .../domain/geo-checks/geo-check.service.ts | 5 +- .../src/domain/incidents/incident.service.ts | 5 +- server/src/domain/monitors/monitor.service.ts | 65 ++++--------------- server/src/utils/dataUtils.ts | 19 +++++- 9 files changed, 78 insertions(+), 119 deletions(-) diff --git a/server/src/domain/checks/check.repository.interface.ts b/server/src/domain/checks/check.repository.interface.ts index 95c0ade80..8c921c843 100644 --- a/server/src/domain/checks/check.repository.interface.ts +++ b/server/src/domain/checks/check.repository.interface.ts @@ -8,6 +8,7 @@ import type { } from "@/domain/checks/check.type.js"; import type { MonitorType } from "@/domain/monitors/monitor.types.js"; import type { LatestChecksMap } from "@/domain/checks/check.repository.mongo.js"; +import { DateRange } from "@/types/query.js"; export interface IChecksRepository { // create @@ -19,7 +20,7 @@ export interface IChecksRepository { findByMonitorId( monitorId: string, sortOrder: string, - dateRange: string, + dateRange: DateRange, filter: string | undefined, page: number, rowsPerPage: number, @@ -27,7 +28,7 @@ export interface IChecksRepository { ): Promise; findByTeamId( sortOrder: string, - dateRange: string, + dateRange: DateRange, filter: string | undefined, page: number, rowsPerPage: number, @@ -36,12 +37,10 @@ export interface IChecksRepository { 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 1433c950a..4bfb65dd4 100644 --- a/server/src/domain/checks/check.repository.mongo.ts +++ b/server/src/domain/checks/check.repository.mongo.ts @@ -15,11 +15,12 @@ import type { import type { MonitorType } from "@/domain/monitors/monitor.types.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 { DateRange } from "@/types/query.js"; const SERVICE_NAME = "StatusService"; @@ -207,7 +208,7 @@ class MongoChecksRepository implements IChecksRepository { findByMonitorId = async ( monitorId: string, sortOrder: string, - dateRange: string, + dateRange: DateRange, filter: string | undefined, page: number, rowsPerPage: number, @@ -217,11 +218,7 @@ class MongoChecksRepository implements IChecksRepository { const matchStage: Record = { "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), ...(typeof status !== "undefined" && { status }), - ...(getDateForRange(dateRange) && { - createdAt: { - $gte: getDateForRange(dateRange), - }, - }), + createdAt: { $gte: getDateForRange(dateRange) }, }; if (filter !== undefined) { @@ -265,14 +262,10 @@ 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, filter: string, page: number, rowsPerPage: number, teamId: string) => { const matchStage: Record = { "metadata.teamId": new mongoose.Types.ObjectId(teamId), - ...(getDateForRange(dateRange) && { - createdAt: { - $gte: getDateForRange(dateRange), - }, - }), + createdAt: { $gte: getDateForRange(dateRange) }, }; // Add filter to match stage if (filter !== undefined) { @@ -341,25 +334,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([ diff --git a/server/src/domain/checks/check.service.ts b/server/src/domain/checks/check.service.ts index f3c18ec91..b577658bd 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 { DateRange } from "@/types/query.js"; const SERVICE_NAME = "checkService"; @@ -18,7 +19,7 @@ export interface ICheckService { monitorId: string; teamId: string; sortOrder: string; - dateRange: string; + dateRange: DateRange; page: number; rowsPerPage: number; filter?: string; @@ -27,12 +28,12 @@ export interface ICheckService { getChecksByTeam(params: { teamId: string; sortOrder: string; - dateRange: string; + dateRange: DateRange; page: number; rowsPerPage: number; filter?: string; }): 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; @@ -164,7 +165,7 @@ export class CheckService implements ICheckService { monitorId: string; teamId: string; sortOrder: string; - dateRange: string; + dateRange: DateRange; page: number; rowsPerPage: number; status?: boolean; @@ -198,7 +199,7 @@ export class CheckService implements ICheckService { }: { teamId: string; sortOrder: string; - dateRange: string; + dateRange: DateRange; page: number; rowsPerPage: number; filter?: string; @@ -210,7 +211,7 @@ export class CheckService implements ICheckService { 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/geo-checks/geo-check.repository.interface.ts b/server/src/domain/geo-checks/geo-check.repository.interface.ts index 2e6f33e33..a2cb7f84e 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,13 @@ 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 e73056e95..d1c56eb1c 100644 --- a/server/src/domain/geo-checks/geo-check.repository.mongo.ts +++ b/server/src/domain/geo-checks/geo-check.repository.mongo.ts @@ -3,9 +3,10 @@ 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 { getDateForRange, getDateFormat } from "@/utils/dataUtils.js"; import { ILogger } from "@/utils/logger.js"; import { toStringId, toDateString } from "@/utils/mongoMappers.js"; +import { DateRange } from "@/types/query.js"; const SERVICE_NAME = "GeoChecksRepository"; @@ -90,7 +91,7 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { findByMonitorId = async ( monitorId: string, sortOrder: string, - dateRange: string, + dateRange: DateRange, page: number, rowsPerPage: number, continents?: GeoContinent[] @@ -98,11 +99,7 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { try { const matchStage: Record = { "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), - ...(getDateForRange(dateRange) && { - createdAt: { - $gte: getDateForRange(dateRange), - }, - }), + createdAt: { $gte: getDateForRange(dateRange) }, }; const convertedSortOrder = sortOrder === "asc" ? 1 : -1; @@ -214,13 +211,11 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { } }; - findGroupedByMonitorIdAndDateRange = async ( - monitorId: string, - startDate: Date, - endDate: Date, - dateFormat: string, - continents?: GeoContinent[] - ): Promise => { + findGroupedByMonitorIdAndDateRange = async (monitorId: string, dateRange: DateRange, continents?: GeoContinent[]): Promise => { + const start = getDateForRange(dateRange); + const dateString = getDateFormat(dateRange); + + const end = new Date(); try { const pipeline: PipelineStage[] = [ // Match geo checks for this monitor in date range @@ -228,8 +223,8 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { $match: { "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), createdAt: { - $gte: startDate, - $lte: endDate, + $gte: start, + $lte: end, }, }, }, @@ -253,7 +248,7 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { _id: { bucketDate: { $dateToString: { - format: dateFormat, + format: dateString, date: "$createdAt", }, }, diff --git a/server/src/domain/geo-checks/geo-check.service.ts b/server/src/domain/geo-checks/geo-check.service.ts index 040383811..26d347228 100644 --- a/server/src/domain/geo-checks/geo-check.service.ts +++ b/server/src/domain/geo-checks/geo-check.service.ts @@ -7,6 +7,7 @@ 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"; @@ -17,7 +18,7 @@ export interface IGeoChecksService { monitorId: string; teamId: string; sortOrder: string; - dateRange: string; + dateRange: DateRange; page?: number; rowsPerPage?: number; continent?: GeoContinent | GeoContinent[]; @@ -155,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/incidents/incident.service.ts b/server/src/domain/incidents/incident.service.ts index 98f092088..9b8fbf25e 100644 --- a/server/src/domain/incidents/incident.service.ts +++ b/server/src/domain/incidents/incident.service.ts @@ -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, @@ -174,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/monitors/monitor.service.ts b/server/src/domain/monitors/monitor.service.ts index e7a79a161..e6d339b80 100644 --- a/server/src/domain/monitors/monitor.service.ts +++ b/server/src/domain/monitors/monitor.service.ts @@ -23,9 +23,9 @@ 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); @@ -37,13 +37,13 @@ export interface IMonitorService { 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; @@ -133,31 +133,6 @@ export class MonitorService implements IMonitorService { this.incidentsRepository = incidentsRepository; } - 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) { @@ -200,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); @@ -237,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) { @@ -247,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, }); @@ -279,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) { @@ -289,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, }); @@ -318,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); @@ -330,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 }; }; diff --git a/server/src/utils/dataUtils.ts b/server/src/utils/dataUtils.ts index b264f85b1..0382abc57 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 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,12 +15,24 @@ 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); } }; +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 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); From b4e42bfd68e9f1873f8749416d4fa711861baaaa Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 23 Jul 2026 10:25:51 -0700 Subject: [PATCH 61/84] add type check script --- server/package.json | 1 + 1 file changed, 1 insertion(+) 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 .", From f50ba1bf23ffdf0e732eb5078044e814c85915f8 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 23 Jul 2026 10:26:01 -0700 Subject: [PATCH 62/84] update tests --- server/test/unit/utils/dataUtils.test.ts | 32 ++++++++++++++++++------ 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/server/test/unit/utils/dataUtils.test.ts b/server/test/unit/utils/dataUtils.test.ts index f4893fe03..01f102cca 100644 --- a/server/test/unit/utils/dataUtils.test.ts +++ b/server/test/unit/utils/dataUtils.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from "@jest/globals"; -import { getDateForRange, NormalizeData, NormalizeDataUptimeDetails } from "../../../src/utils/dataUtils.ts"; +import { getDateForRange, getDateFormat, NormalizeData, NormalizeDataUptimeDetails } from "../../../src/utils/dataUtils.ts"; import type { GroupedCheck } from "../../../src/domain/checks/check.type.ts"; +import { DateRanges, type DateRange } from "../../../src/types/query.ts"; // ── Helpers ────────────────────────────────────────────────────────────────── @@ -29,20 +30,35 @@ describe("getDateForRange", () => { ["month", 30 * 24 * 60 * 60 * 1000], ])("returns now minus the %s window", (range, offsetMs) => { const before = Date.now(); - const result = getDateForRange(range as string); + 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); + expect(result.getTime()).toBeGreaterThanOrEqual(before - offsetMs); + expect(result.getTime()).toBeLessThanOrEqual(after - offsetMs); }); - it("returns undefined for 'all'", () => { - expect(getDateForRange("all")).toBeUndefined(); + 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("returns undefined for unknown ranges", () => { - expect(getDateForRange("fortnight")).toBeUndefined(); + it("has a format for every member of DateRanges", () => { + for (const range of DateRanges) { + expect(getDateFormat(range)).toMatch(/^%Y-%m-%d/); + } }); }); From 61e442493a0d655f2ec8be41a3299dff0bb66ae2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Thu, 23 Jul 2026 10:49:59 -0700 Subject: [PATCH 63/84] extact monitor children deletion to helper --- server/src/domain/monitors/monitor.service.ts | 29 ++++++++++--------- .../test/unit/services/monitorService.test.ts | 28 ++++++++++++++---- 2 files changed, 38 insertions(+), 19 deletions(-) diff --git a/server/src/domain/monitors/monitor.service.ts b/server/src/domain/monitors/monitor.service.ts index e6d339b80..711fc9b9b 100644 --- a/server/src/domain/monitors/monitor.service.ts +++ b/server/src/domain/monitors/monitor.service.ts @@ -451,15 +451,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}`, @@ -491,6 +483,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; }; @@ -500,12 +505,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); - await this.incidentsRepository.deleteByMonitorId(monitor.id, teamId); } catch (error: unknown) { this.logger.warn({ message: `Error deleting associated records for monitor ${monitor.id} with name ${monitor.name}`, diff --git a/server/test/unit/services/monitorService.test.ts b/server/test/unit/services/monitorService.test.ts index 2b6f72d1d..085b3322f 100644 --- a/server/test/unit/services/monitorService.test.ts +++ b/server/test/unit/services/monitorService.test.ts @@ -1443,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, @@ -1456,6 +1459,7 @@ describe("MonitorService", () => { monitorStatsRepository, statusPagesRepository, geoChecksRepository, + incidentsRepository, }); const result = await service.deleteAllMonitors({ teamId: TEAM_ID }); @@ -1466,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, }); @@ -1494,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"); @@ -1523,7 +1541,7 @@ describe("MonitorService", () => { geoChecksRepository, monitorStatsRepository, statusPagesRepository, - incidentsRepository: createIncidentsRepositoryMock(), + incidentsRepository, }); const result = await service.deleteAllMonitors({ teamId: TEAM_ID }); From 25d81f42380b5a6362593d56ceeb045069474346 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 24 Jul 2026 10:43:56 -0700 Subject: [PATCH 64/84] remove dead findByMonitorIdAndDateRange method --- .../geo-checks/geo-check.repository.mongo.ts | 24 ------------------- 1 file changed, 24 deletions(-) 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 d1c56eb1c..5f4067ec7 100644 --- a/server/src/domain/geo-checks/geo-check.repository.mongo.ts +++ b/server/src/domain/geo-checks/geo-check.repository.mongo.ts @@ -187,30 +187,6 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { } }; - 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({ 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, dateRange: DateRange, continents?: GeoContinent[]): Promise => { const start = getDateForRange(dateRange); const dateString = getDateFormat(dateRange); From 86eeda3f0efa2de62a71f2454dacadd03ef41534 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 24 Jul 2026 10:49:39 -0700 Subject: [PATCH 65/84] replace error instances with AppError --- server/src/domain/checks/check.repository.mongo.ts | 8 +++++++- .../domain/geo-checks/geo-check.repository.interface.ts | 1 - server/src/domain/users/user.repository.mongo.ts | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/server/src/domain/checks/check.repository.mongo.ts b/server/src/domain/checks/check.repository.mongo.ts index 4bfb65dd4..f07f7b3ee 100644 --- a/server/src/domain/checks/check.repository.mongo.ts +++ b/server/src/domain/checks/check.repository.mongo.ts @@ -21,6 +21,7 @@ import { toStringId, toDateString } from "@/utils/mongoMappers.js"; import { getHardwareUpChecks, getHardwareStats, getHardwareTotalChecks } from "@/domain/checks/check.hardware.aggregations.js"; import { DateRange } from "@/types/query.js"; +import { AppError } from "@/utils/AppError.js"; const SERVICE_NAME = "StatusService"; @@ -181,7 +182,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(), 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 a2cb7f84e..a9a81f57c 100644 --- a/server/src/domain/geo-checks/geo-check.repository.interface.ts +++ b/server/src/domain/geo-checks/geo-check.repository.interface.ts @@ -22,7 +22,6 @@ export interface IGeoChecksRepository { rowsPerPage: number, continents?: GeoContinent[] ): Promise; - findByMonitorIdAndDateRange(monitorId: string, startDate: Date, endDate: Date): Promise; findGroupedByMonitorIdAndDateRange(monitorId: string, DateRange: DateRange, continents?: GeoContinent[]): Promise; deleteByMonitorId(monitorId: string): Promise; deleteByTeamId(teamId: string): Promise; diff --git a/server/src/domain/users/user.repository.mongo.ts b/server/src/domain/users/user.repository.mongo.ts index af4b78b26..927df23ed 100644 --- a/server/src/domain/users/user.repository.mongo.ts +++ b/server/src/domain/users/user.repository.mongo.ts @@ -71,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); From c021d04f5372ffa754c425cab7a7e6753d0fd6e5 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 24 Jul 2026 11:09:53 -0700 Subject: [PATCH 66/84] extract swtich --- .../checks/check.repository.interface.ts | 10 +-- .../domain/checks/check.repository.mongo.ts | 77 ++++++------------- server/src/domain/checks/check.service.ts | 16 ++-- server/src/service/network/DNSProvider.ts | 3 +- server/src/service/network/DockerProvider.ts | 4 +- server/src/service/network/GameProvider.ts | 2 +- server/src/service/network/HttpProvider.ts | 3 +- server/src/service/network/PortProvider.ts | 3 +- .../src/service/network/WebSocketProvider.ts | 3 +- server/src/service/network/utils.ts | 3 - server/src/service/networkService.ts | 2 +- server/src/types/network.ts | 2 + .../test/unit/services/checkService.test.ts | 8 +- 13 files changed, 56 insertions(+), 80 deletions(-) diff --git a/server/src/domain/checks/check.repository.interface.ts b/server/src/domain/checks/check.repository.interface.ts index 8c921c843..2db5df712 100644 --- a/server/src/domain/checks/check.repository.interface.ts +++ b/server/src/domain/checks/check.repository.interface.ts @@ -8,7 +8,7 @@ import type { } from "@/domain/checks/check.type.js"; import type { MonitorType } from "@/domain/monitors/monitor.types.js"; import type { LatestChecksMap } from "@/domain/checks/check.repository.mongo.js"; -import { DateRange } from "@/types/query.js"; +import { CheckFilter, DateRange } from "@/types/query.js"; export interface IChecksRepository { // create @@ -21,18 +21,18 @@ export interface IChecksRepository { monitorId: string, sortOrder: string, dateRange: DateRange, - filter: string | undefined, page: number, rowsPerPage: number, - status: boolean | undefined + status: boolean | undefined, + filter?: CheckFilter ): Promise; findByTeamId( sortOrder: string, dateRange: DateRange, - filter: string | undefined, page: number, rowsPerPage: number, - teamId: string + teamId: string, + filter?: CheckFilter ): Promise; findLatestByMonitorIds(monitorIds: string[], options?: { limitPerMonitor?: number }): Promise; findByDateRangeAndMonitorId( diff --git a/server/src/domain/checks/check.repository.mongo.ts b/server/src/domain/checks/check.repository.mongo.ts index f07f7b3ee..4687f8ffc 100644 --- a/server/src/domain/checks/check.repository.mongo.ts +++ b/server/src/domain/checks/check.repository.mongo.ts @@ -20,8 +20,9 @@ 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 { DateRange } from "@/types/query.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"; @@ -211,46 +212,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: DateRange, - filter: string | undefined, 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 }), 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; @@ -268,35 +264,12 @@ class MongoChecksRepository implements IChecksRepository { return { checksCount, checks: this.mapDocuments(checks) }; }; - findByTeamId = async (sortOrder: string, dateRange: DateRange, 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), 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; diff --git a/server/src/domain/checks/check.service.ts b/server/src/domain/checks/check.service.ts index b577658bd..40dd88f80 100644 --- a/server/src/domain/checks/check.service.ts +++ b/server/src/domain/checks/check.service.ts @@ -6,7 +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 { DateRange } from "@/types/query.js"; +import { CheckFilter, DateRange } from "@/types/query.js"; const SERVICE_NAME = "checkService"; @@ -22,7 +22,7 @@ export interface ICheckService { dateRange: DateRange; page: number; rowsPerPage: number; - filter?: string; + filter?: CheckFilter; status?: boolean; }): Promise; getChecksByTeam(params: { @@ -31,7 +31,7 @@ export interface ICheckService { dateRange: DateRange; page: number; rowsPerPage: number; - filter?: string; + filter?: CheckFilter; }): Promise; getChecksSummaryByTeamId(params: { teamId: string; dateRange: DateRange }): Promise; deleteChecks(params: { monitorId: string; teamId: string }): Promise; @@ -169,7 +169,7 @@ export class CheckService implements ICheckService { 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 }); @@ -184,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; }; @@ -193,21 +193,21 @@ export class CheckService implements ICheckService { teamId, sortOrder, dateRange, - filter, page, rowsPerPage, + filter, }: { teamId: string; sortOrder: 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; }; diff --git a/server/src/service/network/DNSProvider.ts b/server/src/service/network/DNSProvider.ts index 1aeff7e14..ff136db0e 100644 --- a/server/src/service/network/DNSProvider.ts +++ b/server/src/service/network/DNSProvider.ts @@ -3,7 +3,8 @@ import { IStatusProvider } from "@/service/network/IStatusProvider.js"; import type { Resolver } from "dns/promises"; import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.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..5e5d7b630 100644 --- a/server/src/service/network/DockerProvider.ts +++ b/server/src/service/network/DockerProvider.ts @@ -4,8 +4,8 @@ import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.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..1a07de791 100644 --- a/server/src/service/network/GameProvider.ts +++ b/server/src/service/network/GameProvider.ts @@ -3,7 +3,7 @@ import { GameStatusPayload, MonitorStatusResponse } from "@/types/network.js"; import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.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/HttpProvider.ts b/server/src/service/network/HttpProvider.ts index 2f08dab00..fb974cac1 100644 --- a/server/src/service/network/HttpProvider.ts +++ b/server/src/service/network/HttpProvider.ts @@ -6,7 +6,8 @@ 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 { isStatusUp } from "@/service/network/utils.js"; +import { NETWORK_ERROR } from "@/types/network.js"; import CacheableLookup from "cacheable-lookup"; export class HttpProvider implements IStatusProvider { diff --git a/server/src/service/network/PortProvider.ts b/server/src/service/network/PortProvider.ts index debcd8509..b59eb5907 100644 --- a/server/src/service/network/PortProvider.ts +++ b/server/src/service/network/PortProvider.ts @@ -2,7 +2,8 @@ import { IStatusProvider } from "@/service/network/IStatusProvider.js"; import { PortStatusPayload, MonitorStatusResponse } from "@/types/network.js"; import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.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..b599e6fab 100644 --- a/server/src/service/network/WebSocketProvider.ts +++ b/server/src/service/network/WebSocketProvider.ts @@ -2,7 +2,8 @@ import { IStatusProvider } from "@/service/network/IStatusProvider.js"; import { WebSocketStatusPayload, MonitorStatusResponse } from "@/types/network.js"; import { Monitor, MonitorType } from "@/domain/monitors/monitor.types.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..5a23a9412 100644 --- a/server/src/service/network/utils.ts +++ b/server/src/service/network/utils.ts @@ -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 632a03141..ec54b8e49 100644 --- a/server/src/service/networkService.ts +++ b/server/src/service/networkService.ts @@ -2,7 +2,7 @@ import type { Monitor, MonitorType } from "@/domain/monitors/monitor.types.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"; diff --git a/server/src/types/network.ts b/server/src/types/network.ts index fe85221f2..b5dc1d949 100644 --- a/server/src/types/network.ts +++ b/server/src/types/network.ts @@ -13,6 +13,8 @@ import type { DnsRecordType, Monitor, MonitorMatchMethod, MonitorStatus, Monitor import type { QueryResult } from "gamedig"; +export const NETWORK_ERROR = 5000; + export interface MonitorStatusResponse< T = | HttpStatusPayload diff --git a/server/test/unit/services/checkService.test.ts b/server/test/unit/services/checkService.test.ts index 587731951..8c58259e3 100644 --- a/server/test/unit/services/checkService.test.ts +++ b/server/test/unit/services/checkService.test.ts @@ -375,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 () => { @@ -388,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 () => { @@ -443,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); }); }); From ed36db172a1e7c5793770529e99245f1ccdaf4f2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 24 Jul 2026 11:32:00 -0700 Subject: [PATCH 67/84] dedupe hardware stat definitions, fix tests, remove misleading null return types --- server/src/api/controllers/controllerUtils.ts | 2 +- server/src/api/validation/checkValidation.ts | 2 +- .../src/api/validation/monitorValidation.ts | 2 +- .../checks/check.hardware.aggregations.ts | 17 +++- server/src/domain/checks/check.model.ts | 2 +- .../checks/check.repository.interface.ts | 2 +- .../domain/checks/check.repository.mongo.ts | 59 +++++++------ server/src/domain/checks/check.type.ts | 85 ++++++++++--------- .../src/domain/geo-checks/geo-check.model.ts | 2 +- .../domain/geo-checks/geo-check.service.ts | 2 +- .../src/domain/geo-checks/geo-check.type.ts | 2 +- .../src/domain/incidents/incident.service.ts | 2 +- server/src/domain/monitors/monitor.model.ts | 4 +- .../monitors/monitor.repository.interface.ts | 8 +- .../monitors/monitor.repository.mongo.ts | 6 +- server/src/domain/monitors/monitor.service.ts | 13 ++- .../{monitor.types.ts => monitor.type.ts} | 43 +--------- .../notification.message-builder.ts | 2 +- .../notifications/notification.service.ts | 2 +- .../status-pages/status-page.service.ts | 2 +- .../domain/status-pages/status-page.type.ts | 2 +- server/src/service/globalPingService.ts | 4 +- server/src/service/network/AdvancedMatcher.ts | 2 +- server/src/service/network/DNSProvider.ts | 2 +- server/src/service/network/DockerProvider.ts | 2 +- server/src/service/network/GameProvider.ts | 2 +- server/src/service/network/GrpcProvider.ts | 2 +- .../src/service/network/HardwareProvider.ts | 2 +- server/src/service/network/HttpProvider.ts | 2 +- server/src/service/network/IStatusProvider.ts | 2 +- .../src/service/network/PageSpeedProvider.ts | 2 +- server/src/service/network/PingProvider.ts | 2 +- server/src/service/network/PortProvider.ts | 2 +- .../src/service/network/WebSocketProvider.ts | 2 +- server/src/service/network/utils.ts | 2 +- server/src/service/networkService.ts | 2 +- server/src/service/statusService.ts | 4 +- server/src/types/network.ts | 2 +- server/src/worker/worker.check-evaluator.ts | 2 +- server/src/worker/worker.check-pipeline.ts | 4 +- server/src/worker/worker.check-producer.ts | 2 +- server/src/worker/worker.interface.ts | 2 +- server/src/worker/worker.job-scheduler.ts | 4 +- 43 files changed, 148 insertions(+), 165 deletions(-) rename server/src/domain/monitors/{monitor.types.ts => monitor.type.ts} (87%) diff --git a/server/src/api/controllers/controllerUtils.ts b/server/src/api/controllers/controllerUtils.ts index be3f46533..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; diff --git a/server/src/api/validation/checkValidation.ts b/server/src/api/validation/checkValidation.ts index d041c9e8f..e40d11024 100644 --- a/server/src/api/validation/checkValidation.ts +++ b/server/src/api/validation/checkValidation.ts @@ -1,7 +1,7 @@ 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"; //**************************************** diff --git a/server/src/api/validation/monitorValidation.ts b/server/src/api/validation/monitorValidation.ts index 1ed9dca84..37a31fc6a 100644 --- a/server/src/api/validation/monitorValidation.ts +++ b/server/src/api/validation/monitorValidation.ts @@ -10,7 +10,7 @@ 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" }); diff --git a/server/src/domain/checks/check.hardware.aggregations.ts b/server/src/domain/checks/check.hardware.aggregations.ts index 176817315..6c672b630 100644 --- a/server/src/domain/checks/check.hardware.aggregations.ts +++ b/server/src/domain/checks/check.hardware.aggregations.ts @@ -1,9 +1,22 @@ 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"], @@ -70,13 +83,13 @@ export const getHardwareUpChecks = async (monitorId: string, dates: DateRange): return { totalChecks: count }; }; -export const getHardwareStats = async (monitorId: string, dates: DateRange, dateString: string) => { +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([ + return await CheckModel.aggregate([ { $match: { "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), 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 2db5df712..e6b7c6368 100644 --- a/server/src/domain/checks/check.repository.interface.ts +++ b/server/src/domain/checks/check.repository.interface.ts @@ -6,7 +6,7 @@ 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"; diff --git a/server/src/domain/checks/check.repository.mongo.ts b/server/src/domain/checks/check.repository.mongo.ts index 4687f8ffc..9767947d9 100644 --- a/server/src/domain/checks/check.repository.mongo.ts +++ b/server/src/domain/checks/check.repository.mongo.ts @@ -11,8 +11,9 @@ 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 { getDateFormat, getDateForRange } from "@/utils/dataUtils.js"; @@ -512,33 +513,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, diff --git a/server/src/domain/checks/check.type.ts b/server/src/domain/checks/check.type.ts index fe733e4e9..faac84cef 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,43 +126,6 @@ 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; @@ -205,3 +168,49 @@ export type NormalizedUptimeCheck = T & { }; 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/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.service.ts b/server/src/domain/geo-checks/geo-check.service.ts index 26d347228..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"; 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.service.ts b/server/src/domain/incidents/incident.service.ts index 9b8fbf25e..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"; 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 a710e366f..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; diff --git a/server/src/domain/monitors/monitor.repository.mongo.ts b/server/src/domain/monitors/monitor.repository.mongo.ts index d9e35855c..b96ec3907 100644 --- a/server/src/domain/monitors/monitor.repository.mongo.ts +++ b/server/src/domain/monitors/monitor.repository.mongo.ts @@ -1,7 +1,7 @@ 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"; @@ -96,14 +96,14 @@ class MongoMonitorsRepository implements IMonitorsRepository { return { sort: { [field]: order === "asc" ? 1 : -1 } as const, skip: Math.max(page, 0) * rowsPerPage, limit: rowsPerPage }; }; - findByTeamId = async (teamId: string, config: TeamQueryConfig): Promise => { + 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 => { + findByTeamIdWithStats = async (teamId: string, config: TeamQueryConfig): Promise => { const { sort, skip, limit } = this.pageOptions(config); const query = this.queryBuilder(config, teamId); diff --git a/server/src/domain/monitors/monitor.service.ts b/server/src/domain/monitors/monitor.service.ts index 711fc9b9b..178da5225 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 { 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"; @@ -318,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 }); }; @@ -356,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); @@ -523,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.service.ts b/server/src/domain/notifications/notification.service.ts index 30141c3ea..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"; 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/service/globalPingService.ts b/server/src/service/globalPingService.ts index 4b0257df4..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"; 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 ff136db0e..10eff8d74 100644 --- a/server/src/service/network/DNSProvider.ts +++ b/server/src/service/network/DNSProvider.ts @@ -1,7 +1,7 @@ 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 { timeRequest } from "@/service/network/utils.js"; import { NETWORK_ERROR } from "@/types/network.js"; diff --git a/server/src/service/network/DockerProvider.ts b/server/src/service/network/DockerProvider.ts index 5e5d7b630..9f2a99de7 100644 --- a/server/src/service/network/DockerProvider.ts +++ b/server/src/service/network/DockerProvider.ts @@ -1,6 +1,6 @@ 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"; diff --git a/server/src/service/network/GameProvider.ts b/server/src/service/network/GameProvider.ts index 1a07de791..5ccfaee6e 100644 --- a/server/src/service/network/GameProvider.ts +++ b/server/src/service/network/GameProvider.ts @@ -1,6 +1,6 @@ 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 "@/types/network.js"; 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 fb974cac1..b1debd969 100644 --- a/server/src/service/network/HttpProvider.ts +++ b/server/src/service/network/HttpProvider.ts @@ -5,7 +5,7 @@ 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 { 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"; 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 b59eb5907..692ca40af 100644 --- a/server/src/service/network/PortProvider.ts +++ b/server/src/service/network/PortProvider.ts @@ -1,6 +1,6 @@ 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 { timeRequest } from "@/service/network/utils.js"; import { NETWORK_ERROR } from "@/types/network.js"; diff --git a/server/src/service/network/WebSocketProvider.ts b/server/src/service/network/WebSocketProvider.ts index b599e6fab..d27106989 100644 --- a/server/src/service/network/WebSocketProvider.ts +++ b/server/src/service/network/WebSocketProvider.ts @@ -1,6 +1,6 @@ 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 { timeRequest } from "@/service/network/utils.js"; import { NETWORK_ERROR } from "@/types/network.js"; diff --git a/server/src/service/network/utils.ts b/server/src/service/network/utils.ts index 5a23a9412..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(); diff --git a/server/src/service/networkService.ts b/server/src/service/networkService.ts index ec54b8e49..75ff66bda 100644 --- a/server/src/service/networkService.ts +++ b/server/src/service/networkService.ts @@ -1,4 +1,4 @@ -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"; diff --git a/server/src/service/statusService.ts b/server/src/service/statusService.ts index 6435958cb..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; diff --git a/server/src/types/network.ts b/server/src/types/network.ts index b5dc1d949..2839ed95f 100644 --- a/server/src/types/network.ts +++ b/server/src/types/network.ts @@ -9,7 +9,7 @@ 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"; 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.interface.ts b/server/src/worker/worker.interface.ts index e70af5f35..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"; diff --git a/server/src/worker/worker.job-scheduler.ts b/server/src/worker/worker.job-scheduler.ts index 433e69955..8d103e87f 100644 --- a/server/src/worker/worker.job-scheduler.ts +++ b/server/src/worker/worker.job-scheduler.ts @@ -1,7 +1,7 @@ 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"; @@ -187,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) { From c28be97fb0fa0dd0d522d307541cd81585da718e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 24 Jul 2026 11:32:18 -0700 Subject: [PATCH 68/84] update tests --- server/test/unit/services/monitorService.test.ts | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/server/test/unit/services/monitorService.test.ts b/server/test/unit/services/monitorService.test.ts index 085b3322f..69edbba9c 100644 --- a/server/test/unit/services/monitorService.test.ts +++ b/server/test/unit/services/monitorService.test.ts @@ -574,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 }); @@ -1576,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([]); From 49e8d26bfcb1f6f13611ee3af0283e3374bfe26d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 24 Jul 2026 11:34:54 -0700 Subject: [PATCH 69/84] remove legacy try/catch blocks --- .../geo-checks/geo-check.repository.mongo.ts | 400 ++++++++---------- 1 file changed, 170 insertions(+), 230 deletions(-) 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 5f4067ec7..0d1183720 100644 --- a/server/src/domain/geo-checks/geo-check.repository.mongo.ts +++ b/server/src/domain/geo-checks/geo-check.repository.mongo.ts @@ -64,28 +64,18 @@ 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 ( @@ -96,95 +86,85 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { rowsPerPage: number, continents?: GeoContinent[] ): Promise => { - try { - const matchStage: Record = { - "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), - createdAt: { $gte: getDateForRange(dateRange) }, - }; + const matchStage: Record = { + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + createdAt: { $gte: getDateForRange(dateRange) }, + }; - const convertedSortOrder = sortOrder === "asc" ? 1 : -1; + 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 }; - } 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; + 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 => { @@ -192,146 +172,106 @@ class MongoGeoChecksRepository implements IGeoChecksRepository { const dateString = getDateFormat(dateRange); const end = new Date(); - try { - const pipeline: PipelineStage[] = [ - // Match geo checks for this monitor in date range - { - $match: { - "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), - createdAt: { - $gte: start, - $lte: end, + const pipeline: PipelineStage[] = [ + // Match geo checks for this monitor in date range + { + $match: { + "metadata.monitorId": new mongoose.Types.ObjectId(monitorId), + createdAt: { + $gte: start, + $lte: end, + }, + }, + }, + // 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], }, }, }, - // Unwind the results array to process each location separately - { - $unwind: "$results", - }, - // Filter by continent if specified - ...(continents && continents.length > 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, + ], }, - ] - : []), - // 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], - }, - }, + 2, + ], }, }, - // 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, }, - // 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; }; } From 8369db5ca2d54baab8ba90864f543a883792e6bf Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 24 Jul 2026 11:38:08 -0700 Subject: [PATCH 70/84] remove logger --- server/src/config/services.shared.ts | 2 +- server/src/domain/geo-checks/geo-check.repository.mongo.ts | 6 ------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/server/src/config/services.shared.ts b/server/src/config/services.shared.ts index 72fe3fcf4..0f7813d8a 100644 --- a/server/src/config/services.shared.ts +++ b/server/src/config/services.shared.ts @@ -126,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(); 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 0d1183720..a56699750 100644 --- a/server/src/domain/geo-checks/geo-check.repository.mongo.ts +++ b/server/src/domain/geo-checks/geo-check.repository.mongo.ts @@ -4,7 +4,6 @@ import type { FlatGeoChecksQueryResult } from "./geo-check.repository.interface. import { GeoCheckMetadataDocument, GeoCheckModel, type GeoCheckDocument } from "@/domain/geo-checks/geo-check.model.js"; import mongoose, { PipelineStage } from "mongoose"; import { getDateForRange, getDateFormat } from "@/utils/dataUtils.js"; -import { ILogger } from "@/utils/logger.js"; import { toStringId, toDateString } from "@/utils/mongoMappers.js"; import { DateRange } from "@/types/query.js"; @@ -13,11 +12,6 @@ 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 mapMetadata = (metadata: GeoCheckMetadataDocument): GeoCheckMetadata => ({ monitorId: toStringId(metadata.monitorId), From e0228918fb85a943d1c78bfe52cc4e4d77248419 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 24 Jul 2026 12:28:45 -0700 Subject: [PATCH 71/84] fix tests --- server/test/helpers/InMemoryMonitorsRepository.ts | 2 +- server/test/helpers/heartbeatTestHarness.ts | 2 +- server/test/helpers/statusProviderContract.ts | 2 +- server/test/integration/incidentLifecycle.test.ts | 2 +- server/test/unit/providers/network/advancedMatcher.test.ts | 2 +- server/test/unit/providers/network/dnsProvider.test.ts | 4 ++-- server/test/unit/providers/network/dockerProvider.test.ts | 4 ++-- server/test/unit/providers/network/gameProvider.test.ts | 4 ++-- server/test/unit/providers/network/grpcProvider.test.ts | 2 +- .../test/unit/providers/network/hardwareProvider.test.ts | 2 +- server/test/unit/providers/network/httpProvider.test.ts | 4 ++-- .../test/unit/providers/network/pageSpeedProvider.test.ts | 2 +- server/test/unit/providers/network/pingProvider.test.ts | 2 +- server/test/unit/providers/network/portProvider.test.ts | 4 ++-- server/test/unit/providers/network/utils.test.ts | 7 ++----- .../test/unit/providers/network/webSocketProvider.test.ts | 4 ++-- server/test/unit/services/checkEvaluator.test.ts | 2 +- server/test/unit/services/checkPipeline.test.ts | 2 +- server/test/unit/services/checkProducer.test.ts | 2 +- server/test/unit/services/checkService.test.ts | 2 +- server/test/unit/services/dbQueueWorker.test.ts | 2 +- server/test/unit/services/geoChecksService.test.ts | 2 +- server/test/unit/services/incidentService.test.ts | 2 +- server/test/unit/services/jobScheduler.test.ts | 2 +- server/test/unit/services/networkService.test.ts | 4 ++-- .../test/unit/services/notificationMessageBuilder.test.ts | 2 +- server/test/unit/services/notificationsService.test.ts | 2 +- server/test/unit/services/statusPageService.test.ts | 2 +- server/test/unit/services/statusService.test.ts | 4 ++-- 29 files changed, 38 insertions(+), 41 deletions(-) diff --git a/server/test/helpers/InMemoryMonitorsRepository.ts b/server/test/helpers/InMemoryMonitorsRepository.ts index 09162cfd2..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[] = []; 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..88897aca3 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 ──────────────────────────────────────────────────────────────────── 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/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 8c58259e3..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 ────────────────────────────────────────────────────────────────── diff --git a/server/test/unit/services/dbQueueWorker.test.ts b/server/test/unit/services/dbQueueWorker.test.ts index 4956e474c..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"; diff --git a/server/test/unit/services/geoChecksService.test.ts b/server/test/unit/services/geoChecksService.test.ts index 69bfff730..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 ────────────────────────────────────────────────────────────────── diff --git a/server/test/unit/services/incidentService.test.ts b/server/test/unit/services/incidentService.test.ts index 27b2736df..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"; 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/networkService.test.ts b/server/test/unit/services/networkService.test.ts index 0943b1f22..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 ────────────────────────────────────────────────────────────────── 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 6fbbfa4a1..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"; 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 4f4c0264a..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"; From 4182a964a610e6372938a3bc66aa831e6c7e3c1d Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 28 Jul 2026 09:48:31 -0700 Subject: [PATCH 72/84] minor version bump --- client/package-lock.json | 4 ++-- client/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index ba1812e60..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", 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", From 0cf8cfa5dc21dbe4bd210336e3b7a8d39ce4b60b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 28 Jul 2026 11:13:35 -0700 Subject: [PATCH 73/84] add avg timing phases to details response --- .../domain/checks/check.repository.mongo.ts | 21 ++++++++++++++++++- server/src/domain/checks/check.type.ts | 11 +++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/server/src/domain/checks/check.repository.mongo.ts b/server/src/domain/checks/check.repository.mongo.ts index 9767947d9..2374107d9 100644 --- a/server/src/domain/checks/check.repository.mongo.ts +++ b/server/src/domain/checks/check.repository.mongo.ts @@ -445,11 +445,30 @@ class MongoChecksRepository implements IChecksRepository { $dateToString: { format: dateString, date: "$createdAt" }, }, avgResponseTime: { $avg: "$responseTime" }, + avgDns: { $avg: "$timings.phases.dns" }, + avgTcp: { $avg: "$timings.phases.tcp" }, + avgTls: { $avg: "$timings.phases.tls" }, + avgRequest: { $avg: "$timings.phases.request" }, + avgFirstByte: { $avg: "$timings.phases.firstByte" }, + avgDownload: { $avg: "$timings.phases.download" }, totalChecks: { $sum: 1 }, }, }, { $sort: { _id: 1 } }, - { $project: { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, _id: 0 } }, + { + $project: { + bucketDate: "$_id", + avgResponseTime: 1, + totalChecks: 1, + avgDns: { $ifNull: ["$avgDns", 0] }, + avgTcp: { $ifNull: ["$avgTcp", 0] }, + avgTls: { $ifNull: ["$avgTls", 0] }, + avgRequest: { $ifNull: ["$avgRequest", 0] }, + avgFirstByte: { $ifNull: ["$avgFirstByte", 0] }, + avgDownload: { $ifNull: ["$avgDownload", 0] }, + _id: 0, + }, + }, ], groupedUpChecks: [ { $match: { status: true } }, diff --git a/server/src/domain/checks/check.type.ts b/server/src/domain/checks/check.type.ts index faac84cef..911629e92 100644 --- a/server/src/domain/checks/check.type.ts +++ b/server/src/domain/checks/check.type.ts @@ -132,6 +132,15 @@ export interface GroupedCheck { 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; @@ -143,7 +152,7 @@ export interface PageSpeedGroupedCheck { export interface UptimeChecksResult { monitorType: Exclude; - groupedChecks: GroupedCheck[]; + groupedChecks: GroupedUptimeCheck[]; groupedUpChecks: GroupedCheck[]; groupedDownChecks: GroupedCheck[]; uptimePercentage: number; From 13c921531c6e77616c531bf55f3927f1936ae199 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 28 Jul 2026 11:13:50 -0700 Subject: [PATCH 74/84] update types, add skeleton for stacked timing phase chart --- .../charts/HistogramDetailsStacked.tsx | 39 +++++++++++++++++++ client/src/Pages/Uptime/Details/index.tsx | 16 +++++++- client/src/Types/Check.ts | 9 +++++ client/src/Types/Monitor.ts | 4 +- 4 files changed, 65 insertions(+), 3 deletions(-) create mode 100644 client/src/Components/monitors/charts/HistogramDetailsStacked.tsx diff --git a/client/src/Components/monitors/charts/HistogramDetailsStacked.tsx b/client/src/Components/monitors/charts/HistogramDetailsStacked.tsx new file mode 100644 index 000000000..a47af1a0d --- /dev/null +++ b/client/src/Components/monitors/charts/HistogramDetailsStacked.tsx @@ -0,0 +1,39 @@ +import type { GroupedUptimeCheck } from "@/Types/Check"; +import { Area, AreaChart, CartesianGrid, ResponsiveContainer } from "recharts"; + +import { useTheme } from "@mui/material"; + +export const HistogramDetailsStacked = ({ + checks = [], + range, +}: { + checks: GroupedUptimeCheck[]; + range: string; +}) => { + const theme = useTheme(); + + console.log(JSON.stringify(checks, null, 2)); + + return ( + + + + + + + ); +}; diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx index 8828aa29a..7aa0b1046 100644 --- a/client/src/Pages/Uptime/Details/index.tsx +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -31,6 +31,7 @@ import type { RootState } from "@/Types/state"; import { formatDateWithTz } from "@/Utils/TimeUtils"; import { t } from "i18next"; import { Typography } from "@mui/material"; +import { HistogramDetailsStacked } from "@/Components/monitors/charts/HistogramDetailsStacked"; const certificateDateFormat = "MMM D, YYYY h A"; @@ -72,6 +73,7 @@ const UptimeDetailsPage = () => { ); const monitorData = monitorDetailsData?.monitorData; + const monitor = monitorData?.monitor; const monitorStats = monitorDetailsData?.monitorStats ?? null; @@ -165,10 +167,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; @@ -218,6 +228,10 @@ const UptimeDetailsPage = () => { checks={monitorData?.groupedChecks || []} range={dateRange} /> + Date: Tue, 28 Jul 2026 11:24:23 -0700 Subject: [PATCH 75/84] add area charts --- .../charts/HistogramDetailsStacked.tsx | 72 +++++++++++++++++-- 1 file changed, 68 insertions(+), 4 deletions(-) diff --git a/client/src/Components/monitors/charts/HistogramDetailsStacked.tsx b/client/src/Components/monitors/charts/HistogramDetailsStacked.tsx index a47af1a0d..81ca5a103 100644 --- a/client/src/Components/monitors/charts/HistogramDetailsStacked.tsx +++ b/client/src/Components/monitors/charts/HistogramDetailsStacked.tsx @@ -1,8 +1,18 @@ import type { GroupedUptimeCheck } from "@/Types/Check"; -import { Area, AreaChart, CartesianGrid, ResponsiveContainer } from "recharts"; +import { Area, AreaChart, CartesianGrid, ResponsiveContainer, YAxis } from "recharts"; +import { useMemo } from "react"; import { useTheme } from "@mui/material"; +const PHASE_KEYS = [ + "avgDns", + "avgTcp", + "avgTls", + "avgRequest", + "avgFirstByte", + "avgDownload", +] as const; + export const HistogramDetailsStacked = ({ checks = [], range, @@ -12,6 +22,17 @@ export const HistogramDetailsStacked = ({ }) => { const theme = useTheme(); + // Cap the y-domain at the p95 of bucket totals so a single timeout bucket + // doesn't flatten every other bucket; spikes clip off the top instead. + const yMax = useMemo(() => { + const totals = checks + .map((check) => PHASE_KEYS.reduce((sum, key) => sum + check[key], 0)) + .sort((a, b) => a - b); + if (totals.length < 2) return undefined; + const p95 = totals[Math.min(totals.length - 1, Math.floor(totals.length * 0.95))]; + return p95 > 0 ? Math.ceil(p95) : undefined; + }, [checks]); + console.log(JSON.stringify(checks, null, 2)); return ( @@ -27,11 +48,54 @@ export const HistogramDetailsStacked = ({ fill="transparent" vertical={false} /> + + + + + + + + From 061da5e7ef0db9b0939f23fa6f18be3a72433f7b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 28 Jul 2026 15:26:52 -0700 Subject: [PATCH 76/84] coerce values to 0 if null --- .../domain/checks/check.repository.mongo.ts | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/server/src/domain/checks/check.repository.mongo.ts b/server/src/domain/checks/check.repository.mongo.ts index 2374107d9..fcc967828 100644 --- a/server/src/domain/checks/check.repository.mongo.ts +++ b/server/src/domain/checks/check.repository.mongo.ts @@ -445,12 +445,12 @@ class MongoChecksRepository implements IChecksRepository { $dateToString: { format: dateString, date: "$createdAt" }, }, avgResponseTime: { $avg: "$responseTime" }, - avgDns: { $avg: "$timings.phases.dns" }, - avgTcp: { $avg: "$timings.phases.tcp" }, - avgTls: { $avg: "$timings.phases.tls" }, - avgRequest: { $avg: "$timings.phases.request" }, - avgFirstByte: { $avg: "$timings.phases.firstByte" }, - avgDownload: { $avg: "$timings.phases.download" }, + 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 }, }, }, @@ -460,12 +460,12 @@ class MongoChecksRepository implements IChecksRepository { bucketDate: "$_id", avgResponseTime: 1, totalChecks: 1, - avgDns: { $ifNull: ["$avgDns", 0] }, - avgTcp: { $ifNull: ["$avgTcp", 0] }, - avgTls: { $ifNull: ["$avgTls", 0] }, - avgRequest: { $ifNull: ["$avgRequest", 0] }, - avgFirstByte: { $ifNull: ["$avgFirstByte", 0] }, - avgDownload: { $ifNull: ["$avgDownload", 0] }, + avgDns: 1, + avgTcp: 1, + avgTls: 1, + avgRequest: 1, + avgFirstByte: 1, + avgDownload: 1, _id: 0, }, }, From 4e495eed33576c281712aff888942947709c6dfa Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 28 Jul 2026 15:27:04 -0700 Subject: [PATCH 77/84] add chart colors to theme --- client/src/Utils/Theme/Palette.ts | 31 +++++++++++++++++++++++++++++++ client/src/Utils/Theme/Theme.ts | 20 ++++++++++++++++++++ 2 files changed, 51 insertions(+) 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; + }; + }; } } From 81aab3596103d4f458fa2c7e7534a5c88ac5adfd Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Tue, 28 Jul 2026 15:27:19 -0700 Subject: [PATCH 78/84] conditionally render charts --- .../monitors/charts/HistogramDetails.tsx | 9 +- .../charts/HistogramDetailsStacked.tsx | 272 +++++++++++++----- client/src/Pages/Uptime/Details/index.tsx | 23 +- 3 files changed, 227 insertions(+), 77 deletions(-) diff --git a/client/src/Components/monitors/charts/HistogramDetails.tsx b/client/src/Components/monitors/charts/HistogramDetails.tsx index bd78f3289..17849fadb 100644 --- a/client/src/Components/monitors/charts/HistogramDetails.tsx +++ b/client/src/Components/monitors/charts/HistogramDetails.tsx @@ -12,6 +12,7 @@ import { 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 { useSelector } from "react-redux"; import { @@ -68,12 +69,14 @@ const ResponseTimeToolTip = ({ if (!active) return null; const format = tooltipDateFormatLookup(range); - console.log(payload); const responseTime = Math.floor(payload[0]?.payload?.originalAvgResponseTime || 0); return ( {formatDateWithTz(String(label), format, uiTimezone)} - Response time: {responseTime} ms + + Response time:{" "} + {prettyMilliseconds(responseTime, { formatSubMilliseconds: true, compact: true })} + ); }; @@ -95,7 +98,7 @@ export const HistogramDetails = ({ strokeWidth={1.5} /> } - title="Response times" + title="Average response time (First byte)" > = { + avgDns: "DNS", + avgTcp: "TCP", + avgTls: "TLS", + avgRequest: "Request", + avgFirstByte: "First byte", + avgDownload: "Download", +}; + +type TimingPhaseToolTipProps = TooltipProps & { + range: string; + theme: Theme; + uiTimezone: string; +}; + +type XTickProps = { + x: number; + y: number; + payload: { value: string }; + range: 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 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)} + + ); +}; + export const HistogramDetailsStacked = ({ checks = [], range, @@ -21,83 +135,103 @@ export const HistogramDetailsStacked = ({ range: string; }) => { const theme = useTheme(); + const uiTimezone = useSelector((state: RootState) => state.ui.timezone); - // Cap the y-domain at the p95 of bucket totals so a single timeout bucket - // doesn't flatten every other bucket; spikes clip off the top instead. + // 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)) .sort((a, b) => a - b); if (totals.length < 2) return undefined; const p95 = totals[Math.min(totals.length - 1, Math.floor(totals.length * 0.95))]; - return p95 > 0 ? Math.ceil(p95) : undefined; + return p95 > 0 ? Math.ceil(p95 * 2) : undefined; }, [checks]); - console.log(JSON.stringify(checks, null, 2)); - return ( - + } + title="Timing phases" > - - - - - - - - - - - - - + + + + + {PHASE_KEYS.map((key) => ( + + + + + ))} + + + ( + + )} + /> + ( + + )} + /> + {PHASE_KEYS.map((key) => ( + + ))} + + + ); }; diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx index 7aa0b1046..04d207d06 100644 --- a/client/src/Pages/Uptime/Details/index.tsx +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -31,7 +31,10 @@ import type { RootState } from "@/Types/state"; import { formatDateWithTz } from "@/Utils/TimeUtils"; import { t } from "i18next"; import { Typography } from "@mui/material"; -import { HistogramDetailsStacked } from "@/Components/monitors/charts/HistogramDetailsStacked"; +import { + HistogramDetailsStacked, + PHASE_KEYS, +} from "@/Components/monitors/charts/HistogramDetailsStacked"; const certificateDateFormat = "MMM D, YYYY h A"; @@ -77,6 +80,14 @@ const UptimeDetailsPage = () => { const monitor = monitorData?.monitor; const monitorStats = monitorDetailsData?.monitorStats ?? null; + const hasTimingPhases = useMemo( + () => + (monitorData?.groupedChecks ?? []).some((check) => + PHASE_KEYS.some((key) => check[key] > 0) + ), + [monitorData] + ); + // Certificate fetch - only for HTTP monitors const certificateUrl = useMemo(() => { if (!monitorId || monitor?.type !== "http") { @@ -225,13 +236,15 @@ const UptimeDetailsPage = () => { />
- + {hasTimingPhases && ( + + )} Date: Wed, 29 Jul 2026 10:57:20 -0700 Subject: [PATCH 79/84] denormalize resposne times --- .../Components/monitors/charts/ChartUtils.ts | 6 ++ .../monitors/charts/HistogramDetails.tsx | 15 ++++- .../charts/HistogramDetailsStacked.tsx | 11 ++-- client/src/Pages/Uptime/Details/index.tsx | 1 - client/src/Types/Check.ts | 4 -- .../src/api/validation/monitorValidation.ts | 1 - server/src/domain/checks/check.type.ts | 4 -- server/src/domain/monitors/monitor.service.ts | 8 +-- server/src/utils/dataUtils.ts | 16 +----- server/test/unit/utils/dataUtils.test.ts | 57 +------------------ 10 files changed, 31 insertions(+), 92 deletions(-) create mode 100644 client/src/Components/monitors/charts/ChartUtils.ts diff --git a/client/src/Components/monitors/charts/ChartUtils.ts b/client/src/Components/monitors/charts/ChartUtils.ts new file mode 100644 index 000000000..27c36d38b --- /dev/null +++ b/client/src/Components/monitors/charts/ChartUtils.ts @@ -0,0 +1,6 @@ +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; +}; diff --git a/client/src/Components/monitors/charts/HistogramDetails.tsx b/client/src/Components/monitors/charts/HistogramDetails.tsx index 17849fadb..29bf63f09 100644 --- a/client/src/Components/monitors/charts/HistogramDetails.tsx +++ b/client/src/Components/monitors/charts/HistogramDetails.tsx @@ -4,6 +4,7 @@ import { AreaChart, Area, XAxis, + YAxis, Tooltip, CartesianGrid, ResponsiveContainer, @@ -24,6 +25,8 @@ import { useTheme } from "@mui/material/styles"; import type { Theme } from "@mui/material/styles"; import type { GroupedCheck } from "@/Types/Check"; import type { RootState } from "@/Types/state"; +import { useMemo } from "react"; +import { computeYAxisCap } from "@/Components/monitors/charts/ChartUtils"; type XTickProps = { x: number; @@ -69,7 +72,7 @@ const ResponseTimeToolTip = ({ if (!active) return null; const format = tooltipDateFormatLookup(range); - const responseTime = Math.floor(payload[0]?.payload?.originalAvgResponseTime || 0); + const responseTime = Math.floor(payload[0]?.payload?.avgResponseTime || 0); return ( {formatDateWithTz(String(label), format, uiTimezone)} @@ -90,6 +93,10 @@ export const HistogramDetails = ({ }) => { const theme = useTheme(); const uiTimezone = useSelector((state: RootState) => state.ui.timezone); + const yMax = useMemo(() => { + const totals = checks.map((check) => check.avgResponseTime || 0); + return computeYAxisCap(totals); + }, [checks]); return ( + { - const totals = checks - .map((check) => PHASE_KEYS.reduce((sum, key) => sum + check[key], 0)) - .sort((a, b) => a - b); - if (totals.length < 2) return undefined; - const p95 = totals[Math.min(totals.length - 1, Math.floor(totals.length * 0.95))]; - return p95 > 0 ? Math.ceil(p95 * 2) : undefined; + const totals = checks.map((check) => + PHASE_KEYS.reduce((sum, key) => sum + check[key], 0) + ); + return computeYAxisCap(totals); }, [checks]); return ( diff --git a/client/src/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx index 04d207d06..c4128e5e4 100644 --- a/client/src/Pages/Uptime/Details/index.tsx +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -61,7 +61,6 @@ const UptimeDetailsPage = () => { } const params = new URLSearchParams(); params.append("dateRange", dateRange); - params.append("normalize", "true"); return `/monitors/uptime/details/${monitorId}?${params.toString()}`; }, [monitorId, dateRange]); diff --git a/client/src/Types/Check.ts b/client/src/Types/Check.ts index f629bd122..cfd072944 100644 --- a/client/src/Types/Check.ts +++ b/client/src/Types/Check.ts @@ -261,7 +261,3 @@ export interface HasResponseTime { export type NormalizedCheck = T & { originalResponseTime: number; }; - -export type NormalizedUptimeCheck = T & { - originalAvgResponseTime: number; -}; diff --git a/server/src/api/validation/monitorValidation.ts b/server/src/api/validation/monitorValidation.ts index 37a31fc6a..0e8b7b0bd 100644 --- a/server/src/api/validation/monitorValidation.ts +++ b/server/src/api/validation/monitorValidation.ts @@ -197,7 +197,6 @@ export const getUptimeDetailsByIdParamValidation = z.object({ export const getUptimeDetailsByIdQueryValidation = z.object({ dateRange: z.enum(DateRanges), - normalize: booleanCoercion.optional(), }); const importedMonitorSchema = z diff --git a/server/src/domain/checks/check.type.ts b/server/src/domain/checks/check.type.ts index 911629e92..d4c47cfb9 100644 --- a/server/src/domain/checks/check.type.ts +++ b/server/src/domain/checks/check.type.ts @@ -172,10 +172,6 @@ export type NormalizedCheck = T & { originalResponseTime: number; }; -export type NormalizedUptimeCheck = T & { - originalAvgResponseTime: number; -}; - export type CheckSnapshot = Omit; export interface HardwareDiskStats { diff --git a/server/src/domain/monitors/monitor.service.ts b/server/src/domain/monitors/monitor.service.ts index 178da5225..29046f83d 100644 --- a/server/src/domain/monitors/monitor.service.ts +++ b/server/src/domain/monitors/monitor.service.ts @@ -1,4 +1,4 @@ -import { NormalizeData, NormalizeDataUptimeDetails } from "@/utils/dataUtils.js"; +import { NormalizeData } from "@/utils/dataUtils.js"; import { type Monitor } from "@/domain/monitors/monitor.type.js"; import type { MonitorType, @@ -193,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, }, diff --git a/server/src/utils/dataUtils.ts b/server/src/utils/dataUtils.ts index 0382abc57..2c15b943e 100755 --- a/server/src/utils/dataUtils.ts +++ b/server/src/utils/dataUtils.ts @@ -1,4 +1,4 @@ -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: DateRange): Date => { @@ -61,17 +61,3 @@ export const NormalizeData = (checks: T[], rangeMin: originalResponseTime: original(check), })); }; - -export const NormalizeDataUptimeDetails = (checks: T[], rangeMin: number, rangeMax: number): NormalizedUptimeCheck[] => { - const original = (check: T) => (typeof check.avgResponseTime === "number" ? check.avgResponseTime : 0); - if (checks.length <= 1) { - return checks.map((check) => ({ ...check, originalAvgResponseTime: original(check) })); - } - const min = percentileBy(checks, 0, (c) => c.avgResponseTime); - const max = percentileBy(checks, 95, (c) => c.avgResponseTime); - return checks.map((check) => ({ - ...check, - avgResponseTime: rescale(check.avgResponseTime, min, max, rangeMin, rangeMax), - originalAvgResponseTime: original(check), - })); -}; diff --git a/server/test/unit/utils/dataUtils.test.ts b/server/test/unit/utils/dataUtils.test.ts index 01f102cca..af9434bab 100644 --- a/server/test/unit/utils/dataUtils.test.ts +++ b/server/test/unit/utils/dataUtils.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "@jest/globals"; -import { getDateForRange, getDateFormat, NormalizeData, NormalizeDataUptimeDetails } from "../../../src/utils/dataUtils.ts"; -import type { GroupedCheck } from "../../../src/domain/checks/check.type.ts"; +import { getDateForRange, getDateFormat, NormalizeData } from "../../../src/utils/dataUtils.ts"; import { DateRanges, type DateRange } from "../../../src/types/query.ts"; // ── Helpers ────────────────────────────────────────────────────────────────── @@ -12,13 +11,6 @@ const makeCheck = (responseTime: number, overrides?: Record) => ...overrides, }); -const makeGrouped = (avgResponseTime: number, overrides?: Partial): GroupedCheck => ({ - bucketDate: `bucket-${avgResponseTime}`, - avgResponseTime, - totalChecks: 5, - ...overrides, -}); - // ── Tests ──────────────────────────────────────────────────────────────────── describe("getDateForRange", () => { @@ -126,50 +118,3 @@ describe("NormalizeData", () => { }); }); }); - -describe("NormalizeDataUptimeDetails", () => { - it("rescales avgResponseTime into [rangeMin, rangeMax] anchored to the p0–p95 window", () => { - const checks = [100, 200, 300, 400, 500].map((avg) => makeGrouped(avg)); - - const result = NormalizeDataUptimeDetails(checks, 10, 100); - - expect(result[0]!.avgResponseTime).toBe(10); - expect(result[1]!.avgResponseTime).toBeCloseTo(33.6842, 4); - expect(result[4]!.avgResponseTime).toBe(100); - }); - - it("preserves the raw value in originalAvgResponseTime and spreads other fields", () => { - const checks = [makeGrouped(100), makeGrouped(500)]; - - const result = NormalizeDataUptimeDetails(checks, 10, 100); - - expect(result[0]!.originalAvgResponseTime).toBe(100); - expect(result[1]!.originalAvgResponseTime).toBe(500); - expect(result[0]).toMatchObject({ bucketDate: "bucket-100", totalChecks: 5 }); - }); - - it("passes a single bucket through, only adding originalAvgResponseTime", () => { - const result = NormalizeDataUptimeDetails([makeGrouped(250)], 10, 100); - - expect(result).toEqual([{ bucketDate: "bucket-250", avgResponseTime: 250, totalChecks: 5, originalAvgResponseTime: 250 }]); - }); - - it("returns an empty array for empty input", () => { - expect(NormalizeDataUptimeDetails([], 10, 100)).toEqual([]); - }); - - it("records originalAvgResponseTime as 0 for a null avgResponseTime ($avg over an empty bucket)", () => { - const result = NormalizeDataUptimeDetails([makeGrouped(null as unknown as number)], 10, 100); - - expect(result[0]!.originalAvgResponseTime).toBe(0); - }); - - it("produces NaN when all values are equal (division by zero — known edge case)", () => { - const result = NormalizeDataUptimeDetails([makeGrouped(50), makeGrouped(50)], 10, 100); - - result.forEach((check) => { - expect(check.avgResponseTime).toBeNaN(); - expect(check.originalAvgResponseTime).toBe(50); - }); - }); -}); From 58f1ae7fe9293545d6962a288c074f69d1d3a538 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 29 Jul 2026 11:04:11 -0700 Subject: [PATCH 80/84] extract xtick --- .../charts/{ChartUtils.ts => ChartUtils.tsx} | 0 .../monitors/charts/HistogramDetails.tsx | 34 ++----------------- .../charts/HistogramDetailsStacked.tsx | 33 ++---------------- .../charts/HistogramInfrastructure.tsx | 2 +- .../charts/HistogramPageSpeedDetails.tsx | 2 +- .../src/Components/monitors/charts/XTick.tsx | 30 ++++++++++++++++ 6 files changed, 36 insertions(+), 65 deletions(-) rename client/src/Components/monitors/charts/{ChartUtils.ts => ChartUtils.tsx} (100%) create mode 100644 client/src/Components/monitors/charts/XTick.tsx diff --git a/client/src/Components/monitors/charts/ChartUtils.ts b/client/src/Components/monitors/charts/ChartUtils.tsx similarity index 100% rename from client/src/Components/monitors/charts/ChartUtils.ts rename to client/src/Components/monitors/charts/ChartUtils.tsx diff --git a/client/src/Components/monitors/charts/HistogramDetails.tsx b/client/src/Components/monitors/charts/HistogramDetails.tsx index 29bf63f09..3ad0e03b3 100644 --- a/client/src/Components/monitors/charts/HistogramDetails.tsx +++ b/client/src/Components/monitors/charts/HistogramDetails.tsx @@ -8,19 +8,14 @@ import { 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"; @@ -28,31 +23,6 @@ import type { RootState } from "@/Types/state"; import { useMemo } from "react"; import { computeYAxisCap } from "@/Components/monitors/charts/ChartUtils"; -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)} - - ); -}; - type ResponseTimeToolTipProps = TooltipProps & { range: string; theme: Theme; diff --git a/client/src/Components/monitors/charts/HistogramDetailsStacked.tsx b/client/src/Components/monitors/charts/HistogramDetailsStacked.tsx index 46377cedd..2ef74f56b 100644 --- a/client/src/Components/monitors/charts/HistogramDetailsStacked.tsx +++ b/client/src/Components/monitors/charts/HistogramDetailsStacked.tsx @@ -14,17 +14,13 @@ import { ResponsiveContainer, YAxis, XAxis, - Text, 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, - tickDateFormatLookup, - tooltipDateFormatLookup, -} from "@/Utils/TimeUtils"; +import { formatDateWithTz, tooltipDateFormatLookup } from "@/Utils/TimeUtils"; import { useSelector } from "react-redux"; import { useMemo } from "react"; import { useTheme } from "@mui/material"; @@ -64,13 +60,6 @@ type TimingPhaseToolTipProps = TooltipProps & { uiTimezone: string; }; -type XTickProps = { - x: number; - y: number; - payload: { value: string }; - range: string; -}; - const TimingPhasesToolTip = ({ active, payload, @@ -110,24 +99,6 @@ const TimingPhasesToolTip = ({ ); }; -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)} - - ); -}; - export const HistogramDetailsStacked = ({ checks = [], range, 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/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)} + + ); +}; From 0e0cace676d4a31ce8b57c87891212897a3786aa Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 29 Jul 2026 11:45:49 -0700 Subject: [PATCH 81/84] merge charts --- .../Components/design-elements/BaseChart.tsx | 7 + .../Components/monitors/charts/ChartUtils.ts | 33 +++ .../Components/monitors/charts/ChartUtils.tsx | 6 - .../monitors/charts/HistogramDetails.tsx | 158 +++++++++++-- .../charts/HistogramDetailsStacked.tsx | 207 ------------------ client/src/Pages/Uptime/Details/index.tsx | 18 -- 6 files changed, 178 insertions(+), 251 deletions(-) create mode 100644 client/src/Components/monitors/charts/ChartUtils.ts delete mode 100644 client/src/Components/monitors/charts/ChartUtils.tsx delete mode 100644 client/src/Components/monitors/charts/HistogramDetailsStacked.tsx diff --git a/client/src/Components/design-elements/BaseChart.tsx b/client/src/Components/design-elements/BaseChart.tsx index ddfd82a3e..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 ( { + 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/ChartUtils.tsx b/client/src/Components/monitors/charts/ChartUtils.tsx deleted file mode 100644 index 27c36d38b..000000000 --- a/client/src/Components/monitors/charts/ChartUtils.tsx +++ /dev/null @@ -1,6 +0,0 @@ -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; -}; diff --git a/client/src/Components/monitors/charts/HistogramDetails.tsx b/client/src/Components/monitors/charts/HistogramDetails.tsx index 3ad0e03b3..5c3970d3c 100644 --- a/client/src/Components/monitors/charts/HistogramDetails.tsx +++ b/client/src/Components/monitors/charts/HistogramDetails.tsx @@ -1,5 +1,7 @@ +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, @@ -18,11 +20,21 @@ import { useSelector } from "react-redux"; 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 { useMemo } from "react"; import { computeYAxisCap } from "@/Components/monitors/charts/ChartUtils"; +import { useMemo, useState } from "react"; +import { SPACING } from "@/Utils/Theme/constants"; +import { + PHASE_COLOR_KEYS, + PHASE_KEYS, + PHASE_LABELS, +} from "@/Components/monitors/charts/ChartUtils"; + +type DetailsCheck = GroupedCheck & + Partial>; + type ResponseTimeToolTipProps = TooltipProps & { range: string; theme: Theme; @@ -54,19 +66,71 @@ const ResponseTimeToolTip = ({ ); }; +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 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) => check.avgResponseTime || 0); return computeYAxisCap(totals); }, [checks]); + + const hasTimingPhases = useMemo( + () => (checks ?? []).some((check) => PHASE_KEYS.some((key) => (check[key] ?? 0) > 0)), + [checks] + ); + return ( } - title="Average response time (First byte)" + 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 deleted file mode 100644 index 2ef74f56b..000000000 --- a/client/src/Components/monitors/charts/HistogramDetailsStacked.tsx +++ /dev/null @@ -1,207 +0,0 @@ -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/Pages/Uptime/Details/index.tsx b/client/src/Pages/Uptime/Details/index.tsx index c4128e5e4..56724259a 100644 --- a/client/src/Pages/Uptime/Details/index.tsx +++ b/client/src/Pages/Uptime/Details/index.tsx @@ -31,10 +31,6 @@ import type { RootState } from "@/Types/state"; import { formatDateWithTz } from "@/Utils/TimeUtils"; import { t } from "i18next"; import { Typography } from "@mui/material"; -import { - HistogramDetailsStacked, - PHASE_KEYS, -} from "@/Components/monitors/charts/HistogramDetailsStacked"; const certificateDateFormat = "MMM D, YYYY h A"; @@ -79,14 +75,6 @@ const UptimeDetailsPage = () => { const monitor = monitorData?.monitor; const monitorStats = monitorDetailsData?.monitorStats ?? null; - const hasTimingPhases = useMemo( - () => - (monitorData?.groupedChecks ?? []).some((check) => - PHASE_KEYS.some((key) => check[key] > 0) - ), - [monitorData] - ); - // Certificate fetch - only for HTTP monitors const certificateUrl = useMemo(() => { if (!monitorId || monitor?.type !== "http") { @@ -238,12 +226,6 @@ const UptimeDetailsPage = () => { checks={monitorData.groupedChecks} range={dateRange} /> - {hasTimingPhases && ( - - )} Date: Wed, 29 Jul 2026 12:30:18 -0700 Subject: [PATCH 82/84] first byte to total repsonse --- .../0009_recomputeResponseTimeFromTimings.ts | 209 ++++++++++++++++++ server/src/db/migration/index.ts | 2 + server/src/service/network/HttpProvider.ts | 4 +- 3 files changed, 213 insertions(+), 2 deletions(-) create mode 100644 server/src/db/migration/0009_recomputeResponseTimeFromTimings.ts 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/service/network/HttpProvider.ts b/server/src/service/network/HttpProvider.ts index b1debd969..fb5f9fea5 100644 --- a/server/src/service/network/HttpProvider.ts +++ b/server/src/service/network/HttpProvider.ts @@ -117,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 { @@ -184,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) { From 3f088c70f8c043b08b905c1b32c110bd35c3f3b2 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 29 Jul 2026 12:30:36 -0700 Subject: [PATCH 83/84] universal ymax cap --- .../monitors/charts/HistogramDetails.tsx | 5 +- .../charts/HistogramDetailsStacked.tsx | 207 ++++++++++++++++++ 2 files changed, 211 insertions(+), 1 deletion(-) create mode 100644 client/src/Components/monitors/charts/HistogramDetailsStacked.tsx diff --git a/client/src/Components/monitors/charts/HistogramDetails.tsx b/client/src/Components/monitors/charts/HistogramDetails.tsx index 5c3970d3c..0736abd6c 100644 --- a/client/src/Components/monitors/charts/HistogramDetails.tsx +++ b/client/src/Components/monitors/charts/HistogramDetails.tsx @@ -122,7 +122,10 @@ export const HistogramDetails = ({ const theme = useTheme(); const uiTimezone = useSelector((state: RootState) => state.ui.timezone); const yMax = useMemo(() => { - const totals = checks.map((check) => check.avgResponseTime || 0); + 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]); 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) => ( + + ))} + + + + ); +}; From 0172272dc36a1bad7641c8f560d6b937f87df653 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Wed, 29 Jul 2026 12:33:56 -0700 Subject: [PATCH 84/84] update tests --- .../test/unit/providers/network/httpProvider.test.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/test/unit/providers/network/httpProvider.test.ts b/server/test/unit/providers/network/httpProvider.test.ts index 88897aca3..9ab69f25d 100644 --- a/server/test/unit/providers/network/httpProvider.test.ts +++ b/server/test/unit/providers/network/httpProvider.test.ts @@ -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();