From a6d445818f58fb626f90de3610bc9ffdbea22aca Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Thu, 14 Aug 2025 20:15:56 -0400 Subject: [PATCH 01/24] fix: improve vertical alignment of text and plus icons in filter headers Added flex alignment styles to MuiSelect-select class to ensure proper vertical alignment between text labels and plus icons in Type, Status, and State filter buttons on the Uptime page. --- client/src/Components/FilterHeader/index.jsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/src/Components/FilterHeader/index.jsx b/client/src/Components/FilterHeader/index.jsx index 36dd04081..5327dcd02 100644 --- a/client/src/Components/FilterHeader/index.jsx +++ b/client/src/Components/FilterHeader/index.jsx @@ -37,6 +37,12 @@ const FilterHeader = ({ header, options, value, onChange, multiple = true }) => displayEmpty value={controlledValue} onChange={onChange} + sx={{ + "& .MuiSelect-select": { + display: "flex", + alignItems: "center", + }, + }} renderValue={(selected) => { if (!selected?.length) { return header; From 1ecec115e4ed02b93e487df6da2c737b702d4f20 Mon Sep 17 00:00:00 2001 From: Owaise Date: Fri, 15 Aug 2025 11:28:22 +0530 Subject: [PATCH 02/24] Fixed the selector and also null points for networks. --- .../Details/Components/NetworkStats/index.jsx | 33 +++++------- .../db/mongo/modules/monitorModuleQueries.js | 54 +++++++++---------- 2 files changed, 39 insertions(+), 48 deletions(-) diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx index 227523439..cf1ae436c 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx @@ -1,8 +1,9 @@ import PropTypes from "prop-types"; import { useState, useEffect } from "react"; -import { FormControl, InputLabel, Select, MenuItem, Box } from "@mui/material"; +import { Box } from "@mui/material"; import { useTranslation } from "react-i18next"; import { useTheme } from "@emotion/react"; +import Select from "../../../../../Components/Inputs/Select"; import NetworkStatBoxes from "./NetworkStatBoxes"; import NetworkCharts from "./NetworkCharts"; import MonitorTimeFrameHeader from "../../../../../Components/MonitorTimeFrameHeader"; @@ -63,27 +64,17 @@ const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => { gap={theme.spacing(4)} > {availableInterfaces.length > 0 && ( - setSelectedInterface(e.target.value)} + items={availableInterfaces.map((interfaceName) => ({ + _id: interfaceName, + name: interfaceName + }))} sx={{ minWidth: 200 }} - > - {t("networkInterface")} - - + /> )} { { $project: { diskCount: { - $size: "$disk", + $size: { $ifNull: ["$disk", []] }, }, - netCount: { $size: "$net" }, + netCount: { $size: { $ifNull: ["$net", []] } }, }, }, { @@ -381,7 +381,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, net: { $map: { - input: { $range: [0, { $size: { $arrayElemAt: ["$net", 0] } }] }, + input: { $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$net", 0] }, []] } }] }, as: "netIndex", in: { name: { @@ -409,7 +409,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $arrayElemAt: [ { $map: { - input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + input: { $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }] }, as: "iface", in: "$$iface.bytes_sent", }, @@ -418,7 +418,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { ], }, tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] }, + tLast: { $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }] }, }, in: { $cond: [ @@ -444,7 +444,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $arrayElemAt: [ { $map: { - input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + input: { $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }] }, as: "iface", in: "$$iface.bytes_recv", }, @@ -453,7 +453,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { ], }, tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] }, + tLast: { $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }] }, }, in: { $cond: [ @@ -479,7 +479,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $arrayElemAt: [ { $map: { - input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + input: { $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }] }, as: "iface", in: "$$iface.packets_sent", }, @@ -488,7 +488,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { ], }, tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] }, + tLast: { $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }] }, }, in: { $cond: [ @@ -522,9 +522,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $map: { input: { $arrayElemAt: [ - "$net", + { $ifNull: ["$net", []] }, { - $subtract: [{ $size: "$net" }, 1], + $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1], }, ], }, @@ -538,9 +538,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { tFirst: { $arrayElemAt: ["$updatedAts", 0] }, tLast: { $arrayElemAt: [ - "$updatedAts", + { $ifNull: ["$updatedAts", []] }, { - $subtract: [{ $size: "$updatedAts" }, 1], + $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1], }, ], }, @@ -566,7 +566,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $arrayElemAt: [ { $map: { - input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, + input: { $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }] }, as: "iface", in: "$$iface.err_in", }, @@ -575,7 +575,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { ], }, tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] }, + tLast: { $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }] }, }, in: { $cond: [ @@ -609,9 +609,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $map: { input: { $arrayElemAt: [ - "$net", + { $ifNull: ["$net", []] }, { - $subtract: [{ $size: "$net" }, 1], + $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1], }, ], }, @@ -625,9 +625,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { tFirst: { $arrayElemAt: ["$updatedAts", 0] }, tLast: { $arrayElemAt: [ - "$updatedAts", + { $ifNull: ["$updatedAts", []] }, { - $subtract: [{ $size: "$updatedAts" }, 1], + $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1], }, ], }, @@ -664,9 +664,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $map: { input: { $arrayElemAt: [ - "$net", + { $ifNull: ["$net", []] }, { - $subtract: [{ $size: "$net" }, 1], + $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1], }, ], }, @@ -680,9 +680,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { tFirst: { $arrayElemAt: ["$updatedAts", 0] }, tLast: { $arrayElemAt: [ - "$updatedAts", + { $ifNull: ["$updatedAts", []] }, { - $subtract: [{ $size: "$updatedAts" }, 1], + $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1], }, ], }, @@ -719,9 +719,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $map: { input: { $arrayElemAt: [ - "$net", + { $ifNull: ["$net", []] }, { - $subtract: [{ $size: "$net" }, 1], + $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1], }, ], }, @@ -735,9 +735,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { tFirst: { $arrayElemAt: ["$updatedAts", 0] }, tLast: { $arrayElemAt: [ - "$updatedAts", + { $ifNull: ["$updatedAts", []] }, { - $subtract: [{ $size: "$updatedAts" }, 1], + $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1], }, ], }, From 88c478c5339a035fcead1e6f75ba320119f3f695 Mon Sep 17 00:00:00 2001 From: Owaise Date: Fri, 15 Aug 2025 11:37:38 +0530 Subject: [PATCH 03/24] Formatting is done. --- .../Details/Components/NetworkStats/index.jsx | 2 +- .../db/mongo/modules/monitorModuleQueries.js | 32 ++++++++++++++----- 2 files changed, 25 insertions(+), 9 deletions(-) diff --git a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx index cf1ae436c..41d2b0a87 100644 --- a/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx +++ b/client/src/Pages/Infrastructure/Details/Components/NetworkStats/index.jsx @@ -71,7 +71,7 @@ const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => { onChange={(e) => setSelectedInterface(e.target.value)} items={availableInterfaces.map((interfaceName) => ({ _id: interfaceName, - name: interfaceName + name: interfaceName, }))} sx={{ minWidth: 200 }} /> diff --git a/server/src/db/mongo/modules/monitorModuleQueries.js b/server/src/db/mongo/modules/monitorModuleQueries.js index 13352dcd4..f6d20d855 100755 --- a/server/src/db/mongo/modules/monitorModuleQueries.js +++ b/server/src/db/mongo/modules/monitorModuleQueries.js @@ -409,7 +409,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $arrayElemAt: [ { $map: { - input: { $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }] }, + input: { + $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }], + }, as: "iface", in: "$$iface.bytes_sent", }, @@ -418,7 +420,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { ], }, tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }] }, + tLast: { + $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }], + }, }, in: { $cond: [ @@ -444,7 +448,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $arrayElemAt: [ { $map: { - input: { $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }] }, + input: { + $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }], + }, as: "iface", in: "$$iface.bytes_recv", }, @@ -453,7 +459,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { ], }, tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }] }, + tLast: { + $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }], + }, }, in: { $cond: [ @@ -479,7 +487,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $arrayElemAt: [ { $map: { - input: { $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }] }, + input: { + $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }], + }, as: "iface", in: "$$iface.packets_sent", }, @@ -488,7 +498,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { ], }, tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }] }, + tLast: { + $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }], + }, }, in: { $cond: [ @@ -566,7 +578,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { $arrayElemAt: [ { $map: { - input: { $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }] }, + input: { + $arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }], + }, as: "iface", in: "$$iface.err_in", }, @@ -575,7 +589,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { ], }, tFirst: { $arrayElemAt: ["$updatedAts", 0] }, - tLast: { $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }] }, + tLast: { + $arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }], + }, }, in: { $cond: [ From 5e5daef49ad3dc44bc7291f0dd375d513a7920e0 Mon Sep 17 00:00:00 2001 From: gorkem-bwl Date: Fri, 15 Aug 2025 03:38:22 -0400 Subject: [PATCH 04/24] Move vertical alignment styling from component to theme level - Move MuiSelect vertical alignment from FilterHeader component to globalTheme.js - Ensures consistent vertical alignment for all Select components application-wide - Removes component-specific styling in favor of theme-level consistency - Addresses reviewer feedback to make this a theme-level change Components already with custom Select styling will continue to work as local styles override theme styles. --- client/src/Components/FilterHeader/index.jsx | 6 ------ client/src/Utils/Theme/globalTheme.js | 4 ++++ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/client/src/Components/FilterHeader/index.jsx b/client/src/Components/FilterHeader/index.jsx index 5327dcd02..36dd04081 100644 --- a/client/src/Components/FilterHeader/index.jsx +++ b/client/src/Components/FilterHeader/index.jsx @@ -37,12 +37,6 @@ const FilterHeader = ({ header, options, value, onChange, multiple = true }) => displayEmpty value={controlledValue} onChange={onChange} - sx={{ - "& .MuiSelect-select": { - display: "flex", - alignItems: "center", - }, - }} renderValue={(selected) => { if (!selected?.length) { return header; diff --git a/client/src/Utils/Theme/globalTheme.js b/client/src/Utils/Theme/globalTheme.js index ce58153ec..bda415c90 100644 --- a/client/src/Utils/Theme/globalTheme.js +++ b/client/src/Utils/Theme/globalTheme.js @@ -564,6 +564,10 @@ const baseTheme = (palette) => ({ "& .MuiSelect-icon": { color: theme.palette.primary.contrastTextSecondary, // Dropdown + color }, + "& .MuiSelect-select": { + display: "flex", + alignItems: "center", + }, "&:hover": { backgroundColor: theme.palette.primary.main, // Background on hover }, From 8aee01e008e5cb0d66bde74124277022188d6ef6 Mon Sep 17 00:00:00 2001 From: Matt Stein Date: Fri, 15 Aug 2025 14:34:42 -0700 Subject: [PATCH 05/24] Fix PageSpeed settings validation. --- client/src/Pages/PageSpeed/Create/index.jsx | 2 +- client/src/Validation/validation.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/client/src/Pages/PageSpeed/Create/index.jsx b/client/src/Pages/PageSpeed/Create/index.jsx index 78709093a..72f282b83 100644 --- a/client/src/Pages/PageSpeed/Create/index.jsx +++ b/client/src/Pages/PageSpeed/Create/index.jsx @@ -152,7 +152,7 @@ const PageSpeedSetup = () => { }); const { error } = monitorValidation.validate( - { [name]: value }, + { ...monitor, [name]: value }, { abortEarly: false } ); setErrors((prev) => ({ diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index de248f87b..efb94ea2c 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -213,6 +213,7 @@ const monitorValidation = joi.object({ }), otherwise: joi.string().allow(null, ""), }), + notifications: joi.array().optional(), }); const imageValidation = joi.object({ From e3fde8d5646a9c2363ff1c450a6fac3583ecb20c Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 15 Aug 2025 16:02:52 -0700 Subject: [PATCH 06/24] add got --- server/package-lock.json | 190 +++++++++++++++++++++++++++++++-------- server/package.json | 1 + 2 files changed, 156 insertions(+), 35 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 3084a5361..b761c4d79 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -21,6 +21,7 @@ "express": "^4.19.2", "express-rate-limit": "8.0.1", "gamedig": "^5.3.1", + "got": "14.4.7", "handlebars": "^4.7.8", "helmet": "^8.0.0", "ioredis": "^5.4.2", @@ -1137,6 +1138,12 @@ "hasInstallScript": true, "license": "Apache-2.0" }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "license": "MIT" + }, "node_modules/@sideway/address": { "version": "4.1.5", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", @@ -1159,12 +1166,12 @@ "license": "BSD-3-Clause" }, "node_modules/@sindresorhus/is": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", - "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.2.tgz", + "integrity": "sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==", "license": "MIT", "engines": { - "node": ">=14.16" + "node": ">=18" }, "funding": { "url": "https://github.com/sindresorhus/is?sponsor=1" @@ -1821,21 +1828,21 @@ } }, "node_modules/cacheable-request": { - "version": "10.2.14", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", - "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "version": "12.0.1", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-12.0.1.tgz", + "integrity": "sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg==", "license": "MIT", "dependencies": { - "@types/http-cache-semantics": "^4.0.2", - "get-stream": "^6.0.1", + "@types/http-cache-semantics": "^4.0.4", + "get-stream": "^9.0.1", "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.3", + "keyv": "^4.5.4", "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", + "normalize-url": "^8.0.1", "responselike": "^3.0.0" }, "engines": { - "node": ">=14.16" + "node": ">=18" } }, "node_modules/call-bind-apply-helpers": { @@ -3750,12 +3757,12 @@ } }, "node_modules/form-data-encoder": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", - "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz", + "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==", "license": "MIT", "engines": { - "node": ">= 14.17" + "node": ">= 18" } }, "node_modules/formdata-polyfill": { @@ -3840,6 +3847,82 @@ "node": ">=16.20.0" } }, + "node_modules/gamedig/node_modules/@sindresorhus/is": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", + "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/gamedig/node_modules/cacheable-request": { + "version": "10.2.14", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", + "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "^4.0.2", + "get-stream": "^6.0.1", + "http-cache-semantics": "^4.1.1", + "keyv": "^4.5.3", + "mimic-response": "^4.0.0", + "normalize-url": "^8.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + } + }, + "node_modules/gamedig/node_modules/form-data-encoder": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", + "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", + "license": "MIT", + "engines": { + "node": ">= 14.17" + } + }, + "node_modules/gamedig/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gamedig/node_modules/got": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", + "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==", + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^5.2.0", + "@szmarczak/http-timer": "^5.0.1", + "cacheable-lookup": "^7.0.0", + "cacheable-request": "^10.2.8", + "decompress-response": "^6.0.0", + "form-data-encoder": "^2.1.2", + "get-stream": "^6.0.1", + "http2-wrapper": "^2.1.10", + "lowercase-keys": "^3.0.0", + "p-cancelable": "^3.0.0", + "responselike": "^3.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/gamedig/node_modules/iconv-lite": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", @@ -3852,6 +3935,15 @@ "node": ">=0.10.0" } }, + "node_modules/gamedig/node_modules/p-cancelable": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", + "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, "node_modules/gaxios": { "version": "6.7.1", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", @@ -3930,12 +4022,28 @@ } }, "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-stream/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", "license": "MIT", "engines": { - "node": ">=10" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -4024,28 +4132,40 @@ } }, "node_modules/got": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", - "integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==", + "version": "14.4.7", + "resolved": "https://registry.npmjs.org/got/-/got-14.4.7.tgz", + "integrity": "sha512-DI8zV1231tqiGzOiOzQWDhsBmncFW7oQDH6Zgy6pDPrqJuVZMtoSgPLLsBZQj8Jg4JFfwoOsDA8NGtLQLnIx2g==", "license": "MIT", "dependencies": { - "@sindresorhus/is": "^5.2.0", + "@sindresorhus/is": "^7.0.1", "@szmarczak/http-timer": "^5.0.1", "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", + "cacheable-request": "^12.0.1", "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", + "form-data-encoder": "^4.0.2", + "http2-wrapper": "^2.2.1", "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" + "p-cancelable": "^4.0.1", + "responselike": "^3.0.0", + "type-fest": "^4.26.1" }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, + "node_modules/got/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=16" }, "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/handlebars": { @@ -6350,12 +6470,12 @@ } }, "node_modules/p-cancelable": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", - "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz", + "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==", "license": "MIT", "engines": { - "node": ">=12.20" + "node": ">=14.16" } }, "node_modules/p-limit": { diff --git a/server/package.json b/server/package.json index 929fab58a..98738aece 100755 --- a/server/package.json +++ b/server/package.json @@ -28,6 +28,7 @@ "express": "^4.19.2", "express-rate-limit": "8.0.1", "gamedig": "^5.3.1", + "got": "14.4.7", "handlebars": "^4.7.8", "helmet": "^8.0.0", "ioredis": "^5.4.2", From 9925e10ffc8c323f7f3a17ec6a7e174b1a1a57fd Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 15 Aug 2025 16:03:12 -0700 Subject: [PATCH 07/24] add timings to model --- server/src/db/models/Check.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/server/src/db/models/Check.js b/server/src/db/models/Check.js index 8bb6cd028..0037ffadc 100755 --- a/server/src/db/models/Check.js +++ b/server/src/db/models/Check.js @@ -36,6 +36,15 @@ const BaseCheckSchema = mongoose.Schema({ responseTime: { type: Number, }, + /** + * Timings from Got. + * + * @type {Object} + */ + timings: { + type: Object, // Accepts any object + default: {}, + }, /** * HTTP status code received during the check. * From 45ac5899753d13b1fcf2c1fe7396ee3efa319e69 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Fri, 15 Aug 2025 16:04:43 -0700 Subject: [PATCH 08/24] refactor network service --- server/src/config/services.js | 19 +- .../SuperSimpleQueue/SuperSimpleQueue.js | 1 + .../SuperSimpleQueueHelper.js | 3 +- .../service/infrastructure/networkService.js | 491 +++++++----------- .../service/infrastructure/statusService.js | 2 + 5 files changed, 206 insertions(+), 310 deletions(-) mode change 100755 => 100644 server/src/service/infrastructure/networkService.js diff --git a/server/src/config/services.js b/server/src/config/services.js index 73220128f..515fe5cfe 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -19,8 +19,10 @@ import MaintenanceWindowService from "../service/business/maintenanceWindowServi import MonitorService from "../service/business/monitorService.js"; import papaparse from "papaparse"; import axios from "axios"; +import got from "got"; import ping from "ping"; import http from "http"; +import https from "https"; import Docker from "dockerode"; import net from "net"; import fs from "fs"; @@ -32,6 +34,8 @@ import mjml2html from "mjml"; import jwt from "jsonwebtoken"; import crypto from "crypto"; import { games } from "gamedig"; +import jmespath from "jmespath"; +import { GameDig } from "gamedig"; import { fileURLToPath } from "url"; import { ObjectId } from "mongodb"; @@ -125,7 +129,20 @@ export const initializeServices = async ({ logger, envSettings, settingsService await db.connect(); - const networkService = new NetworkService(axios, ping, logger, http, Docker, net, stringService, settingsService); + const networkService = new NetworkService({ + axios, + got, + https, + jmespath, + GameDig, + ping, + logger, + http, + Docker, + net, + stringService, + settingsService, + }); const emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger); const bufferService = new BufferService({ db, logger, envSettings }); const statusService = new StatusService({ db, logger, buffer: bufferService }); diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js index 60bcb0754..47d73c0df 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueue.js @@ -54,6 +54,7 @@ class SuperSimpleQueue { id: monitorId.toString(), template: "monitor-job", repeat: monitor.interval, + active: monitor.isActive, data: monitor.toObject(), }); }; diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js index ca2e5f642..7659b1c71 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js @@ -31,7 +31,8 @@ class SuperSimpleQueueHelper { }); return; } - const networkResponse = await this.networkService.getStatus(monitor); + const networkResponse = await this.networkService.requestStatus(monitor); + if (!networkResponse) { throw new Error("No network response"); } diff --git a/server/src/service/infrastructure/networkService.js b/server/src/service/infrastructure/networkService.js old mode 100755 new mode 100644 index 8f904bf56..c624c961c --- a/server/src/service/infrastructure/networkService.js +++ b/server/src/service/infrastructure/networkService.js @@ -1,23 +1,9 @@ -import jmespath from "jmespath"; -import https from "https"; -import { GameDig } from "gamedig"; - const SERVICE_NAME = "NetworkService"; -const UPROCK_ENDPOINT = "https://api.uprock.com/checkmate/push"; -/** - * Constructs a new NetworkService instance. - * - * @param {Object} axios - The axios instance for HTTP requests. - * @param {Object} ping - The ping utility for network checks. - * @param {Object} logger - The logger instance for logging. - * @param {Object} http - The HTTP utility for network operations. - * @param {Object} net - The net utility for network operations. - */ class NetworkService { static SERVICE_NAME = SERVICE_NAME; - constructor(axios, ping, logger, http, Docker, net, stringService, settingsService) { + constructor({ axios, got, https, jmespath, GameDig, ping, logger, http, Docker, net, stringService, settingsService }) { this.TYPE_PING = "ping"; this.TYPE_HTTP = "http"; this.TYPE_PAGESPEED = "pagespeed"; @@ -29,6 +15,10 @@ class NetworkService { this.NETWORK_ERROR = 5000; this.PING_ERROR = 5001; this.axios = axios; + this.got = got; + this.https = https; + this.jmespath = jmespath; + this.GameDig = GameDig; this.ping = ping; this.logger = logger; this.http = http; @@ -38,151 +28,134 @@ class NetworkService { this.settingsService = settingsService; } - get serviceName() { - return NetworkService.SERVICE_NAME; - } - - /** - * Times the execution of an asynchronous operation. - * - * @param {Function} operation - The asynchronous operation to be timed. - * @returns {Promise} An object containing the response, response time, and optionally an error. - * @property {Object|null} response - The response from the operation, or null if an error occurred. - * @property {number} responseTime - The time taken for the operation to complete, in milliseconds. - * @property {Error} [error] - The error object if an error occurred during the operation. - */ + // Helper functions async timeRequest(operation) { - const startTime = Date.now(); + const start = process.hrtime.bigint(); try { const response = await operation(); - const endTime = Date.now(); - const responseTime = endTime - startTime; - return { response, responseTime }; + const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000); + return { response, responseTime: elapsedMs }; } catch (error) { - const endTime = Date.now(); - const responseTime = endTime - startTime; - return { response: null, responseTime, error }; + const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000); + return { response: null, responseTime: elapsedMs, error }; + } + } + + // Main entry point + async requestStatus(monitor) { + const type = monitor?.type || "unknown"; + switch (type) { + case this.TYPE_PING: + return await this.requestPing(monitor); + case this.TYPE_HTTP: + return await this.requestHttp(monitor); + case this.TYPE_PAGESPEED: + return await this.requestPageSpeed(monitor); + case this.TYPE_HARDWARE: + return await this.requestHardware(monitor); + case this.TYPE_DOCKER: + return await this.requestDocker(monitor); + case this.TYPE_PORT: + return await this.requestPort(monitor); + case this.TYPE_GAME: + return await this.requestGame(monitor); + default: + return await this.handleUnsupportedType(type); } } - /** - * Performs a ping check to a specified host to verify its availability. - * @async - * @param {Object} monitor - The monitor configuration object - * @param {string} monitor.url - The host URL to ping - * @param {string} monitor._id - The unique identifier of the monitor - * @returns {Promise} A promise that resolves to a ping response object - * @returns {string} pingResponse.monitorId - The ID of the monitor - * @returns {string} pingResponse.type - The type of monitor (always "ping") - * @returns {number} pingResponse.responseTime - The time taken for the ping - * @returns {Object} pingResponse.payload - The raw ping response data - * @returns {boolean} pingResponse.status - Whether the host is alive (true) or not (false) - * @returns {number} pingResponse.code - Status code (200 for success, PING_ERROR for failure) - * @returns {string} pingResponse.message - Success or failure message - * @throws {Error} If there's an error during the ping operation - */ async requestPing(monitor) { try { - const url = monitor.url; + if (!monitor?.url) { + throw new Error("Monitor URL is required"); + } + const { response, responseTime, error } = await this.timeRequest(() => this.ping.promise.probe(url)); + if (!response) { + throw new Error("Ping failed - no result returned"); + } + const pingResponse = { monitorId: monitor._id, type: "ping", + status: response.alive, + code: 200, responseTime, + message: "Success", payload: response, }; + if (error) { pingResponse.status = false; - pingResponse.code = this.PING_ERROR; - pingResponse.message = "No response"; + pingResponse.code = 200; + pingResponse.message = "Ping failed"; return pingResponse; } - pingResponse.code = 200; - pingResponse.status = response.alive; - pingResponse.message = "Success"; return pingResponse; - } catch (error) { - error.service = this.SERVICE_NAME; - error.method = "requestPing"; - throw error; + } catch (err) { + err.service = this.SERVICE_NAME; + err.method = "requestPing"; + throw err; } } - /** - * Performs an HTTP GET request to a specified URL with optional validation of response data. - * @async - * @param {Object} monitor - The monitor configuration object - * @param {string} monitor.url - The URL to make the HTTP request to - * @param {string} [monitor.secret] - Optional Bearer token for authentication - * @param {string} monitor._id - The unique identifier of the monitor - * @param {string} monitor.name - The name of the monitor - * @param {string} monitor.teamId - The team ID associated with the monitor - * @param {string} monitor.type - The type of monitor - * @param {boolean} [monitor.ignoreTlsErrors] - Whether to ignore TLS certificate errors - * @param {string} [monitor.jsonPath] - Optional JMESPath expression to extract data from JSON response - * @param {string} [monitor.matchMethod] - Method to match response data ('include', 'regex', or exact match) - * @param {string} [monitor.expectedValue] - Expected value to match against response data - * @returns {Promise} A promise that resolves to an HTTP response object - * @returns {string} httpResponse.monitorId - The ID of the monitor - * @returns {string} httpResponse.teamId - The team ID - * @returns {string} httpResponse.type - The type of monitor - * @returns {number} httpResponse.responseTime - The time taken for the request - * @returns {Object} httpResponse.payload - The response data - * @returns {boolean} httpResponse.status - Whether the request was successful and matched expected value (if specified) - * @returns {number} httpResponse.code - HTTP status code or NETWORK_ERROR - * @returns {string} httpResponse.message - Success or failure message - * @throws {Error} If there's an error during the HTTP request or data validation - */ async requestHttp(monitor) { + const { url, secret, _id, name, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor; + const httpResponse = { + monitorId: _id, + teamId: teamId, + type, + }; + try { - const { url, secret, _id, name, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor; - const config = {}; - - secret !== undefined && (config.headers = { Authorization: `Bearer ${secret}` }); - - if (ignoreTlsErrors === true) { - config.httpsAgent = new https.Agent({ - rejectUnauthorized: false, - }); + if (!url) { + throw new Error("Monitor URL is required"); } - - const { response, responseTime, error } = await this.timeRequest(() => this.axios.get(url, config)); - - const httpResponse = { - monitorId: _id, - teamId, - type, - responseTime, - payload: response?.data, + const config = { + headers: secret ? { Authorization: `Bearer ${secret}` } : undefined, }; - if (error) { - const code = error.response?.status || this.NETWORK_ERROR; - httpResponse.code = code; - httpResponse.status = false; - httpResponse.message = this.http.STATUS_CODES[code] || this.stringService.httpNetworkError; - return httpResponse; + if (ignoreTlsErrors) { + config.agent = { + https: new this.https.Agent({ + rejectUnauthorized: false, + }), + }; } - httpResponse.code = response.status; + const response = await this.got(url, config); + + let payload; + const contentType = response.headers["content-type"]; + + if (contentType && contentType.includes("application/json")) { + try { + payload = JSON.parse(response.body); + } catch { + payload = response.body; + } + } else { + payload = response.body; + } + + httpResponse.code = response.statusCode; + httpResponse.status = response.ok; + httpResponse.message = response.statusMessage; + httpResponse.responseTime = response.timings.phases.total || 0; + httpResponse.payload = payload; + httpResponse.timings = response.timings || {}; if (!expectedValue) { - // not configure expected value, return - httpResponse.status = true; - httpResponse.message = this.http.STATUS_CODES[response.status]; return httpResponse; } - // validate if response data match expected value - let result = response?.data; - this.logger.info({ service: this.SERVICE_NAME, method: "requestHttp", message: `Job: [${name}](${_id}) match result with expected value`, - details: { expectedValue, result, jsonPath, matchMethod }, + details: { expectedValue, payload, jsonPath, matchMethod }, }); if (jsonPath) { @@ -196,106 +169,75 @@ class NetworkService { } try { - result = jmespath.search(result, jsonPath); - } catch (error) { + this.jmespath.search(payload, jsonPath); + } catch { httpResponse.status = false; httpResponse.message = this.stringService.httpJsonPathError; return httpResponse; } } - - if (result === null || result === undefined) { + return httpResponse; + } catch (err) { + if (err.name === "HTTPError" || err.name === "RequestError") { + httpResponse.code = err?.response?.statusCode || this.NETWORK_ERROR; httpResponse.status = false; - httpResponse.message = this.stringService.httpEmptyResult; + httpResponse.message = err?.response?.statusCode || err.message; + httpResponse.responseTime = err?.timings?.phases?.total || 0; + httpResponse.payload = null; + httpResponse.timings = err?.timings || {}; return httpResponse; } - - let match; - result = typeof result === "object" ? JSON.stringify(result) : result.toString(); - if (matchMethod === "include") match = result.includes(expectedValue); - else if (matchMethod === "regex") match = new RegExp(expectedValue).test(result); - else match = result === expectedValue; - - httpResponse.status = match; - httpResponse.message = match ? this.stringService.httpMatchSuccess : this.stringService.httpMatchFail; - return httpResponse; - } catch (error) { - error.service = this.SERVICE_NAME; - error.method = "requestHttp"; - throw error; + err.service = this.SERVICE_NAME; + err.method = "requestHttp"; + throw err; } } - /** - * Checks the performance of a webpage using Google's PageSpeed Insights API. - * @async - * @param {Object} monitor - The monitor configuration object - * @param {string} monitor.url - The URL of the webpage to analyze - * @returns {Promise} A promise that resolves to a pagespeed response object or undefined if API key is missing - * @returns {string} response.monitorId - The ID of the monitor - * @returns {string} response.type - The type of monitor - * @returns {number} response.responseTime - The time taken for the analysis - * @returns {boolean} response.status - Whether the analysis was successful - * @returns {number} response.code - HTTP status code from the PageSpeed API - * @returns {string} response.message - Success or failure message - * @returns {Object} response.payload - The PageSpeed analysis results - * @throws {Error} If there's an error during the PageSpeed analysis - */ - async requestPagespeed(monitor) { + async requestPageSpeed(monitor) { try { const url = monitor.url; - const updatedMonitor = { ...monitor }; - let pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`; - + if (!url) { + throw new Error("Monitor URL is required"); + } + let pageSpeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`; const dbSettings = await this.settingsService.getDBSettings(); if (dbSettings?.pagespeedApiKey) { - pagespeedUrl += `&key=${dbSettings.pagespeedApiKey}`; + pageSpeedUrl += `&key=${dbSettings.pagespeedApiKey}`; } else { this.logger.warn({ - message: "Pagespeed API key not found, job not executed", + message: "PageSpeed API key not found, job not executed", service: this.SERVICE_NAME, method: "requestPagespeed", details: { url }, }); - return; } - updatedMonitor.url = pagespeedUrl; - return await this.requestHttp(updatedMonitor); - } catch (error) { - error.service = this.SERVICE_NAME; - error.method = "requestPagespeed"; - throw error; + return await this.requestHttp({ + ...monitor, + url: pageSpeedUrl, + }); + } catch (err) { + err.service = this.SERVICE_NAME; + err.method = "requestPageSpeed"; + throw err; } } async requestHardware(monitor) { try { return await this.requestHttp(monitor); - } catch (error) { - error.service = this.SERVICE_NAME; - error.method = "requestHardware"; - throw error; + } catch (err) { + err.service = this.SERVICE_NAME; + err.method = "requestHardware"; + throw err; } } - /** - * Checks the status of a Docker container by its ID. - * @async - * @param {Object} monitor - The monitor configuration object - * @param {string} monitor.url - The Docker container ID to check - * @param {string} monitor._id - The unique identifier of the monitor - * @param {string} monitor.type - The type of monitor - * @returns {Promise} A promise that resolves to a docker response object - * @returns {string} dockerResponse.monitorId - The ID of the monitor - * @returns {string} dockerResponse.type - The type of monitor - * @returns {number} dockerResponse.responseTime - The time taken for the container inspection - * @returns {boolean} dockerResponse.status - Whether the container is running (true) or not (false) - * @returns {number} dockerResponse.code - HTTP-like status code (200 for success, NETWORK_ERROR for failure) - * @returns {string} dockerResponse.message - Success or failure message - * @throws {Error} If the container is not found or if there's an error inspecting the container - */ async requestDocker(monitor) { try { + if (!monitor.url) { + throw new Error("Monitor URL is required"); + } + const docker = new this.Docker({ socketPath: "/var/run/docker.sock", handleError: true, // Enable error handling @@ -306,14 +248,17 @@ class NetworkService { if (!containerExists) { throw new Error(this.stringService.dockerNotFound); } - const container = docker.getContainer(monitor.url); + const container = docker.getContainer(monitor.url); const { response, responseTime, error } = await this.timeRequest(() => container.inspect()); const dockerResponse = { monitorId: monitor._id, type: monitor.type, responseTime, + status: response?.State?.Status === "running" ? true : false, + code: 200, + message: "Docker container status fetched successfully", }; if (error) { @@ -322,34 +267,15 @@ class NetworkService { dockerResponse.message = error.reason || "Failed to fetch Docker container information"; return dockerResponse; } - dockerResponse.status = response?.State?.Status === "running" ? true : false; - dockerResponse.code = 200; - dockerResponse.message = "Docker container status fetched successfully"; + return dockerResponse; - } catch (error) { - error.service = this.SERVICE_NAME; - error.method = "requestDocker"; - throw error; + } catch (err) { + err.service = this.SERVICE_NAME; + err.method = "requestDocker"; + throw err; } } - /** - * Attempts to establish a TCP connection to a specified host and port. - * @async - * @param {Object} monitor - The monitor configuration object - * @param {string} monitor.url - The host URL to connect to - * @param {number} monitor.port - The port number to connect to - * @param {string} monitor._id - The unique identifier of the monitor - * @param {string} monitor.type - The type of monitor - * @returns {Promise} A promise that resolves to a port response object - * @returns {string} portResponse.monitorId - The ID of the monitor - * @returns {string} portResponse.type - The type of monitor - * @returns {number} portResponse.responseTime - The time taken for the connection attempt - * @returns {boolean} portResponse.status - Whether the connection was successful - * @returns {number} portResponse.code - HTTP-like status code (200 for success, NETWORK_ERROR for failure) - * @returns {string} portResponse.message - Success or failure message - * @throws {Error} If the connection times out or encounters an error - */ async requestPort(monitor) { try { const { url, port } = monitor; @@ -381,21 +307,21 @@ class NetworkService { }); const portResponse = { + code: 200, + status: response.success, + message: this.stringService.portSuccess, monitorId: monitor._id, type: monitor.type, responseTime, }; if (error) { - portResponse.status = false; portResponse.code = this.NETWORK_ERROR; + portResponse.status = false; portResponse.message = this.stringService.portFail; return portResponse; } - portResponse.status = response.success; - portResponse.code = 200; - portResponse.message = this.stringService.portSuccess; return portResponse; } catch (error) { error.service = this.SERVICE_NAME; @@ -404,19 +330,55 @@ class NetworkService { } } - /** - * Handles unsupported job types by throwing an error with details. - * - * @param {string} type - The unsupported job type that was provided - * @throws {Error} An error with service name, method name and unsupported type message - */ - handleUnsupportedType(type) { + async requestGame(monitor) { + try { + const { url, port, gameId } = monitor; + + const gameResponse = { + code: 200, + status: true, + message: "Success", + monitorId: monitor._id, + type: "game", + }; + + const state = await this.GameDig.query({ + type: gameId, + host: url, + port: port, + }).catch((error) => { + this.logger.warn({ + message: error.message, + service: this.SERVICE_NAME, + method: "requestGame", + details: { url, port, gameId }, + }); + }); + + if (!state) { + gameResponse.code = this.NETWORK_ERROR; + gameResponse.status = false; + gameResponse.message = "No response"; + return gameResponse; + } + + gameResponse.responseTime = state.ping; + gameResponse.payload = state; + return gameResponse; + } catch (error) { + error.service = this.SERVICE_NAME; + error.method = "requestPing"; + throw error; + } + } + async handleUnsupportedType(type) { const err = new Error(`Unsupported type: ${type}`); err.service = this.SERVICE_NAME; err.method = "getStatus"; throw err; } + // Other network requests unrelated to monitoring: async requestWebhook(type, url, body) { try { const response = await this.axios.post(url, body, { @@ -471,93 +433,6 @@ class NetworkService { throw error; } } - - /** - * Requests the status of a game monitor. - * - * @param {Object} monitor - The monitor object to request the status for. - * @returns {Promise} The response from the game status request. - * @throws {Error} Throws an error if the request fails or if the monitor is not configured correctly. - * @property {string} monitorId - The ID of the monitor. - * @property {string} type - The type of the monitor (should be "game"). - * @property {number} responseTime - The time taken for the request. - * @property {Object|null} payload - The game state response or null if the request failed. - * @property {boolean} status - Indicates if the request was successful (true) or not (false). - * @property {number} code - The status code of the request (200 for success, NETWORK_ERROR for failure). - * @property {string} message - A message indicating the result of the request. - */ - async requestGame(monitor) { - try { - const { url, port, gameId } = monitor; - - const gameResponse = { - code: 200, - status: true, - message: "Success", - monitorId: monitor._id, - type: "game", - }; - - const state = await GameDig.query({ - type: gameId, - host: url, - port: port, - }).catch((error) => { - this.logger.warn({ - message: error.message, - service: this.SERVICE_NAME, - method: "requestGame", - details: { url, port, gameId }, - }); - }); - - if (!state) { - gameResponse.status = false; - gameResponse.code = this.NETWORK_ERROR; - gameResponse.message = "No response"; - return gameResponse; - } - - gameResponse.responseTime = state.ping; - gameResponse.payload = state; - return gameResponse; - } catch (error) { - error.service = this.SERVICE_NAME; - error.method = "requestPing"; - throw error; - } - } - - /** - * Gets the status of a job based on its type and returns the appropriate response. - * - * @param {Object} job - The job object containing the data for the status request. - * @param {Object} job.data - The data object within the job. - * @param {string} job.data.type - The type of the job (e.g., "ping", "http", "pagespeed", "hardware"). - * @returns {Promise} The response object from the appropriate request method. - * @throws {Error} Throws an error if the job type is unsupported. - */ - async getStatus(monitor) { - const type = monitor.type ?? "unknown"; - switch (type) { - case this.TYPE_PING: - return await this.requestPing(monitor); - case this.TYPE_HTTP: - return await this.requestHttp(monitor); - case this.TYPE_PAGESPEED: - return await this.requestPagespeed(monitor); - case this.TYPE_HARDWARE: - return await this.requestHardware(monitor); - case this.TYPE_DOCKER: - return await this.requestDocker(monitor); - case this.TYPE_PORT: - return await this.requestPort(monitor); - case this.TYPE_GAME: - return await this.requestGame(monitor); - default: - return this.handleUnsupportedType(type); - } - } } export default NetworkService; diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index 72916a0ce..4a98996fb 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -192,6 +192,7 @@ class StatusService { conn_took, connect_took, tls_took, + timings, } = networkResponse; const check = { @@ -200,6 +201,7 @@ class StatusService { status, statusCode: code, responseTime, + timings: timings || {}, message, first_byte_took, body_read_took, From 7178f537a86400b484d59aaa4d7e3ebf5f01f261 Mon Sep 17 00:00:00 2001 From: Matt Stein Date: Fri, 15 Aug 2025 16:40:21 -0700 Subject: [PATCH 09/24] Update with changes from code review. --- client/src/Pages/PageSpeed/Create/index.jsx | 2 +- client/src/Validation/validation.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/client/src/Pages/PageSpeed/Create/index.jsx b/client/src/Pages/PageSpeed/Create/index.jsx index 72f282b83..46bc7f977 100644 --- a/client/src/Pages/PageSpeed/Create/index.jsx +++ b/client/src/Pages/PageSpeed/Create/index.jsx @@ -152,7 +152,7 @@ const PageSpeedSetup = () => { }); const { error } = monitorValidation.validate( - { ...monitor, [name]: value }, + { [name]: value, type: monitor.type }, { abortEarly: false } ); setErrors((prev) => ({ diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index efb94ea2c..de248f87b 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -213,7 +213,6 @@ const monitorValidation = joi.object({ }), otherwise: joi.string().allow(null, ""), }), - notifications: joi.array().optional(), }); const imageValidation = joi.object({ From 56025f6d5669c14cfb86818ea3f36c10afa8ca79 Mon Sep 17 00:00:00 2001 From: karenvicent Date: Sun, 17 Aug 2025 15:28:06 -0400 Subject: [PATCH 10/24] refactor create infraestructure monitor page --- .../Create/Components/CustomAlertsSection.jsx | 87 ++++ .../Components/MonitorActionButtons.jsx | 78 ++++ .../Create/Components/MonitorStatusHeader.jsx | 73 ++++ .../hooks/useInfrastructureMonitorForm.jsx | 90 ++++ .../Create/hooks/useInfrastructureSubmit.jsx | 80 ++++ .../hooks/useValidateInfrastructureForm.jsx | 37 ++ .../src/Pages/Infrastructure/Create/index.jsx | 392 ++---------------- 7 files changed, 486 insertions(+), 351 deletions(-) create mode 100644 client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx create mode 100644 client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx create mode 100644 client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx create mode 100644 client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx create mode 100644 client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx create mode 100644 client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx diff --git a/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx b/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx new file mode 100644 index 000000000..5a6f5e981 --- /dev/null +++ b/client/src/Pages/Infrastructure/Create/Components/CustomAlertsSection.jsx @@ -0,0 +1,87 @@ +import ConfigBox from "../../../../Components/ConfigBox"; +import { Box, Stack, Typography } from "@mui/material"; +import { CustomThreshold } from "../Components/CustomThreshold"; +import { capitalizeFirstLetter } from "../../../../Utils/stringUtils"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import PropTypes from "prop-types"; +const CustomAlertsSection = ({ + errors, + onChange, + infrastructureMonitor, + handleCheckboxChange, +}) => { + const theme = useTheme(); + const { t } = useTranslation(); + + const METRICS = ["cpu", "memory", "disk", "temperature"]; + const METRIC_PREFIX = "usage_"; + const hasAlertError = (errors) => { + return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0; + }; + const getAlertError = (errors) => { + const errorKey = Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX)); + return errorKey ? errors[errorKey] : null; + }; + return ( + + + + {t("infrastructureCustomizeAlerts")} + + + {t("infrastructureAlertNotificationDescription")} + + + + {METRICS.map((metric) => { + return ( + + ); + })} + + {hasAlertError(errors) && ( + + {getAlertError(errors)} + + )} + + + ); +}; + +CustomAlertsSection.propTypes = { + errors: PropTypes.object.isRequired, + onChange: PropTypes.func.isRequired, + infrastructureMonitor: PropTypes.object.isRequired, + handleCheckboxChange: PropTypes.func.isRequired, +}; + +export default CustomAlertsSection; diff --git a/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx b/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx new file mode 100644 index 000000000..becda3e3a --- /dev/null +++ b/client/src/Pages/Infrastructure/Create/Components/MonitorActionButtons.jsx @@ -0,0 +1,78 @@ +import { useState } from "react"; +import { Box, Button } from "@mui/material"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline"; +import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; +import Dialog from "../../../../Components/Dialog"; +import PropTypes from "prop-types"; + +const MonitorActionButtons = ({ monitor, isBusy, handlePause, handleRemove }) => { + const theme = useTheme(); + const { t } = useTranslation(); + const [isOpen, setIsOpen] = useState(false); + + return ( + + + + setIsOpen(false)} + confirmationButtonLabel={t("delete")} + onConfirm={handleRemove} + /> + + ); +}; + +MonitorActionButtons.propTypes = { + monitor: PropTypes.object.isRequired, + isBusy: PropTypes.bool.isRequired, + handlePause: PropTypes.func.isRequired, + handleRemove: PropTypes.func.isRequired, +}; + +export default MonitorActionButtons; diff --git a/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx b/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx new file mode 100644 index 000000000..2ed48bf64 --- /dev/null +++ b/client/src/Pages/Infrastructure/Create/Components/MonitorStatusHeader.jsx @@ -0,0 +1,73 @@ +import { Box, Stack, Tooltip, Typography } from "@mui/material"; +import { useMonitorUtils } from "../../../../Hooks/useMonitorUtils"; +import { useTheme } from "@emotion/react"; +import { useTranslation } from "react-i18next"; +import PulseDot from "../../../../Components/Animated/PulseDot"; +import PropTypes from "prop-types"; +const MonitorStatusHeader = ({ monitor, infrastructureMonitor }) => { + const theme = useTheme(); + const { t } = useTranslation(); + const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils(); + return ( + + + + + + + + {infrastructureMonitor.url?.replace(/^https?:\/\//, "") || "..."} + + + {t("editing")} + + + ); +}; + +MonitorStatusHeader.propTypes = { + monitor: PropTypes.object.isRequired, + infrastructureMonitor: PropTypes.object.isRequired, +}; + +export default MonitorStatusHeader; diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx new file mode 100644 index 000000000..fa8a6ebe9 --- /dev/null +++ b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureMonitorForm.jsx @@ -0,0 +1,90 @@ +import { useState } from "react"; +const useInfrastructureMonitorForm = () => { + const [infrastructureMonitor, setInfrastructureMonitor] = useState({ + url: "", + name: "", + notifications: [], + notify_email: false, + interval: 0.25, + cpu: false, + usage_cpu: "", + memory: false, + usage_memory: "", + disk: false, + usage_disk: "", + temperature: false, + usage_temperature: "", + secret: "", + }); + + const onChangeForm = (name, value) => { + setInfrastructureMonitor({ + ...infrastructureMonitor, + [name]: value, + }); + }; + const handleCheckboxChange = (event) => { + setInfrastructureMonitor({ + ...infrastructureMonitor, + [event.target.name]: event.target.checked, + }); + }; + const initializeInfrastructureMonitorForCreate = (globalSettings) => { + const gt = globalSettings?.data?.settings?.globalThresholds || {}; + setInfrastructureMonitor({ + url: "", + name: "", + notifications: [], + interval: 0.25, + cpu: gt.cpu !== undefined, + usage_cpu: gt.cpu !== undefined ? gt.cpu.toString() : "", + memory: gt.memory !== undefined, + usage_memory: gt.memory !== undefined ? gt.memory.toString() : "", + disk: gt.disk !== undefined, + usage_disk: gt.disk !== undefined ? gt.disk.toString() : "", + temperature: gt.temperature !== undefined, + usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "", + secret: "", + }); + }; + + const initializeInfrastructureMonitorForUpdate = (monitor) => { + const MS_PER_MINUTE = 60000; + const { thresholds = {} } = monitor; + setInfrastructureMonitor({ + url: monitor.url.replace(/^https?:\/\//, ""), + name: monitor.name || "", + notifications: monitor.notifications || [], + interval: monitor.interval / MS_PER_MINUTE, + cpu: thresholds.usage_cpu !== undefined, + usage_cpu: + thresholds.usage_cpu !== undefined ? (thresholds.usage_cpu * 100).toString() : "", + memory: thresholds.usage_memory !== undefined, + usage_memory: + thresholds.usage_memory !== undefined + ? (thresholds.usage_memory * 100).toString() + : "", + disk: thresholds.usage_disk !== undefined, + usage_disk: + thresholds.usage_disk !== undefined + ? (thresholds.usage_disk * 100).toString() + : "", + temperature: thresholds.usage_temperature !== undefined, + usage_temperature: + thresholds.usage_temperature !== undefined + ? (thresholds.usage_temperature * 100).toString() + : "", + secret: monitor.secret || "", + }); + }; + return { + infrastructureMonitor, + setInfrastructureMonitor, + onChangeForm, + handleCheckboxChange, + initializeInfrastructureMonitorForCreate, + initializeInfrastructureMonitorForUpdate, + }; +}; + +export default useInfrastructureMonitorForm; diff --git a/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx new file mode 100644 index 000000000..da564ae7c --- /dev/null +++ b/client/src/Pages/Infrastructure/Create/hooks/useInfrastructureSubmit.jsx @@ -0,0 +1,80 @@ +import { useCreateMonitor, useUpdateMonitor } from "../../../../Hooks/monitorHooks"; +const useInfrastructureSubmit = () => { + const [createMonitor, isCreating] = useCreateMonitor(); + const [updateMonitor, isUpdating] = useUpdateMonitor(); + const buildForm = (infrastructureMonitor, https) => { + const MS_PER_MINUTE = 60000; + + let form = { + url: `http${https ? "s" : ""}://` + infrastructureMonitor.url, + name: + infrastructureMonitor.name === "" + ? infrastructureMonitor.url + : infrastructureMonitor.name, + interval: infrastructureMonitor.interval * MS_PER_MINUTE, + cpu: infrastructureMonitor.cpu, + ...(infrastructureMonitor.cpu + ? { usage_cpu: infrastructureMonitor.usage_cpu } + : {}), + memory: infrastructureMonitor.memory, + ...(infrastructureMonitor.memory + ? { usage_memory: infrastructureMonitor.usage_memory } + : {}), + disk: infrastructureMonitor.disk, + ...(infrastructureMonitor.disk + ? { usage_disk: infrastructureMonitor.usage_disk } + : {}), + temperature: infrastructureMonitor.temperature, + ...(infrastructureMonitor.temperature + ? { usage_temperature: infrastructureMonitor.usage_temperature } + : {}), + secret: infrastructureMonitor.secret, + }; + return form; + }; + const submitInfrastructureForm = async ( + infrastructureMonitor, + form, + isCreate, + monitorId + ) => { + const { + cpu, + usage_cpu, + memory, + usage_memory, + disk, + usage_disk, + temperature, + usage_temperature, + ...rest + } = form; + + const thresholds = { + ...(cpu ? { usage_cpu: usage_cpu / 100 } : {}), + ...(memory ? { usage_memory: usage_memory / 100 } : {}), + ...(disk ? { usage_disk: usage_disk / 100 } : {}), + ...(temperature ? { usage_temperature: usage_temperature / 100 } : {}), + }; + + const finalForm = { + ...(isCreate ? {} : { _id: monitorId }), + ...rest, + description: form.name, + type: "hardware", + notifications: infrastructureMonitor.notifications, + thresholds, + }; + // Handle create or update + isCreate + ? await createMonitor({ monitor: finalForm, redirect: "/infrastructure" }) + : await updateMonitor({ monitor: finalForm, redirect: "/infrastructure" }); + }; + return { + buildForm, + submitInfrastructureForm, + isCreating, + isUpdating, + }; +}; +export default useInfrastructureSubmit; diff --git a/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx b/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx new file mode 100644 index 000000000..080f441c4 --- /dev/null +++ b/client/src/Pages/Infrastructure/Create/hooks/useValidateInfrastructureForm.jsx @@ -0,0 +1,37 @@ +import { useState } from "react"; +import { infrastructureMonitorValidation } from "../../../../Validation/validation"; +import { createToast } from "../../../../Utils/toastUtils"; +const useValidateInfrastructureForm = () => { + const [errors, setErrors] = useState({}); + + const validateField = (name, value) => { + const { error } = infrastructureMonitorValidation.validate( + { [name]: value }, + { abortEarly: false } + ); + setErrors((prev) => ({ + ...prev, + ...(error ? { [name]: error.details[0].message } : { [name]: undefined }), + })); + }; + + const validateForm = (form) => { + const { error } = infrastructureMonitorValidation.validate(form, { + abortEarly: false, + }); + + if (error) { + const newErrors = {}; + error.details.forEach((err) => { + newErrors[err.path[0]] = err.message; + }); + console.log(newErrors); + setErrors(newErrors); + createToast({ body: "Please check the form for errors." }); + return error; + } + return null; + }; + return { errors, validateField, validateForm }; +}; +export default useValidateInfrastructureForm; diff --git a/client/src/Pages/Infrastructure/Create/index.jsx b/client/src/Pages/Infrastructure/Create/index.jsx index ae95cce33..28007c538 100644 --- a/client/src/Pages/Infrastructure/Create/index.jsx +++ b/client/src/Pages/Infrastructure/Create/index.jsx @@ -1,40 +1,33 @@ //Components import Breadcrumbs from "../../../Components/Breadcrumbs"; import ConfigBox from "../../../Components/ConfigBox"; -import Dialog from "../../../Components/Dialog"; import FieldWrapper from "../../../Components/Inputs/FieldWrapper"; import Link from "../../../Components/Link"; -import PauseCircleOutlineIcon from "@mui/icons-material/PauseCircleOutline"; -import PlayCircleOutlineRoundedIcon from "@mui/icons-material/PlayCircleOutlineRounded"; -import PulseDot from "../../../Components/Animated/PulseDot"; import Select from "../../../Components/Inputs/Select"; import TextInput from "../../../Components/Inputs/TextInput"; -import { Box, Stack, Tooltip, Typography, Button, ButtonGroup } from "@mui/material"; -import { CustomThreshold } from "./Components/CustomThreshold"; +import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material"; import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; -import { createToast } from "../../../Utils/toastUtils"; +import MonitorStatusHeader from "./Components/MonitorStatusHeader"; +import MonitorActionButtons from "./Components/MonitorActionButtons"; +import CustomAlertsSection from "./Components/CustomAlertsSection"; // Utils import NotificationsConfig from "../../../Components/NotificationConfig"; -import { capitalizeFirstLetter } from "../../../Utils/stringUtils"; -import { infrastructureMonitorValidation } from "../../../Validation/validation"; import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications"; -import { useMonitorUtils } from "../../../Hooks/useMonitorUtils"; import { useParams } from "react-router-dom"; -import { useSelector } from "react-redux"; import { useState, useEffect } from "react"; import { useTheme } from "@emotion/react"; import { useTranslation } from "react-i18next"; import { - useCreateMonitor, useDeleteMonitor, useFetchGlobalSettings, useFetchHardwareMonitorById, usePauseMonitor, - useUpdateMonitor, } from "../../../Hooks/monitorHooks"; +import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm"; +import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm"; +import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit"; const CreateInfrastructureMonitor = () => { - const { user } = useSelector((state) => state.auth); const { monitorId } = useParams(); const isCreate = typeof monitorId === "undefined"; @@ -42,39 +35,29 @@ const CreateInfrastructureMonitor = () => { const { t } = useTranslation(); // State - const [errors, setErrors] = useState({}); const [https, setHttps] = useState(false); - const [isOpen, setIsOpen] = useState(false); const [updateTrigger, setUpdateTrigger] = useState(false); - const [infrastructureMonitor, setInfrastructureMonitor] = useState({ - url: "", - name: "", - notifications: [], - notify_email: false, - interval: 0.25, - cpu: false, - usage_cpu: "", - memory: false, - usage_memory: "", - disk: false, - usage_disk: "", - temperature: false, - usage_temperature: "", - secret: "", - }); // Fetch monitor details if editing - const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils(); const [monitor, isLoading] = useFetchHardwareMonitorById({ monitorId, updateTrigger, }); - const [createMonitor, isCreating] = useCreateMonitor(); const [deleteMonitor, isDeleting] = useDeleteMonitor(); const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings(); const [notifications, notificationsAreLoading] = useGetNotificationsByTeamId(); const [pauseMonitor, isPausing] = usePauseMonitor(); - const [updateMonitor, isUpdating] = useUpdateMonitor(); + const { + infrastructureMonitor, + setInfrastructureMonitor, + onChangeForm, + handleCheckboxChange, + initializeInfrastructureMonitorForCreate, + initializeInfrastructureMonitorForUpdate, + } = useInfrastructureMonitorForm(); + const { errors, validateField, validateForm } = useValidateInfrastructureForm(); + const { buildForm, submitInfrastructureForm, isCreating, isUpdating } = + useInfrastructureSubmit(); const FREQUENCIES = [ { _id: 0.25, name: t("time.fifteenSeconds") }, @@ -93,157 +76,27 @@ const CreateInfrastructureMonitor = () => { { name: "Configure", path: `/infrastructure/configure/${monitorId}` }, ]), ]; - const METRICS = ["cpu", "memory", "disk", "temperature"]; - const METRIC_PREFIX = "usage_"; - const MS_PER_MINUTE = 60000; - - const hasAlertError = (errors) => { - return Object.keys(errors).filter((k) => k.startsWith(METRIC_PREFIX)).length > 0; - }; - - const getAlertError = (errors) => { - const errorKey = Object.keys(errors).find((key) => key.startsWith(METRIC_PREFIX)); - return errorKey ? errors[errorKey] : null; - }; - // Populate form fields if editing useEffect(() => { if (isCreate) { if (globalSettingsLoading) return; - - const gt = globalSettings?.data?.settings?.globalThresholds || {}; - setHttps(false); - - setInfrastructureMonitor({ - url: "", - name: "", - notifications: [], - interval: 0.25, - cpu: gt.cpu !== undefined, - usage_cpu: gt.cpu !== undefined ? gt.cpu.toString() : "", - memory: gt.memory !== undefined, - usage_memory: gt.memory !== undefined ? gt.memory.toString() : "", - disk: gt.disk !== undefined, - usage_disk: gt.disk !== undefined ? gt.disk.toString() : "", - temperature: gt.temperature !== undefined, - usage_temperature: gt.temperature !== undefined ? gt.temperature.toString() : "", - secret: "", - }); + initializeInfrastructureMonitorForCreate(globalSettings); } else if (monitor) { - const { thresholds = {} } = monitor; - setHttps(monitor.url.startsWith("https")); - - setInfrastructureMonitor({ - url: monitor.url.replace(/^https?:\/\//, ""), - name: monitor.name || "", - notifications: monitor.notifications || [], - interval: monitor.interval / MS_PER_MINUTE, - cpu: thresholds.usage_cpu !== undefined, - usage_cpu: - thresholds.usage_cpu !== undefined - ? (thresholds.usage_cpu * 100).toString() - : "", - memory: thresholds.usage_memory !== undefined, - usage_memory: - thresholds.usage_memory !== undefined - ? (thresholds.usage_memory * 100).toString() - : "", - disk: thresholds.usage_disk !== undefined, - usage_disk: - thresholds.usage_disk !== undefined - ? (thresholds.usage_disk * 100).toString() - : "", - temperature: thresholds.usage_temperature !== undefined, - usage_temperature: - thresholds.usage_temperature !== undefined - ? (thresholds.usage_temperature * 100).toString() - : "", - secret: monitor.secret || "", - }); + initializeInfrastructureMonitorForUpdate(monitor); } }, [isCreate, monitor, globalSettings, globalSettingsLoading]); // Handlers const onSubmit = async (event) => { event.preventDefault(); - - // Build the form - let form = { - url: `http${https ? "s" : ""}://` + infrastructureMonitor.url, - name: - infrastructureMonitor.name === "" - ? infrastructureMonitor.url - : infrastructureMonitor.name, - interval: infrastructureMonitor.interval * MS_PER_MINUTE, - cpu: infrastructureMonitor.cpu, - ...(infrastructureMonitor.cpu - ? { usage_cpu: infrastructureMonitor.usage_cpu } - : {}), - memory: infrastructureMonitor.memory, - ...(infrastructureMonitor.memory - ? { usage_memory: infrastructureMonitor.usage_memory } - : {}), - disk: infrastructureMonitor.disk, - ...(infrastructureMonitor.disk - ? { usage_disk: infrastructureMonitor.usage_disk } - : {}), - temperature: infrastructureMonitor.temperature, - ...(infrastructureMonitor.temperature - ? { usage_temperature: infrastructureMonitor.usage_temperature } - : {}), - secret: infrastructureMonitor.secret, - }; - - const { error } = infrastructureMonitorValidation.validate(form, { - abortEarly: false, - }); - + const form = buildForm(infrastructureMonitor, https); + const error = validateForm(form); if (error) { - const newErrors = {}; - error.details.forEach((err) => { - newErrors[err.path[0]] = err.message; - }); - console.log(newErrors); - setErrors(newErrors); - createToast({ body: "Please check the form for errors." }); return; } - - // Build the thresholds for the form - const { - cpu, - usage_cpu, - memory, - usage_memory, - disk, - usage_disk, - temperature, - usage_temperature, - ...rest - } = form; - - const thresholds = { - ...(cpu ? { usage_cpu: usage_cpu / 100 } : {}), - ...(memory ? { usage_memory: usage_memory / 100 } : {}), - ...(disk ? { usage_disk: usage_disk / 100 } : {}), - ...(temperature ? { usage_temperature: usage_temperature / 100 } : {}), - }; - - form = { - ...(isCreate ? {} : { _id: monitorId }), - ...rest, - description: form.name, - type: "hardware", - notifications: infrastructureMonitor.notifications, - thresholds, - }; - - // Handle create or update - isCreate - ? await createMonitor({ monitor: form, redirect: "/infrastructure" }) - : await updateMonitor({ monitor: form, redirect: "/infrastructure" }); + submitInfrastructureForm(infrastructureMonitor, form, isCreate, monitorId); }; const triggerUpdate = () => { @@ -252,28 +105,8 @@ const CreateInfrastructureMonitor = () => { const onChange = (event) => { const { value, name } = event.target; - setInfrastructureMonitor({ - ...infrastructureMonitor, - [name]: value, - }); - - const { error } = infrastructureMonitorValidation.validate( - { [name]: value }, - { abortEarly: false } - ); - setErrors((prev) => ({ - ...prev, - ...(error ? { [name]: error.details[0].message } : { [name]: undefined }), - })); - }; - - const handleCheckboxChange = (event) => { - const { name } = event.target; - const { checked } = event.target; - setInfrastructureMonitor({ - ...infrastructureMonitor, - [name]: checked, - }); + onChangeForm(name, value); + validateField(name, value); }; const handlePause = async () => { @@ -336,105 +169,19 @@ const CreateInfrastructureMonitor = () => { )} {!isCreate && ( - - - - - - - - {infrastructureMonitor.url?.replace(/^https?:\/\//, "") || "..."} - - - {t("editing")} - - + )} {!isCreate && ( - - - - + )} @@ -534,58 +281,12 @@ const CreateInfrastructureMonitor = () => { setNotifications={infrastructureMonitor.notifications} /> - - - - {t("infrastructureCustomizeAlerts")} - - - {t("infrastructureAlertNotificationDescription")} - - - - {METRICS.map((metric) => { - return ( - - ); - })} - {/* Error text */} - {hasAlertError(errors) && ( - - {getAlertError(errors)} - - )} - - + { - {!isCreate && ( - setIsOpen(false)} - confirmationButtonLabel={t("delete")} - onConfirm={handleRemove} - /> - )} ); }; From 9f9a2be1d8137a36474198b63f6c91fe9c5fad3e Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 17 Aug 2025 15:08:12 -0700 Subject: [PATCH 11/24] fix missing URL --- server/src/service/infrastructure/networkService.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/service/infrastructure/networkService.js b/server/src/service/infrastructure/networkService.js index c624c961c..c90096d7c 100644 --- a/server/src/service/infrastructure/networkService.js +++ b/server/src/service/infrastructure/networkService.js @@ -70,7 +70,7 @@ class NetworkService { throw new Error("Monitor URL is required"); } - const { response, responseTime, error } = await this.timeRequest(() => this.ping.promise.probe(url)); + const { response, responseTime, error } = await this.timeRequest(() => this.ping.promise.probe(monitor.url)); if (!response) { throw new Error("Ping failed - no result returned"); From 993339d0d1659d362dc7d5419d998d20862d3223 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Sun, 17 Aug 2025 20:21:18 -0700 Subject: [PATCH 12/24] update pager_duty validation --- .../src/Pages/Notifications/create/index.jsx | 2 - client/src/Pages/Notifications/utils.js | 8 ++-- client/src/Validation/validation.js | 44 ++++++++++++------- 3 files changed, 33 insertions(+), 21 deletions(-) diff --git a/client/src/Pages/Notifications/create/index.jsx b/client/src/Pages/Notifications/create/index.jsx index 10c280efb..6d8afe665 100644 --- a/client/src/Pages/Notifications/create/index.jsx +++ b/client/src/Pages/Notifications/create/index.jsx @@ -89,8 +89,6 @@ const CreateNotifications = () => { error.details.forEach((err) => { newErrors[err.path[0]] = err.message; }); - console.log(JSON.stringify(newErrors)); - console.log(JSON.stringify(form, null, 2)); createToast({ body: Object.values(newErrors)[0] }); setErrors(newErrors); return; diff --git a/client/src/Pages/Notifications/utils.js b/client/src/Pages/Notifications/utils.js index 7ef6e5a60..0813f6868 100644 --- a/client/src/Pages/Notifications/utils.js +++ b/client/src/Pages/Notifications/utils.js @@ -9,7 +9,7 @@ export const NOTIFICATION_TYPES = [ export const TITLE_MAP = { email: "createNotifications.emailSettings.title", slack: "createNotifications.slackSettings.title", - pager_duty: "createNotifications.pagerDutySettings.title", + pager_duty: "createNotifications.pagerdutySettings.title", webhook: "createNotifications.webhookSettings.title", discord: "createNotifications.discordSettings.title", }; @@ -17,7 +17,7 @@ export const TITLE_MAP = { export const DESCRIPTION_MAP = { email: "createNotifications.emailSettings.description", slack: "createNotifications.slackSettings.description", - pager_duty: "createNotifications.pagerDutySettings.description", + pager_duty: "createNotifications.pagerdutySettings.description", webhook: "createNotifications.webhookSettings.description", discord: "createNotifications.discordSettings.description", }; @@ -25,7 +25,7 @@ export const DESCRIPTION_MAP = { export const LABEL_MAP = { email: "createNotifications.emailSettings.emailLabel", slack: "createNotifications.slackSettings.webhookLabel", - pager_duty: "createNotifications.pagerDutySettings.integrationKeyLabel", + pager_duty: "createNotifications.pagerdutySettings.integrationKeyLabel", webhook: "createNotifications.webhookSettings.webhookLabel", discord: "createNotifications.discordSettings.webhookLabel", }; @@ -33,7 +33,7 @@ export const LABEL_MAP = { export const PLACEHOLDER_MAP = { email: "createNotifications.emailSettings.emailPlaceholder", slack: "createNotifications.slackSettings.webhookPlaceholder", - pager_duty: "createNotifications.pagerDutySettings.integrationKeyPlaceholder", + pager_duty: "createNotifications.pagerdutySettings.integrationKeyPlaceholder", webhook: "createNotifications.webhookSettings.webhookPlaceholder", discord: "createNotifications.discordSettings.webhookPlaceholder", }; diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index de248f87b..c778efc9e 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -439,21 +439,35 @@ const notificationValidation = joi.object({ }), address: joi.when("type", { - is: "email", - then: joi - .string() - .email({ tlds: { allow: false } }) - .required() - .messages({ - "string.empty": "E-mail address cannot be empty", - "any.required": "E-mail address is required", - "string.email": "Please enter a valid e-mail address", - }), - otherwise: joi.string().uri().required().messages({ - "string.empty": "Webhook URL cannot be empty", - "any.required": "Webhook URL is required", - "string.uri": "Please enter a valid Webhook URL", - }), + switch: [ + { + is: "email", + then: joi + .string() + .email({ tlds: { allow: false } }) + .required() + .messages({ + "string.empty": "E-mail address cannot be empty", + "any.required": "E-mail address is required", + "string.email": "Please enter a valid e-mail address", + }), + }, + { + is: "pager_duty", + then: joi.string().required().messages({ + "string.empty": "PagerDuty routing key cannot be empty", + "any.required": "PagerDuty routing key is required", + }), + }, + { + is: joi.valid("webhook", "slack", "discord"), + then: joi.string().uri().required().messages({ + "string.empty": "Webhook URL cannot be empty", + "any.required": "Webhook URL is required", + "string.uri": "Please enter a valid Webhook URL", + }), + }, + ], }), }); From 54635425327c1fec92082ec09409e44457238266 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 18 Aug 2025 10:52:00 -0700 Subject: [PATCH 13/24] unify check models --- client/src/Components/Fallback/FallbackBackground.jsx | 3 +-- .../PageSpeed/Details/Components/Charts/PieChartLegend.jsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/client/src/Components/Fallback/FallbackBackground.jsx b/client/src/Components/Fallback/FallbackBackground.jsx index 00ecafb4e..fe46f4064 100644 --- a/client/src/Components/Fallback/FallbackBackground.jsx +++ b/client/src/Components/Fallback/FallbackBackground.jsx @@ -1,7 +1,6 @@ import { useTheme } from "@emotion/react"; import Box from "@mui/material/Box"; import Background from "../../assets/Images/background-grid.svg?react"; -import SkeletonDark from "../../assets/Images/create-placeholder-dark.svg?react"; import OutputAnimation from "../../assets/Animations/output.gif"; import DarkmodeOutput from "../../assets/Animations/darkmodeOutput.gif"; import { useSelector } from "react-redux"; @@ -13,7 +12,7 @@ const FallbackBackground = () => { { : theme.palette.tertiary.main; // Find the position where the number ends and the unit begins - const match = audit.displayValue.match(/(\d+\.?\d*)\s*([a-zA-Z]+)/); + const match = audit?.displayValue?.match(/(\d+\.?\d*)\s*([a-zA-Z]+)/); let value; let unit; if (match) { From 155adf549e1a3e5cecb355c9306b8b34d7b5a3cf Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 18 Aug 2025 10:55:12 -0700 Subject: [PATCH 14/24] add backend changes --- server/src/config/services.js | 16 +- server/src/db/models/Check.js | 264 +++++++++++------- server/src/db/models/HardwareCheck.js | 100 ------- server/src/db/models/Monitor.js | 18 +- server/src/db/models/MonitorStats.js | 4 - server/src/db/models/NetworkCheck.js | 46 --- server/src/db/models/PageSpeedCheck.js | 83 ------ server/src/db/mongo/modules/checkModule.js | 28 +- .../db/mongo/modules/hardwareCheckModule.js | 22 -- server/src/db/mongo/modules/monitorModule.js | 46 +-- .../db/mongo/modules/monitorModuleQueries.js | 68 +---- .../db/mongo/modules/networkCheckModule.js | 30 -- .../db/mongo/modules/pageSpeedCheckModule.js | 31 -- server/src/service/business/checkService.js | 3 +- .../SuperSimpleQueueHelper.js | 10 + .../service/infrastructure/bufferService.js | 66 ++--- .../service/infrastructure/statusService.js | 36 +-- 17 files changed, 228 insertions(+), 643 deletions(-) delete mode 100755 server/src/db/models/HardwareCheck.js delete mode 100644 server/src/db/models/NetworkCheck.js delete mode 100755 server/src/db/models/PageSpeedCheck.js delete mode 100755 server/src/db/mongo/modules/hardwareCheckModule.js delete mode 100644 server/src/db/mongo/modules/networkCheckModule.js delete mode 100755 server/src/db/mongo/modules/pageSpeedCheckModule.js diff --git a/server/src/config/services.js b/server/src/config/services.js index 515fe5cfe..08c2525f3 100644 --- a/server/src/config/services.js +++ b/server/src/config/services.js @@ -47,8 +47,6 @@ import { ParseBoolean } from "../utils/utils.js"; // Models import Check from "../db/models/Check.js"; -import HardwareCheck from "../db/models/HardwareCheck.js"; -import PageSpeedCheck from "../db/models/PageSpeedCheck.js"; import Monitor from "../db/models/Monitor.js"; import User from "../db/models/User.js"; import InviteToken from "../db/models/InviteToken.js"; @@ -56,7 +54,6 @@ import StatusPage from "../db/models/StatusPage.js"; import Team from "../db/models/Team.js"; import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; import MonitorStats from "../db/models/MonitorStats.js"; -import NetworkCheck from "../db/models/NetworkCheck.js"; import Notification from "../db/models/Notification.js"; import RecoveryToken from "../db/models/RecoveryToken.js"; import AppSettings from "../db/models/AppSettings.js"; @@ -65,12 +62,9 @@ import InviteModule from "../db/mongo/modules/inviteModule.js"; import CheckModule from "../db/mongo/modules/checkModule.js"; import StatusPageModule from "../db/mongo/modules/statusPageModule.js"; import UserModule from "../db/mongo/modules/userModule.js"; -import HardwareCheckModule from "../db/mongo/modules/hardwareCheckModule.js"; import MaintenanceWindowModule from "../db/mongo/modules/maintenanceWindowModule.js"; import MonitorModule from "../db/mongo/modules/monitorModule.js"; -import NetworkCheckModule from "../db/mongo/modules/networkCheckModule.js"; import NotificationModule from "../db/mongo/modules/notificationModule.js"; -import PageSpeedCheckModule from "../db/mongo/modules/pageSpeedCheckModule.js"; import RecoveryModule from "../db/mongo/modules/recoveryModule.js"; import SettingsModule from "../db/mongo/modules/settingsModule.js"; @@ -84,18 +78,15 @@ export const initializeServices = async ({ logger, envSettings, settingsService const stringService = new StringService(translationService); // Create DB - const checkModule = new CheckModule({ logger, Check, HardwareCheck, PageSpeedCheck, Monitor, User }); + const checkModule = new CheckModule({ logger, Check, Monitor, User }); const inviteModule = new InviteModule({ InviteToken, crypto, stringService }); const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService }); const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); - const hardwareCheckModule = new HardwareCheckModule({ HardwareCheck, Monitor, logger }); const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); const monitorModule = new MonitorModule({ Monitor, MonitorStats, Check, - PageSpeedCheck, - HardwareCheck, stringService, fs, path, @@ -104,9 +95,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService NormalizeData, NormalizeDataUptimeDetails, }); - const networkCheckModule = new NetworkCheckModule({ NetworkCheck }); const notificationModule = new NotificationModule({ Notification, Monitor }); - const pageSpeedCheckModule = new PageSpeedCheckModule({ PageSpeedCheck }); const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService }); const settingsModule = new SettingsModule({ AppSettings }); @@ -117,12 +106,9 @@ export const initializeServices = async ({ logger, envSettings, settingsService inviteModule, statusPageModule, userModule, - hardwareCheckModule, maintenanceWindowModule, monitorModule, - networkCheckModule, notificationModule, - pageSpeedCheckModule, recoveryModule, settingsModule, }); diff --git a/server/src/db/models/Check.js b/server/src/db/models/Check.js index 0037ffadc..42a718bd0 100755 --- a/server/src/db/models/Check.js +++ b/server/src/db/models/Check.js @@ -1,108 +1,176 @@ import mongoose from "mongoose"; -const BaseCheckSchema = mongoose.Schema({ - /** - * Reference to the associated Monitor document. - * - * @type {mongoose.Schema.Types.ObjectId} - */ - monitorId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Monitor", - immutable: true, - index: true, - }, - - teamId: { - type: mongoose.Schema.Types.ObjectId, - ref: "Team", - immutable: true, - index: true, - }, - /** - * Status of the check (true for up, false for down). - * - * @type {Boolean} - */ - status: { - type: Boolean, - index: true, - }, - /** - * Response time of the check in milliseconds. - * - * @type {Number} - */ - responseTime: { - type: Number, - }, - /** - * Timings from Got. - * - * @type {Object} - */ - timings: { - type: Object, // Accepts any object - default: {}, - }, - /** - * HTTP status code received during the check. - * - * @type {Number} - */ - statusCode: { - type: Number, - index: true, - }, - /** - * Message or description of the check result. - * - * @type {String} - */ - message: { - type: String, - }, - /** - * Expiry date of the check, auto-calculated to expire after 30 days. - * - * @type {Date} - */ - - expiry: { - type: Date, - default: Date.now, - expires: 60 * 60 * 24 * 30, // 30 days - }, - /** - * Acknowledgment of the check. - * - * @type {Boolean} - */ - ack: { - type: Boolean, - default: false, - }, - /** - * Resolution date of the check (when the check was resolved). - * - * @type {Date} - */ - ackAt: { - type: Date, - }, +const cpuSchema = mongoose.Schema({ + physical_core: { type: Number, default: 0 }, + logical_core: { type: Number, default: 0 }, + frequency: { type: Number, default: 0 }, + temperature: { type: [Number], default: [] }, + free_percent: { type: Number, default: 0 }, + usage_percent: { type: Number, default: 0 }, }); -/** - * Check Schema for MongoDB collection. - * - * Represents a check associated with a monitor, storing information - * about the status and response of a particular check event. - */ -const CheckSchema = mongoose.Schema({ ...BaseCheckSchema.obj }, { timestamps: true }); +const memorySchema = mongoose.Schema({ + total_bytes: { type: Number, default: 0 }, + available_bytes: { type: Number, default: 0 }, + used_bytes: { type: Number, default: 0 }, + usage_percent: { type: Number, default: 0 }, +}); + +const diskSchema = mongoose.Schema({ + read_speed_bytes: { type: Number, default: 0 }, + write_speed_bytes: { type: Number, default: 0 }, + total_bytes: { type: Number, default: 0 }, + free_bytes: { type: Number, default: 0 }, + usage_percent: { type: Number, default: 0 }, +}); + +const hostSchema = mongoose.Schema({ + os: { type: String, default: "" }, + platform: { type: String, default: "" }, + kernel_version: { type: String, default: "" }, +}); + +const errorSchema = mongoose.Schema({ + metric: { type: [String], default: [] }, + err: { type: String, default: "" }, +}); + +const captureSchema = mongoose.Schema({ + version: { type: String, default: "" }, + mode: { type: String, default: "" }, +}); + +const networkInterfaceSchema = mongoose.Schema({ + name: { type: String }, + bytes_sent: { type: Number, default: 0 }, + bytes_recv: { type: Number, default: 0 }, + packets_sent: { type: Number, default: 0 }, + packets_recv: { type: Number, default: 0 }, + err_in: { type: Number, default: 0 }, + err_out: { type: Number, default: 0 }, + drop_in: { type: Number, default: 0 }, + drop_out: { type: Number, default: 0 }, + fifo_in: { type: Number, default: 0 }, + fifo_out: { type: Number, default: 0 }, +}); + +const CheckSchema = new mongoose.Schema( + { + // Common fields + monitorId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Monitor", + immutable: true, + index: true, + }, + + teamId: { + type: mongoose.Schema.Types.ObjectId, + ref: "Team", + immutable: true, + index: true, + }, + type: { + type: String, + enum: ["http", "hardware", "pagespeed", "distributed"], + required: true, + index: true, + }, + + status: { + type: Boolean, + index: true, + }, + + responseTime: { + type: Number, + }, + + timings: { + type: Object, + default: {}, + }, + + statusCode: { + type: Number, + index: true, + }, + + message: { + type: String, + }, + + expiry: { + type: Date, + default: Date.now, + expires: 60 * 60 * 24 * 30, // 30 days + }, + + ack: { + type: Boolean, + default: false, + }, + + ackAt: { + type: Date, + }, + + // Hardware fields + cpu: { + type: cpuSchema, + default: () => ({}), + }, + memory: { + type: memorySchema, + default: () => ({}), + }, + disk: { + type: [diskSchema], + default: () => [], + }, + host: { + type: hostSchema, + default: () => ({}), + }, + + errors: { + type: [errorSchema], + default: () => [], + }, + + capture: { + type: captureSchema, + default: () => ({}), + }, + + net: { + type: [networkInterfaceSchema], + default: () => [], + }, + + // PageSpeed fields + accessibility: { + type: Number, + }, + bestPractices: { + type: Number, + }, + seo: { + type: Number, + }, + performance: { + type: Number, + }, + audits: { + type: Object, + }, + }, + { timestamps: true } +); + CheckSchema.index({ updatedAt: 1 }); CheckSchema.index({ monitorId: 1, updatedAt: 1 }); CheckSchema.index({ monitorId: 1, updatedAt: -1 }); CheckSchema.index({ teamId: 1, updatedAt: -1 }); export default mongoose.model("Check", CheckSchema); -export { BaseCheckSchema }; diff --git a/server/src/db/models/HardwareCheck.js b/server/src/db/models/HardwareCheck.js deleted file mode 100755 index 5b7d56213..000000000 --- a/server/src/db/models/HardwareCheck.js +++ /dev/null @@ -1,100 +0,0 @@ -import mongoose from "mongoose"; -import { BaseCheckSchema } from "./Check.js"; -const cpuSchema = mongoose.Schema({ - physical_core: { type: Number, default: 0 }, - logical_core: { type: Number, default: 0 }, - frequency: { type: Number, default: 0 }, - temperature: { type: [Number], default: [] }, - free_percent: { type: Number, default: 0 }, - usage_percent: { type: Number, default: 0 }, -}); - -const memorySchema = mongoose.Schema({ - total_bytes: { type: Number, default: 0 }, - available_bytes: { type: Number, default: 0 }, - used_bytes: { type: Number, default: 0 }, - usage_percent: { type: Number, default: 0 }, -}); - -const diskSchema = mongoose.Schema({ - read_speed_bytes: { type: Number, default: 0 }, - write_speed_bytes: { type: Number, default: 0 }, - total_bytes: { type: Number, default: 0 }, - free_bytes: { type: Number, default: 0 }, - usage_percent: { type: Number, default: 0 }, -}); - -const hostSchema = mongoose.Schema({ - os: { type: String, default: "" }, - platform: { type: String, default: "" }, - kernel_version: { type: String, default: "" }, -}); - -const errorSchema = mongoose.Schema({ - metric: { type: [String], default: [] }, - err: { type: String, default: "" }, -}); - -const captureSchema = mongoose.Schema({ - version: { type: String, default: "" }, - mode: { type: String, default: "" }, -}); - -const networkInterfaceSchema = mongoose.Schema({ - name: { type: String }, - bytes_sent: { type: Number, default: 0 }, - bytes_recv: { type: Number, default: 0 }, - packets_sent: { type: Number, default: 0 }, - packets_recv: { type: Number, default: 0 }, - err_in: { type: Number, default: 0 }, - err_out: { type: Number, default: 0 }, - drop_in: { type: Number, default: 0 }, - drop_out: { type: Number, default: 0 }, - fifo_in: { type: Number, default: 0 }, - fifo_out: { type: Number, default: 0 }, -}); - -const HardwareCheckSchema = mongoose.Schema( - { - ...BaseCheckSchema.obj, - cpu: { - type: cpuSchema, - default: () => ({}), - }, - memory: { - type: memorySchema, - default: () => ({}), - }, - disk: { - type: [diskSchema], - default: () => [], - }, - host: { - type: hostSchema, - default: () => ({}), - }, - - errors: { - type: [errorSchema], - default: () => [], - }, - - capture: { - type: captureSchema, - default: () => ({}), - }, - - net: { - type: [networkInterfaceSchema], - default: () => [], - required: false, - }, - }, - { timestamps: true } -); - -HardwareCheckSchema.index({ createdAt: 1 }); -HardwareCheckSchema.index({ monitorId: 1, createdAt: 1 }); -HardwareCheckSchema.index({ monitorId: 1, createdAt: -1 }); - -export default mongoose.model("HardwareCheck", HardwareCheckSchema); diff --git a/server/src/db/models/Monitor.js b/server/src/db/models/Monitor.js index 1ee467999..8d4ff9468 100755 --- a/server/src/db/models/Monitor.js +++ b/server/src/db/models/Monitor.js @@ -1,6 +1,4 @@ import mongoose from "mongoose"; -import HardwareCheck from "./HardwareCheck.js"; -import PageSpeedCheck from "./PageSpeedCheck.js"; import Check from "./Check.js"; import MonitorStats from "./MonitorStats.js"; import StatusPage from "./StatusPage.js"; @@ -133,13 +131,7 @@ MonitorSchema.pre("findOneAndDelete", async function (next) { throw new Error("Monitor not found"); } - if (doc?.type === "pagespeed") { - await PageSpeedCheck.deleteMany({ monitorId: doc._id }); - } else if (doc?.type === "hardware") { - await HardwareCheck.deleteMany({ monitorId: doc._id }); - } else { - await Check.deleteMany({ monitorId: doc._id }); - } + await Check.deleteMany({ monitorId: doc._id }); // Deal with status pages await StatusPage.updateMany({ monitors: doc?._id }, { $pull: { monitors: doc?._id } }); @@ -156,13 +148,7 @@ MonitorSchema.pre("deleteMany", async function (next) { const monitors = await this.model.find(filter).select(["_id", "type"]).lean(); for (const monitor of monitors) { - if (monitor.type === "pagespeed") { - await PageSpeedCheck.deleteMany({ monitorId: monitor._id }); - } else if (monitor.type === "hardware") { - await HardwareCheck.deleteMany({ monitorId: monitor._id }); - } else { - await Check.deleteMany({ monitorId: monitor._id }); - } + await Check.deleteMany({ monitorId: monitor._id }); await StatusPage.updateMany({ monitors: monitor._id }, { $pull: { monitors: monitor._id } }); await MonitorStats.deleteMany({ monitorId: monitor._id.toString() }); } diff --git a/server/src/db/models/MonitorStats.js b/server/src/db/models/MonitorStats.js index c28811b33..c081c8a4f 100755 --- a/server/src/db/models/MonitorStats.js +++ b/server/src/db/models/MonitorStats.js @@ -40,10 +40,6 @@ const MonitorStatsSchema = new mongoose.Schema( type: Number, default: 0, }, - uptBurnt: { - type: mongoose.Schema.Types.Decimal128, - required: false, - }, }, { timestamps: true } ); diff --git a/server/src/db/models/NetworkCheck.js b/server/src/db/models/NetworkCheck.js deleted file mode 100644 index eca69eef9..000000000 --- a/server/src/db/models/NetworkCheck.js +++ /dev/null @@ -1,46 +0,0 @@ -import mongoose from "mongoose"; -import { BaseCheckSchema } from "./Check.js"; - -const networkInterfaceSchema = mongoose.Schema({ - name: { type: String, required: true }, - bytes_sent: { type: Number, default: 0 }, - bytes_recv: { type: Number, default: 0 }, - packets_sent: { type: Number, default: 0 }, - packets_recv: { type: Number, default: 0 }, - err_in: { type: Number, default: 0 }, - err_out: { type: Number, default: 0 }, - drop_in: { type: Number, default: 0 }, - drop_out: { type: Number, default: 0 }, - fifo_in: { type: Number, default: 0 }, - fifo_out: { type: Number, default: 0 }, -}); - -const captureSchema = mongoose.Schema({ - version: { type: String, default: "" }, - mode: { type: String, default: "" }, -}); - -const NetworkCheckSchema = mongoose.Schema( - { - ...BaseCheckSchema.obj, - data: { - type: [networkInterfaceSchema], - default: () => [], - }, - capture: { - type: captureSchema, - default: () => ({}), - }, - errors: { - type: mongoose.Schema.Types.Mixed, - default: null, - }, - }, - { timestamps: true } -); - -NetworkCheckSchema.index({ createdAt: 1 }); -NetworkCheckSchema.index({ monitorId: 1, createdAt: 1 }); -NetworkCheckSchema.index({ monitorId: 1, createdAt: -1 }); - -export default mongoose.model("NetworkCheck", NetworkCheckSchema); diff --git a/server/src/db/models/PageSpeedCheck.js b/server/src/db/models/PageSpeedCheck.js deleted file mode 100755 index 67c7c375e..000000000 --- a/server/src/db/models/PageSpeedCheck.js +++ /dev/null @@ -1,83 +0,0 @@ -import mongoose from "mongoose"; -import { BaseCheckSchema } from "./Check.js"; -const AuditSchema = mongoose.Schema({ - id: { type: String, required: true }, - title: { type: String, required: true }, - description: { type: String, required: true }, - score: { type: Number, required: true }, - scoreDisplayMode: { type: String, required: true }, - displayValue: { type: String, required: true }, - numericValue: { type: Number, required: true }, - numericUnit: { type: String, required: true }, -}); - -const AuditsSchema = mongoose.Schema({ - cls: { - type: AuditSchema, - required: true, - }, - si: { - type: AuditSchema, - required: true, - }, - fcp: { - type: AuditSchema, - required: true, - }, - lcp: { - type: AuditSchema, - required: true, - }, - tbt: { - type: AuditSchema, - required: true, - }, -}); - -/** - * Mongoose schema for storing metrics from Google Lighthouse. - * @typedef {Object} PageSpeedCheck - * @property {mongoose.Schema.Types.ObjectId} monitorId - Reference to the Monitor model. - * @property {number} accessibility - Accessibility score. - * @property {number} bestPractices - Best practices score. - * @property {number} seo - SEO score. - * @property {number} performance - Performance score. - */ - -const PageSpeedCheck = mongoose.Schema( - { - ...BaseCheckSchema.obj, - accessibility: { - type: Number, - required: true, - }, - bestPractices: { - type: Number, - required: true, - }, - seo: { - type: Number, - required: true, - }, - performance: { - type: Number, - required: true, - }, - audits: { - type: AuditsSchema, - required: true, - }, - }, - { timestamps: true } -); - -/** - * Mongoose model for storing metrics from Google Lighthouse. - * @typedef {mongoose.Model} LighthouseMetricsModel - */ - -PageSpeedCheck.index({ createdAt: 1 }); -PageSpeedCheck.index({ monitorId: 1, createdAt: 1 }); -PageSpeedCheck.index({ monitorId: 1, createdAt: -1 }); - -export default mongoose.model("PageSpeedCheck", PageSpeedCheck); diff --git a/server/src/db/mongo/modules/checkModule.js b/server/src/db/mongo/modules/checkModule.js index a606a593b..dfca02bed 100755 --- a/server/src/db/mongo/modules/checkModule.js +++ b/server/src/db/mongo/modules/checkModule.js @@ -31,7 +31,7 @@ class CheckModule { } }; - getChecksByMonitor = async ({ monitorId, type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status }) => { + getChecksByMonitor = async ({ monitorId, sortOrder, dateRange, filter, ack, page, rowsPerPage, status }) => { try { status = status === "true" ? true : status === "false" ? false : undefined; page = parseInt(page); @@ -79,19 +79,7 @@ class CheckModule { skip = page * rowsPerPage; } - const checkModels = { - http: this.Check, - ping: this.Check, - docker: this.Check, - port: this.Check, - pagespeed: this.PageSpeedCheck, - hardware: this.HardwareCheck, - game: this.Check, - }; - - const Model = checkModels[type]; - - const checks = await Model.aggregate([ + const checks = await this.Check.aggregate([ { $match: matchStage }, { $sort: { createdAt: sortOrder } }, { @@ -166,18 +154,6 @@ class CheckModule { const aggregatePipeline = [ { $match: matchStage }, - { - $unionWith: { - coll: "hardwarechecks", - pipeline: [{ $match: matchStage }], - }, - }, - { - $unionWith: { - coll: "pagespeedchecks", - pipeline: [{ $match: matchStage }], - }, - }, { $sort: { createdAt: sortOrder } }, { diff --git a/server/src/db/mongo/modules/hardwareCheckModule.js b/server/src/db/mongo/modules/hardwareCheckModule.js deleted file mode 100755 index 448e72fed..000000000 --- a/server/src/db/mongo/modules/hardwareCheckModule.js +++ /dev/null @@ -1,22 +0,0 @@ -const SERVICE_NAME = "hardwareCheckModule"; - -class HardwareCheckModule { - constructor({ HardwareCheck, Monitor, logger }) { - this.HardwareCheck = HardwareCheck; - this.Monitor = Monitor; - this.logger = logger; - } - - createHardwareChecks = async (hardwareChecks) => { - try { - await this.HardwareCheck.insertMany(hardwareChecks, { ordered: false }); - return true; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createHardwareChecks"; - throw error; - } - }; -} - -export default HardwareCheckModule; diff --git a/server/src/db/mongo/modules/monitorModule.js b/server/src/db/mongo/modules/monitorModule.js index 0e301cdcf..8beb9316c 100755 --- a/server/src/db/mongo/modules/monitorModule.js +++ b/server/src/db/mongo/modules/monitorModule.js @@ -11,25 +11,10 @@ import { const SERVICE_NAME = "monitorModule"; class MonitorModule { - constructor({ - Monitor, - MonitorStats, - Check, - PageSpeedCheck, - HardwareCheck, - stringService, - fs, - path, - fileURLToPath, - ObjectId, - NormalizeData, - NormalizeDataUptimeDetails, - }) { + constructor({ Monitor, MonitorStats, Check, stringService, fs, path, fileURLToPath, ObjectId, NormalizeData, NormalizeDataUptimeDetails }) { this.Monitor = Monitor; this.MonitorStats = MonitorStats; this.Check = Check; - this.PageSpeedCheck = PageSpeedCheck; - this.HardwareCheck = HardwareCheck; this.stringService = stringService; this.fs = fs; this.path = path; @@ -38,15 +23,6 @@ class MonitorModule { this.NormalizeData = NormalizeData; this.NormalizeDataUptimeDetails = NormalizeDataUptimeDetails; - this.CHECK_MODEL_LOOKUP = { - http: Check, - ping: Check, - docker: Check, - port: Check, - pagespeed: PageSpeedCheck, - hardware: HardwareCheck, - }; - const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -146,19 +122,18 @@ class MonitorModule { }; //Helper - getMonitorChecks = async (monitorId, model, dateRange, sortOrder) => { + getMonitorChecks = async (monitorId, dateRange, sortOrder) => { const indexSpec = { monitorId: 1, - createdAt: sortOrder, // This will be 1 or -1 + updatedAt: sortOrder, // This will be 1 or -1 }; const [checksAll, checksForDateRange] = await Promise.all([ - model.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(), - model - .find({ - monitorId, - createdAt: { $gte: dateRange.start, $lte: dateRange.end }, - }) + this.Check.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(), + this.Check.find({ + monitorId, + createdAt: { $gte: dateRange.start, $lte: dateRange.end }, + }) .hint(indexSpec) .lean(), ]); @@ -295,9 +270,8 @@ class MonitorModule { const sort = sortOrder === "asc" ? 1 : -1; // Get Checks for monitor in date range requested - const model = this.CHECK_MODEL_LOOKUP[monitor.type]; const dates = this.getDateRange(dateRange); - const { checksAll, checksForDateRange } = await this.getMonitorChecks(monitorId, model, dates, sort); + const { checksAll, checksForDateRange } = await this.getMonitorChecks(monitorId, dates, sort); // Build monitor stats const monitorStats = { @@ -339,7 +313,7 @@ class MonitorModule { }; const dateString = formatLookup[dateRange]; - const hardwareStats = await this.HardwareCheck.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString)); + const hardwareStats = await this.Check.aggregate(buildHardwareDetailsPipeline(monitor, dates, dateString)); const stats = hardwareStats[0]; diff --git a/server/src/db/mongo/modules/monitorModuleQueries.js b/server/src/db/mongo/modules/monitorModuleQueries.js index f6d20d855..0a8f807e8 100755 --- a/server/src/db/mongo/modules/monitorModuleQueries.js +++ b/server/src/db/mongo/modules/monitorModuleQueries.js @@ -5,12 +5,12 @@ const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => { { $match: { monitorId: new ObjectId(monitorId), - createdAt: { $gte: dates.start, $lte: dates.end }, + updatedAt: { $gte: dates.start, $lte: dates.end }, }, }, { $sort: { - createdAt: 1, + updatedAt: 1, }, }, { @@ -173,6 +173,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { { $match: { monitorId: monitor._id, + type: "hardware", createdAt: { $gte: dates.start, $lte: dates.end }, }, }, @@ -225,7 +226,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => { }, { $lookup: { - from: "hardwarechecks", + from: "checks", let: { diskCount: "$diskCount", netCount: "$netCount", @@ -987,11 +988,6 @@ const buildMonitorsWithChecksByTeamIdPipeline = ({ matchStage, filter, page, row // Add checks if (limit) { let checksCollection = "checks"; - if (type === "pagespeed") { - checksCollection = "pagespeedchecks"; - } else if (type === "hardware") { - checksCollection = "hardwarechecks"; - } monitorsPipeline.push({ $lookup: { from: checksCollection, @@ -1057,11 +1053,7 @@ const buildFilteredMonitorsByTeamIdPipeline = ({ matchStage, filter, page, rowsP // Add checks if (limit) { let checksCollection = "checks"; - if (type === "pagespeed") { - checksCollection = "pagespeedchecks"; - } else if (type === "hardware") { - checksCollection = "hardwarechecks"; - } + pipeline.push({ $lookup: { from: checksCollection, @@ -1175,46 +1167,6 @@ const buildGetMonitorsByTeamIdPipeline = (req) => { }, ] : []), - ...(limit - ? [ - { - $lookup: { - from: "pagespeedchecks", - let: { monitorId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - }, - }, - { $sort: { createdAt: -1 } }, - ...(limit ? [{ $limit: limit }] : []), - ], - as: "pagespeedchecks", - }, - }, - ] - : []), - ...(limit - ? [ - { - $lookup: { - from: "hardwarechecks", - let: { monitorId: "$_id" }, - pipeline: [ - { - $match: { - $expr: { $eq: ["$monitorId", "$$monitorId"] }, - }, - }, - { $sort: { createdAt: -1 } }, - ...(limit ? [{ $limit: limit }] : []), - ], - as: "hardwarechecks", - }, - }, - ] - : []), { $addFields: { @@ -1225,14 +1177,6 @@ const buildGetMonitorsByTeamIdPipeline = (req) => { case: { $in: ["$type", ["http", "ping", "docker", "port", "game"]] }, then: "$standardchecks", }, - { - case: { $eq: ["$type", "pagespeed"] }, - then: "$pagespeedchecks", - }, - { - case: { $eq: ["$type", "hardware"] }, - then: "$hardwarechecks", - }, ], default: [], }, @@ -1242,8 +1186,6 @@ const buildGetMonitorsByTeamIdPipeline = (req) => { { $project: { standardchecks: 0, - pagespeedchecks: 0, - hardwarechecks: 0, }, }, ], diff --git a/server/src/db/mongo/modules/networkCheckModule.js b/server/src/db/mongo/modules/networkCheckModule.js deleted file mode 100644 index 917e68440..000000000 --- a/server/src/db/mongo/modules/networkCheckModule.js +++ /dev/null @@ -1,30 +0,0 @@ -const SERVICE_NAME = "networkCheckModule"; - -class NetworkCheckModule { - constructor({ NetworkCheck }) { - this.NetworkCheck = NetworkCheck; - } - createNetworkCheck = async (networkCheckData) => { - try { - const networkCheck = await new this.NetworkCheck(networkCheckData); - await networkCheck.save(); - return networkCheck; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createNetworkCheck"; - throw error; - } - }; - getNetworkChecksByMonitorId = async (monitorId, limit = 100) => { - try { - const networkChecks = await this.NetworkCheck.find({ monitorId }).sort({ createdAt: -1 }).limit(limit); - return networkChecks; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "getNetworkChecksByMonitorId"; - throw error; - } - }; -} - -export default NetworkCheckModule; diff --git a/server/src/db/mongo/modules/pageSpeedCheckModule.js b/server/src/db/mongo/modules/pageSpeedCheckModule.js deleted file mode 100755 index b2a3eb71d..000000000 --- a/server/src/db/mongo/modules/pageSpeedCheckModule.js +++ /dev/null @@ -1,31 +0,0 @@ -// import PageSpeedCheck from "../../models/PageSpeedCheck.js"; -const SERVICE_NAME = "pageSpeedCheckModule"; - -class PageSpeedCheckModule { - constructor({ PageSpeedCheck }) { - this.PageSpeedCheck = PageSpeedCheck; - } - - createPageSpeedChecks = async (pageSpeedChecks) => { - try { - await this.PageSpeedCheck.insertMany(pageSpeedChecks, { ordered: false }); - return true; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "createPageSpeedCheck"; - throw error; - } - }; - - deletePageSpeedChecksByMonitorId = async (monitorId) => { - try { - const result = await this.PageSpeedCheck.deleteMany({ monitorId }); - return result.deletedCount; - } catch (error) { - error.service = SERVICE_NAME; - error.method = "deletePageSpeedChecksByMonitorId"; - throw error; - } - }; -} -export default PageSpeedCheckModule; diff --git a/server/src/service/business/checkService.js b/server/src/service/business/checkService.js index 7966fdbb4..9beb43326 100644 --- a/server/src/service/business/checkService.js +++ b/server/src/service/business/checkService.js @@ -33,10 +33,9 @@ class CheckService { throw this.errorService.createAuthorizationError(); } - let { type, sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query; + let { sortOrder, dateRange, filter, ack, page, rowsPerPage, status } = query; const result = await this.db.checkModule.getChecksByMonitor({ monitorId, - type, sortOrder, dateRange, filter, diff --git a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js index 7659b1c71..ac8da9169 100644 --- a/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js +++ b/server/src/service/infrastructure/SuperSimpleQueue/SuperSimpleQueueHelper.js @@ -2,6 +2,16 @@ const SERVICE_NAME = "JobQueueHelper"; class SuperSimpleQueueHelper { static SERVICE_NAME = SERVICE_NAME; + + /** + * @param {{ + * db: import("../database").Database, + * logger: import("../logger").Logger, + * networkService: import("../networkService").NetworkService, + * statusService: import("../statusService").StatusService, + * notificationService: import("../notificationService").NotificationService + * }} + */ constructor({ db, logger, networkService, statusService, notificationService }) { this.db = db; this.logger = logger; diff --git a/server/src/service/infrastructure/bufferService.js b/server/src/service/infrastructure/bufferService.js index dc86597b2..f55e25037 100755 --- a/server/src/service/infrastructure/bufferService.js +++ b/server/src/service/infrastructure/bufferService.js @@ -1,13 +1,4 @@ const SERVICE_NAME = "BufferService"; -const TYPE_MAP = { - http: "checks", - ping: "checks", - port: "checks", - docker: "checks", - pagespeed: "pagespeedChecks", - hardware: "hardwareChecks", - game: "checks", -}; class BufferService { static SERVICE_NAME = SERVICE_NAME; @@ -16,17 +7,7 @@ class BufferService { this.db = db; this.logger = logger; this.SERVICE_NAME = SERVICE_NAME; - this.buffers = { - checks: [], - pagespeedChecks: [], - hardwareChecks: [], - }; - this.OPERATION_MAP = { - checks: this.db.checkModule.createChecks, - pagespeedChecks: this.db.pageSpeedCheckModule.createPageSpeedChecks, - hardwareChecks: this.db.hardwareCheckModule.createHardwareChecks, - }; - + this.buffer = []; this.scheduleNextFlush(); this.logger.info({ message: `Buffer service initialized, flushing every ${this.BUFFER_TIMEOUT / 1000}s`, @@ -39,9 +20,9 @@ class BufferService { return BufferService.SERVICE_NAME; } - addToBuffer({ check, type }) { + addToBuffer({ check }) { try { - this.buffers[TYPE_MAP[type]].push(check); + this.buffer.push(check); } catch (error) { this.logger.error({ message: error.message, @@ -55,7 +36,7 @@ class BufferService { scheduleNextFlush() { this.bufferTimer = setTimeout(async () => { try { - await this.flushBuffers(); + await this.flushBuffer(); } catch (error) { this.logger.error({ message: `Error in flush cycle: ${error.message}`, @@ -69,35 +50,24 @@ class BufferService { } }, this.BUFFER_TIMEOUT); } - async flushBuffers() { - let items = 0; - for (const [bufferName, buffer] of Object.entries(this.buffers)) { - items += buffer.length; - const operation = this.OPERATION_MAP[bufferName]; - if (!operation) { - this.logger.error({ - message: `No operation found for ${bufferName}`, - service: this.SERVICE_NAME, - method: "flushBuffers", - }); - continue; - } - try { - await operation(buffer); - } catch (error) { - this.logger.error({ - message: error.message, - service: this.SERVICE_NAME, - method: "flushBuffers", - stack: error.stack, - }); - } - this.buffers[bufferName] = []; + async flushBuffer() { + let items = this.buffer.length; + + try { + await this.db.checkModule.createChecks(this.buffer); + } catch (error) { + this.logger.error({ + message: error.message, + service: this.SERVICE_NAME, + method: "flushBuffer", + stack: error.stack, + }); } + this.buffer = []; this.logger.debug({ message: `Flushed ${items} items`, service: this.SERVICE_NAME, - method: "flushBuffers", + method: "flushBuffer", }); items = 0; } diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index 4a98996fb..af2c82fa9 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -1,17 +1,14 @@ import MonitorStats from "../../db/models/MonitorStats.js"; -import { safelyParseFloat } from "../../utils/dataUtils.js"; const SERVICE_NAME = "StatusService"; class StatusService { static SERVICE_NAME = SERVICE_NAME; /** - * Creates an instance of StatusService. - * - * @param {Object} db - The database instance. - * @param {Object} logger - The logger instance. - */ - constructor({ db, logger, buffer }) { + * @param {{ + * buffer: import("./bufferService.js").BufferService + * }} + */ constructor({ db, logger, buffer }) { this.db = db; this.logger = logger; this.buffer = buffer; @@ -24,7 +21,7 @@ class StatusService { async updateRunningStats({ monitor, networkResponse }) { try { const monitorId = monitor._id; - const { responseTime, status, upt_burnt } = networkResponse; + const { responseTime, status } = networkResponse; // Get stats let stats = await MonitorStats.findOne({ monitorId }); if (!stats) { @@ -36,8 +33,6 @@ class StatusService { totalDownChecks: 0, uptimePercentage: 0, lastCheck: null, - timeSInceLastCheck: 0, - uptBurnt: 0, }); } @@ -82,12 +77,6 @@ class StatusService { // latest check stats.lastCheckTimestamp = new Date().getTime(); - // UPT burned - if (typeof upt_burnt !== "undefined" && upt_burnt !== null) { - const currentUptBurnt = safelyParseFloat(stats.uptBurnt); - const newUptBurnt = safelyParseFloat(upt_burnt); - stats.uptBurnt = currentUptBurnt + newUptBurnt; - } await stats.save(); return true; } catch (error) { @@ -119,7 +108,8 @@ class StatusService { * @returns {boolean} returnObject.prevStatus - The previous status of the monitor */ updateStatus = async (networkResponse) => { - this.insertCheck(networkResponse); + const check = this.buildCheck(networkResponse); + await this.insertCheck(check); try { const { monitorId, status, code } = networkResponse; const monitor = await this.db.monitorModule.getMonitorById(monitorId); @@ -198,6 +188,7 @@ class StatusService { const check = { monitorId, teamId, + type, status, statusCode: code, responseTime, @@ -264,25 +255,24 @@ class StatusService { * @param {Object} networkResponse.payload - The payload of the response. * @returns {Promise} A promise that resolves when the check is inserted. */ - insertCheck = async (networkResponse) => { + insertCheck = async (check) => { try { - const check = this.buildCheck(networkResponse); if (typeof check === "undefined") { this.logger.warn({ message: "Failed to build check", service: this.SERVICE_NAME, method: "insertCheck", - details: networkResponse, }); - return; + return false; } - this.buffer.addToBuffer({ check, type: networkResponse.type }); + this.buffer.addToBuffer({ check }); + return true; } catch (error) { this.logger.error({ message: error.message, service: error.service || this.SERVICE_NAME, method: error.method || "insertCheck", - details: error.details || `Error inserting check for monitor: ${networkResponse?.monitorId}`, + details: error.details || `Error inserting check for monitor: ${check?.monitorId}`, stack: error.stack, }); } From 1788cda5c2816011db318860a222f833bd9a1e84 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 18 Aug 2025 11:27:41 -0700 Subject: [PATCH 15/24] let -> const --- server/src/db/mongo/modules/monitorModuleQueries.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/src/db/mongo/modules/monitorModuleQueries.js b/server/src/db/mongo/modules/monitorModuleQueries.js index 0a8f807e8..32aed1529 100755 --- a/server/src/db/mongo/modules/monitorModuleQueries.js +++ b/server/src/db/mongo/modules/monitorModuleQueries.js @@ -987,7 +987,7 @@ const buildMonitorsWithChecksByTeamIdPipeline = ({ matchStage, filter, page, row // Add checks if (limit) { - let checksCollection = "checks"; + const checksCollection = "checks"; monitorsPipeline.push({ $lookup: { from: checksCollection, @@ -1052,7 +1052,7 @@ const buildFilteredMonitorsByTeamIdPipeline = ({ matchStage, filter, page, rowsP // Add checks if (limit) { - let checksCollection = "checks"; + const checksCollection = "checks"; pipeline.push({ $lookup: { From 613fbf8c395a357c044471437e1ddbcf55a6f0ea Mon Sep 17 00:00:00 2001 From: Br0wnHammer Date: Tue, 19 Aug 2025 01:11:56 +0530 Subject: [PATCH 16/24] Fix: Minor Changes --- client/src/Components/StarPrompt/index.jsx | 2 +- client/src/Pages/Uptime/Create/index.jsx | 16 +--------------- client/src/Validation/validation.js | 6 ++++++ 3 files changed, 8 insertions(+), 16 deletions(-) diff --git a/client/src/Components/StarPrompt/index.jsx b/client/src/Components/StarPrompt/index.jsx index 64fce1d3f..f5f130387 100644 --- a/client/src/Components/StarPrompt/index.jsx +++ b/client/src/Components/StarPrompt/index.jsx @@ -74,7 +74,7 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" }) { - try { - return new URL(url); - } catch (error) { - return null; - } -}; - /** * Create page renders monitor creation or configuration views. * @component @@ -299,7 +285,7 @@ const UptimeCreate = ({ isClone = false }) => { const isBusy = isLoading || isCreating || isDeleting || isUpdating || isPausing; const displayInterval = monitor?.interval / MS_PER_MINUTE || 1; - const parsedUrl = parseUrl(monitor?.url); + const parsedUrl = monitor?.url; const protocol = parsedUrl?.protocol?.replace(":", "") || ""; useEffect(() => { diff --git a/client/src/Validation/validation.js b/client/src/Validation/validation.js index c778efc9e..902356ae2 100644 --- a/client/src/Validation/validation.js +++ b/client/src/Validation/validation.js @@ -150,6 +150,11 @@ const monitorValidation = joi.object({ // can be replaced by a shortest alternative // (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+ "(?:" + + // Single hostname without dots (like localhost) + "[a-z0-9\\u00a1-\\uffff][a-z0-9\\u00a1-\\uffff_-]{0,62}" + + "|" + + // Domain with dots + "(?:" + "(?:" + "[a-z0-9\\u00a1-\\uffff]" + "[a-z0-9\\u00a1-\\uffff_-]{0,62}" + @@ -159,6 +164,7 @@ const monitorValidation = joi.object({ // TLD identifier name, may end with dot "(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" + ")" + + ")" + // port number (optional) "(?::\\d{2,5})?" + // resource path (optional) From 74e80f086a3e34f08a1a36e5fabad7012ac1840b Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 18 Aug 2025 13:40:22 -0700 Subject: [PATCH 17/24] udpate monitor model --- server/src/db/models/Monitor.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/server/src/db/models/Monitor.js b/server/src/db/models/Monitor.js index 8d4ff9468..0ad0e0a78 100755 --- a/server/src/db/models/Monitor.js +++ b/server/src/db/models/Monitor.js @@ -28,6 +28,18 @@ const MonitorSchema = mongoose.Schema( type: Boolean, default: undefined, }, + statusWindow: { + type: [Boolean], + default: [], + }, + statusWindowSize: { + type: Number, + default: 5, + }, + statusWindowThreshold: { + type: Number, + default: 0.6, + }, type: { type: String, required: true, From d5778ec1737bb740e15eb6111e9593b5d6124ed1 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 18 Aug 2025 13:40:56 -0700 Subject: [PATCH 18/24] update stauts service to use n of m status changes --- .../service/infrastructure/bufferService.js | 2 +- .../service/infrastructure/statusService.js | 59 ++++++++++++++----- 2 files changed, 46 insertions(+), 15 deletions(-) diff --git a/server/src/service/infrastructure/bufferService.js b/server/src/service/infrastructure/bufferService.js index f55e25037..ed48cc654 100755 --- a/server/src/service/infrastructure/bufferService.js +++ b/server/src/service/infrastructure/bufferService.js @@ -3,7 +3,7 @@ const SERVICE_NAME = "BufferService"; class BufferService { static SERVICE_NAME = SERVICE_NAME; constructor({ db, logger, envSettings }) { - this.BUFFER_TIMEOUT = envSettings.nodeEnv === "development" ? 5000 : 1000 * 60 * 1; // 1 minute + this.BUFFER_TIMEOUT = envSettings.nodeEnv === "development" ? 1000 : 1000 * 60 * 1; // 1 minute this.db = db; this.logger = logger; this.SERVICE_NAME = SERVICE_NAME; diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index af2c82fa9..0c3434df0 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -117,31 +117,62 @@ class StatusService { // Update running stats this.updateRunningStats({ monitor, networkResponse }); - // No change in monitor status, return early - if (monitor.status === status) + // Update status sliding window + monitor.statusWindow.push(status); + if (monitor.statusWindow.length > monitor.statusWindowSize) { + monitor.statusWindow.shift(); + } + + if (!monitor.status) { + monitor.status = status; + } + + let newStatus = monitor.status; + let statusChanged = false; + const prevStatus = monitor.status; + + // Return early if not enough data points + if (monitor.statusWindow.length < monitor.statusWindowSize) { + await monitor.save(); return { monitor, statusChanged: false, - prevStatus: monitor.status, + prevStatus, code, - timestamp: new Date().getTime(), + timestamp: Date.now(), }; + } - // Monitor status changed, save prev status and update monitor - this.logger.info({ - service: this.SERVICE_NAME, - message: `${monitor.name} went from ${this.getStatusString(monitor.status)} to ${this.getStatusString(status)}`, - prevStatus: monitor.status, - newStatus: status, - }); + // Check if threshold has been met + const failures = monitor.statusWindow.filter((s) => s === false).length; + const failureRate = failures / monitor.statusWindow.length; - const prevStatus = monitor.status; - monitor.status = status; + // If threshold has been met and the monitor is not already down, mark down: + if (failureRate > monitor.statusWindowThreshold && monitor.status !== false) { + newStatus = false; + statusChanged = true; + } + // If the failure rate is below the threshold and the monitor is down, recover: + else if (failureRate <= monitor.statusWindowThreshold && monitor.status === false) { + newStatus = true; + statusChanged = true; + } + + if (statusChanged) { + this.logger.info({ + service: this.SERVICE_NAME, + message: `${monitor.name} went from ${this.getStatusString(prevStatus)} to ${this.getStatusString(newStatus)}`, + prevStatus, + newStatus, + }); + } + + monitor.status = newStatus; await monitor.save(); return { monitor, - statusChanged: true, + statusChanged, prevStatus, code, timestamp: new Date().getTime(), From 27415aeca624ec6b90d713614fdd69b8cf26d282 Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 18 Aug 2025 15:34:15 -0700 Subject: [PATCH 19/24] fix undefined bug, updated server validation --- .../src/service/infrastructure/statusService.js | 17 ++++++++++++++--- server/src/validation/joi.js | 4 ++++ 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/server/src/service/infrastructure/statusService.js b/server/src/service/infrastructure/statusService.js index 0c3434df0..eac2d34bd 100755 --- a/server/src/service/infrastructure/statusService.js +++ b/server/src/service/infrastructure/statusService.js @@ -117,13 +117,18 @@ class StatusService { // Update running stats this.updateRunningStats({ monitor, networkResponse }); + // If the status window size has changed, empty + while (monitor.statusWindow.length > monitor.statusWindowSize) { + monitor.statusWindow.shift(); + } + // Update status sliding window monitor.statusWindow.push(status); if (monitor.statusWindow.length > monitor.statusWindowSize) { monitor.statusWindow.shift(); } - if (!monitor.status) { + if (monitor.status === undefined || monitor.status === null) { monitor.status = status; } @@ -145,10 +150,16 @@ class StatusService { // Check if threshold has been met const failures = monitor.statusWindow.filter((s) => s === false).length; - const failureRate = failures / monitor.statusWindow.length; + const failureRate = (failures / monitor.statusWindow.length) * 100; + + console.log({ + failureRate, + status: monitor.status, + statusWindow: monitor.statusWindow, + }); // If threshold has been met and the monitor is not already down, mark down: - if (failureRate > monitor.statusWindowThreshold && monitor.status !== false) { + if (failureRate >= monitor.statusWindowThreshold && monitor.status !== false) { newStatus = false; statusChanged = true; } diff --git a/server/src/validation/joi.js b/server/src/validation/joi.js index 31f8e97c0..725004afa 100755 --- a/server/src/validation/joi.js +++ b/server/src/validation/joi.js @@ -155,6 +155,8 @@ const createMonitorBodyValidation = joi.object({ name: joi.string().required(), description: joi.string().required(), type: joi.string().required(), + statusWindowSize: joi.number().min(1).max(20).default(5), + statusWindowThreshold: joi.number().min(1).max(100).default(60), url: joi.string().required(), ignoreTlsErrors: joi.boolean().default(false), port: joi.number(), @@ -183,6 +185,8 @@ const createMonitorsBodyValidation = joi.array().items( const editMonitorBodyValidation = joi.object({ name: joi.string(), + statusWindowSize: joi.number().min(1).max(20).default(5), + statusWindowThreshold: joi.number().min(1).max(100).default(60), description: joi.string(), interval: joi.number(), notifications: joi.array().items(joi.string()), From ced2417c986dd0a56927903c194cd3295749fb8f Mon Sep 17 00:00:00 2001 From: Alex Holliday Date: Mon, 18 Aug 2025 15:34:30 -0700 Subject: [PATCH 20/24] add incident config --- client/src/Hooks/monitorHooks.js | 2 ++ client/src/Pages/Uptime/Create/index.jsx | 44 ++++++++++++++++++++++++ client/src/Validation/validation.js | 10 ++++++ 3 files changed, 56 insertions(+) diff --git a/client/src/Hooks/monitorHooks.js b/client/src/Hooks/monitorHooks.js index 970b5d97f..eb1e7acdc 100644 --- a/client/src/Hooks/monitorHooks.js +++ b/client/src/Hooks/monitorHooks.js @@ -369,6 +369,8 @@ const useUpdateMonitor = () => { setIsLoading(true); const updatedFields = { name: monitor.name, + statusWindowSize: monitor.statusWindowSize, + statusWindowThreshold: monitor.statusWindowThreshold, description: monitor.description, interval: monitor.interval, notifications: monitor.notifications, diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index 668d840af..c763071da 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -55,6 +55,8 @@ const UptimeCreate = ({ isClone = false }) => { // States const [monitor, setMonitor] = useState({ type: "http", + statusWindowSize: 5, + statusWindowThreshold: 60, matchMethod: "equal", expectedValue: "", jsonPath: "", @@ -177,7 +179,10 @@ const UptimeCreate = ({ isClone = false }) => { ? `http${https ? "s" : ""}://` + monitor.url : monitor.url, name: monitor.name || monitor.url.substring(0, 50), + statusWindowSize: monitor.statusWindowSize, + statusWindowThreshold: monitor.statusWindowThreshold, type: monitor.type, + port: monitor.type === "port" || monitor.type === "game" ? monitor.port : undefined, interval: monitor.interval, @@ -192,6 +197,8 @@ const UptimeCreate = ({ isClone = false }) => { _id: monitor._id, url: monitor.url, name: monitor.name || monitor.url.substring(0, 50), + statusWindowSize: monitor.statusWindowSize, + statusWindowThreshold: monitor.statusWindowThreshold, type: monitor.type, matchMethod: monitor.matchMethod, expectedValue: monitor.expectedValue, @@ -602,6 +609,43 @@ const UptimeCreate = ({ isClone = false }) => { /> + + + + Incidents + + + { + "A sliding window is used to determine when a monitor goes down. The status of a monitor will only change when the percentage of checks in the sliding window meet the specified value." + } + + + + + + + Date: Mon, 18 Aug 2025 15:46:41 -0700 Subject: [PATCH 21/24] extract strings, sort json --- client/src/Pages/Uptime/Create/index.jsx | 12 +-- client/src/locales/en.json | 104 ++++++++++++----------- 2 files changed, 59 insertions(+), 57 deletions(-) diff --git a/client/src/Pages/Uptime/Create/index.jsx b/client/src/Pages/Uptime/Create/index.jsx index c763071da..3f905fa11 100644 --- a/client/src/Pages/Uptime/Create/index.jsx +++ b/client/src/Pages/Uptime/Create/index.jsx @@ -615,18 +615,16 @@ const UptimeCreate = ({ isClone = false }) => { component="h2" variant="h2" > - Incidents + {t("createMonitorPage.incidentConfigTitle")} - { - "A sliding window is used to determine when a monitor goes down. The status of a monitor will only change when the percentage of checks in the sliding window meet the specified value." - } + {t("createMonitorPage.incidentConfigDescription")} { /> to get the full container ID.", + "game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type.", "http": "Enter the URL or IP to monitor (e.g., https://example.com/ or 192.168.1.100) and add a clear display name that appears on the dashboard.", "ping": "Enter the IP address or hostname to ping (e.g., 192.168.1.100 or example.com) and add a clear display name that appears on the dashboard.", - "port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard.", - "game": "Enter the IP address or hostname and the port number to ping (e.g., 192.168.1.100 or example.com) and choose game type." + "port": "Enter the URL or IP of the server, the port number and a clear display name that appears on the dashboard." }, "uptimeMonitor": { "fallback": { @@ -1066,5 +1071,6 @@ "websiteMonitoring": "Website monitoring", "websiteMonitoringDescription": "Use HTTP(s) to monitor your website or API endpoint.", "whenNewIncident": "When there is a new incident,", - "window": "window" + "window": "window", + "YourPhoto": "Profile photo" } From 4a116381826efc1084357da13e0b53c0f40dff6f Mon Sep 17 00:00:00 2001 From: karenvicent Date: Wed, 20 Aug 2025 12:51:24 -0400 Subject: [PATCH 22/24] refactor create maintenance window page --- .../Components/ConfigSelect/index.jsx | 38 +++ .../hooks/useMaintenanceActions.jsx | 64 +++++ .../hooks/useMaintenanceData.jsx | 74 ++++++ .../Maintenance/CreateMaintenance/index.jsx | 246 +++--------------- 4 files changed, 219 insertions(+), 203 deletions(-) create mode 100644 client/src/Pages/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx create mode 100644 client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceActions.jsx create mode 100644 client/src/Pages/Maintenance/CreateMaintenance/hooks/useMaintenanceData.jsx diff --git a/client/src/Pages/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx b/client/src/Pages/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx new file mode 100644 index 000000000..1627ef3d5 --- /dev/null +++ b/client/src/Pages/Maintenance/CreateMaintenance/Components/ConfigSelect/index.jsx @@ -0,0 +1,38 @@ +import Select from "../../../../../Components/Inputs/Select"; +import PropTypes from "prop-types"; +const ConfigSelect = ({ configSelection, valueSelect, onChange, ...props }) => { + const getValueById = (config, id) => { + const item = config.find((config) => config._id === id); + return item ? (item.value ? item.value : item.name) : null; + }; + + const getIdByValue = (config, name) => { + const item = config.find((config) => { + if (config.value) { + return config.value === name; + } else { + return config.name === name; + } + }); + return item ? item._id : null; + }; + + return ( + { - handleFormChange( - "repeat", - getValueById(repeatConfig, event.target.value) - ); - }} - items={repeatConfig} + valueSelect={form.repeat} + configSelection={repeatConfig} + onChange={(value) => handleFormChange("repeat", value)} /> @@ -437,7 +280,7 @@ const CreateMaintenance = () => { }} sx={{}} onChange={(newDate) => { - handleTimeChange("startDate", newDate); + handleFormChange("startDate", newDate); }} error={errors["startDate"]} /> @@ -453,7 +296,7 @@ const CreateMaintenance = () => { > {t("startTime")} - {t("timeZoneInfo")} + {t("timeZoneInfo")} @@ -462,7 +305,7 @@ const CreateMaintenance = () => { label={t("startTime")} value={form.startTime} onChange={(newTime) => { - handleTimeChange("startTime", newTime); + handleFormChange("startTime", newTime); }} slotProps={{ nextIconButton: { sx: { ml: theme.spacing(2) } }, @@ -508,16 +351,11 @@ const CreateMaintenance = () => { error={errors["duration"] ? true : false} helperText={errors["duration"]} /> -