Compare commits

...
53 Commits
Author SHA1 Message Date
Alexander Holliday 8f971cfb02 Merge pull request #2826 from bluewave-labs/develop
develop -> master
2025-08-20 14:05:35 -07:00
Alexander Holliday 32c811ec5a Merge pull request #2827 from bluewave-labs/fix/version-bump
fix: bump version
2025-08-20 14:04:34 -07:00
Alex Holliday 4febc56c30 bump version 2025-08-20 14:04:02 -07:00
Alexander Holliday f22ee4a68b Merge pull request #2824 from bluewave-labs/fix/remove-log
remove console statement
2025-08-20 14:01:57 -07:00
Alex Holliday 87bec0399f remove console statement 2025-08-20 14:01:32 -07:00
Alexander Holliday 1f29255dd1 Merge pull request #2821 from bluewave-labs/refactor/create-maintenance-window
refactor create maintenance window page
2025-08-20 13:29:56 -07:00
karenvicent 4a11638182 refactor create maintenance window page 2025-08-20 12:51:24 -04:00
Alexander Holliday 10743372f8 Merge pull request #2813 from bluewave-labs/feat/n-of-m-frontend
feat/n of m frontend
2025-08-19 08:08:52 -07:00
Alex Holliday d4b11bbb76 extract strings, sort json 2025-08-18 15:46:41 -07:00
Alex Holliday ced2417c98 add incident config 2025-08-18 15:34:30 -07:00
Alex Holliday 27415aeca6 fix undefined bug, updated server validation 2025-08-18 15:34:15 -07:00
Alexander Holliday 9bd2e336a6 Merge pull request #2811 from bluewave-labs/feat/alerts-n-of-m
feat: alerts on  `n` of `m`, resolves #2366
2025-08-18 14:16:35 -07:00
Alex Holliday d5778ec173 update stauts service to use n of m status changes 2025-08-18 13:40:56 -07:00
Alex Holliday 74e80f086a udpate monitor model 2025-08-18 13:40:22 -07:00
Alexander Holliday b1c6191fa1 Merge pull request #2809 from bluewave-labs/fix/fe/minor-changes
Fix: Minor Changes
2025-08-18 12:56:00 -07:00
Alexander Holliday 8e0245c844 Merge pull request #2807 from bluewave-labs/feat/simplify-checks
feat: unify check models
2025-08-18 12:54:45 -07:00
Br0wnHammer 613fbf8c39 Fix: Minor Changes 2025-08-19 01:11:56 +05:30
Alex Holliday 1788cda5c2 let -> const 2025-08-18 11:27:41 -07:00
Alex Holliday 155adf549e add backend changes 2025-08-18 10:55:12 -07:00
Alex Holliday 5463542532 unify check models 2025-08-18 10:52:00 -07:00
Alexander Holliday f5a25bc0e5 Merge pull request #2806 from bluewave-labs/fix/pager_duty
fix: update pager_duty validation, resolves #2805
2025-08-18 09:17:22 -07:00
Alex Holliday 993339d0d1 update pager_duty validation 2025-08-17 20:21:18 -07:00
Alexander Holliday bc4fe897cf Merge pull request #2804 from bluewave-labs/refactor-create-infrastruture
Refactor create infrastruture
2025-08-17 15:16:28 -07:00
Alexander Holliday c773bdaabd Merge pull request #2802 from bluewave-labs/feat/network-service-refactor
feat: network service refactor
2025-08-17 15:08:40 -07:00
Alex Holliday 9f9a2be1d8 fix missing URL 2025-08-17 15:08:12 -07:00
karenvicent 1e4b0c2f20 Merge branch 'develop' into refactor-create-infrastruture 2025-08-17 15:28:49 -04:00
karenvicent 56025f6d56 refactor create infraestructure monitor page 2025-08-17 15:28:06 -04:00
Alexander Holliday 5188b90f32 Merge pull request #2800 from mattstein/fix/pagespeed-validation
Fix: resolve erroneous PageSpeed settings validation
2025-08-15 17:47:30 -07:00
Matt Stein 7178f537a8 Update with changes from code review. 2025-08-15 16:40:21 -07:00
Alex Holliday 45ac589975 refactor network service 2025-08-15 16:04:43 -07:00
Alex Holliday 9925e10ffc add timings to model 2025-08-15 16:03:12 -07:00
Alex Holliday e3fde8d564 add got 2025-08-15 16:02:52 -07:00
Matt Stein 8aee01e008 Fix PageSpeed settings validation. 2025-08-15 14:34:42 -07:00
Alexander Holliday cd203ff067 Merge pull request #2794 from Owaiseimdad/Network-patch-fixes-part-1
Fixed the selector and also null points for networks
2025-08-15 13:12:04 -07:00
Alexander Holliday 254cb6d80f Merge pull request #2788 from bluewave-labs/fix/filter-header-alignment
Fix: Improve vertical alignment of text and plus icons in filter headers
2025-08-15 12:21:50 -07:00
Alexander Holliday 9b3ad54d0a Merge pull request #2797 from bluewave-labs/develop
develop -> master
2025-08-15 12:21:10 -07:00
Alexander Holliday 4ae3bb5780 Merge pull request #2795 from bluewave-labs/hotfix/path
fix path, remove redis form build script
2025-08-15 09:18:18 -07:00
Alex Holliday b8aa1ef765 fix path, remove redis form build script 2025-08-15 09:17:33 -07:00
gorkem-bwl 5e5daef49a 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.
2025-08-15 03:38:22 -04:00
Owaise 88c478c533 Formatting is done. 2025-08-15 11:37:38 +05:30
Owaise 1ecec115e4 Fixed the selector and also null points for networks. 2025-08-15 11:28:22 +05:30
Alexander Holliday 8ff597eedb Merge pull request #2791 from bluewave-labs/fix/remove-logs-descriptions
Fix: Remove description text and dividers from Logs sections
2025-08-14 20:36:44 -07:00
Alexander Holliday 69f74488e5 Merge pull request #2790 from bluewave-labs/fix/logs-default-tab
Fix: Set Server Logs as default tab on Logs page
2025-08-14 20:34:15 -07:00
gorkem-bwl ded9a27f42 fix: remove excess spacing after removing description text
Removed top margin (mt={theme.spacing(10)}) and adjusted sticky position
from the log level selector since it was creating unwanted space between
the tab bar and the 'Log level' dropdown after removing the description
text and divider.
2025-08-14 22:22:05 -04:00
gorkem-bwl 43284ae625 fix: remove description text and dividers from Logs sections
Removed description strings and divider lines from:
- Server logs: 'System logs - last 1000 lines' and divider
- Queue metrics: queue description and divider

This cleans up the UI by removing redundant descriptive text
that users can infer from the tab names and content context.
2025-08-14 22:19:54 -04:00
gorkem-bwl f7b1ef9741 fix: set Server Logs as default tab on Logs page
Changed default tab from Diagnostics (index 2) to Server Logs (index 0)
when users navigate to the Logs section from the sidebar. This makes
Server Logs the active tab by default, which is more intuitive since
it's the first tab in the list.
2025-08-14 22:11:47 -04:00
gorkem-bwl a6d445818f 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.
2025-08-14 20:15:56 -04:00
Alexander Holliday 122a84b9e2 Merge pull request #2787 from bluewave-labs/develop
develop -> master for 3.0-beta
2025-08-14 15:28:35 -07:00
Alexander Holliday 9716e64c93 Merge pull request #2612 from bluewave-labs/develop
Develop -> Master
2025-07-14 14:23:40 -07:00
Alexander Holliday eb8cf1b6da Merge pull request #2584 from bluewave-labs/develop
bump version
2025-07-08 09:51:08 -07:00
Alexander Holliday c633a9ad8d Merge pull request #2582 from bluewave-labs/develop
develop -> master
2025-07-07 15:38:46 -07:00
Alexander Holliday 923eda1307 Merge pull request #2545 from bluewave-labs/develop
develop -> master
2025-06-25 12:42:33 +08:00
Alexander Holliday 997342a31c Merge pull request #2536 from bluewave-labs/develop
Develop -> Master for 2.2 release
2025-06-25 09:38:08 +08:00
51 changed files with 1580 additions and 1716 deletions

No files matched your search

@@ -1,7 +1,6 @@
import { useTheme } from "@emotion/react"; import { useTheme } from "@emotion/react";
import Box from "@mui/material/Box"; import Box from "@mui/material/Box";
import Background from "../../assets/Images/background-grid.svg?react"; 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 OutputAnimation from "../../assets/Animations/output.gif";
import DarkmodeOutput from "../../assets/Animations/darkmodeOutput.gif"; import DarkmodeOutput from "../../assets/Animations/darkmodeOutput.gif";
import { useSelector } from "react-redux"; import { useSelector } from "react-redux";
@@ -13,7 +12,7 @@ const FallbackBackground = () => {
<Box <Box
component="img" component="img"
src={mode === "light" ? OutputAnimation : DarkmodeOutput} src={mode === "light" ? OutputAnimation : DarkmodeOutput}
Background="transparent" background="transparent"
alt="Loading animation" alt="Loading animation"
sx={{ sx={{
zIndex: 1, zIndex: 1,
+1 -1
View File
@@ -74,7 +74,7 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" })
<Typography <Typography
variant="body1" variant="body1"
sx={{ sx={{
color: theme.palette.text.secondary, color: theme.palette.primary.contrastTextTertiary,
fontSize: "0.938rem", fontSize: "0.938rem",
lineHeight: 1.5, lineHeight: 1.5,
mb: 1, mb: 1,
+2
View File
@@ -369,6 +369,8 @@ const useUpdateMonitor = () => {
setIsLoading(true); setIsLoading(true);
const updatedFields = { const updatedFields = {
name: monitor.name, name: monitor.name,
statusWindowSize: monitor.statusWindowSize,
statusWindowThreshold: monitor.statusWindowThreshold,
description: monitor.description, description: monitor.description,
interval: monitor.interval, interval: monitor.interval,
notifications: monitor.notifications, notifications: monitor.notifications,
@@ -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 (
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("infrastructureCustomizeAlerts")}
</Typography>
<Typography component="p">
{t("infrastructureAlertNotificationDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(6)}>
{METRICS.map((metric) => {
return (
<CustomThreshold
key={metric}
infrastructureMonitor={infrastructureMonitor}
errors={errors}
checkboxId={metric}
checkboxName={metric}
checkboxLabel={
metric !== "cpu" ? capitalizeFirstLetter(metric) : metric.toUpperCase()
}
onCheckboxChange={handleCheckboxChange}
isChecked={infrastructureMonitor[metric]}
fieldId={METRIC_PREFIX + metric}
fieldName={METRIC_PREFIX + metric}
fieldValue={String(infrastructureMonitor[METRIC_PREFIX + metric])}
onFieldChange={onChange}
alertUnit={metric == "temperature" ? "°C" : "%"}
/>
);
})}
{hasAlertError(errors) && (
<Typography
component="span"
className="input-error"
color={theme.palette.error.main}
mt={theme.spacing(2)}
sx={{
opacity: 0.8,
}}
>
{getAlertError(errors)}
</Typography>
)}
</Stack>
</ConfigBox>
);
};
CustomAlertsSection.propTypes = {
errors: PropTypes.object.isRequired,
onChange: PropTypes.func.isRequired,
infrastructureMonitor: PropTypes.object.isRequired,
handleCheckboxChange: PropTypes.func.isRequired,
};
export default CustomAlertsSection;
@@ -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 (
<Box
alignSelf="flex-end"
ml="auto"
>
<Button
onClick={handlePause}
loading={isBusy}
variant="contained"
color="secondary"
sx={{
pl: theme.spacing(4),
pr: theme.spacing(6),
"& svg": {
mr: theme.spacing(2),
"& path": {
stroke: theme.palette.primary.contrastTextTertiary,
strokeWidth: 0.1,
},
},
}}
>
{monitor?.isActive ? (
<>
<PauseCircleOutlineIcon />
{t("pause")}
</>
) : (
<>
<PlayCircleOutlineRoundedIcon />
{t("resume")}
</>
)}
</Button>
<Button
loading={isBusy}
variant="contained"
color="error"
onClick={() => setIsOpen(true)}
sx={{ ml: theme.spacing(6) }}
>
{t("remove")}
</Button>
<Dialog
open={isOpen}
theme={theme}
title={t("deleteDialogTitle")}
description={t("deleteDialogDescription")}
onCancel={() => setIsOpen(false)}
confirmationButtonLabel={t("delete")}
onConfirm={handleRemove}
/>
</Box>
);
};
MonitorActionButtons.propTypes = {
monitor: PropTypes.object.isRequired,
isBusy: PropTypes.bool.isRequired,
handlePause: PropTypes.func.isRequired,
handleRemove: PropTypes.func.isRequired,
};
export default MonitorActionButtons;
@@ -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 (
<Stack
direction="row"
alignItems="center"
height="fit-content"
gap={theme.spacing(2)}
>
<Tooltip
title={pagespeedStatusMsg[determineState(monitor)]}
disableInteractive
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: { offset: [0, -8] },
},
],
},
}}
>
<Box>
<PulseDot color={statusColor[determineState(monitor)]} />
</Box>
</Tooltip>
<Typography
component="h2"
variant="monitorUrl"
>
{infrastructureMonitor.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Typography
position="relative"
variant="body2"
ml={theme.spacing(6)}
mt={theme.spacing(1)}
sx={{
"&:before": {
position: "absolute",
content: `""`,
width: theme.spacing(2),
height: theme.spacing(2),
borderRadius: "50%",
backgroundColor: theme.palette.primary.contrastTextTertiary,
opacity: 0.8,
left: theme.spacing(-5),
top: "50%",
transform: "translateY(-50%)",
},
}}
>
{t("editing")}
</Typography>
</Stack>
);
};
MonitorStatusHeader.propTypes = {
monitor: PropTypes.object.isRequired,
infrastructureMonitor: PropTypes.object.isRequired,
};
export default MonitorStatusHeader;
@@ -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;
@@ -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;
@@ -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;
+41 -351
View File
@@ -1,40 +1,33 @@
//Components //Components
import Breadcrumbs from "../../../Components/Breadcrumbs"; import Breadcrumbs from "../../../Components/Breadcrumbs";
import ConfigBox from "../../../Components/ConfigBox"; import ConfigBox from "../../../Components/ConfigBox";
import Dialog from "../../../Components/Dialog";
import FieldWrapper from "../../../Components/Inputs/FieldWrapper"; import FieldWrapper from "../../../Components/Inputs/FieldWrapper";
import Link from "../../../Components/Link"; 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 Select from "../../../Components/Inputs/Select";
import TextInput from "../../../Components/Inputs/TextInput"; import TextInput from "../../../Components/Inputs/TextInput";
import { Box, Stack, Tooltip, Typography, Button, ButtonGroup } from "@mui/material"; import { Box, Stack, Typography, Button, ButtonGroup } from "@mui/material";
import { CustomThreshold } from "./Components/CustomThreshold";
import { HttpAdornment } from "../../../Components/Inputs/TextInput/Adornments"; 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 // Utils
import NotificationsConfig from "../../../Components/NotificationConfig"; import NotificationsConfig from "../../../Components/NotificationConfig";
import { capitalizeFirstLetter } from "../../../Utils/stringUtils";
import { infrastructureMonitorValidation } from "../../../Validation/validation";
import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications"; import { useGetNotificationsByTeamId } from "../../../Hooks/useNotifications";
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useSelector } from "react-redux";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useTheme } from "@emotion/react"; import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
useCreateMonitor,
useDeleteMonitor, useDeleteMonitor,
useFetchGlobalSettings, useFetchGlobalSettings,
useFetchHardwareMonitorById, useFetchHardwareMonitorById,
usePauseMonitor, usePauseMonitor,
useUpdateMonitor,
} from "../../../Hooks/monitorHooks"; } from "../../../Hooks/monitorHooks";
import useInfrastructureMonitorForm from "./hooks/useInfrastructureMonitorForm";
import useValidateInfrastructureForm from "./hooks/useValidateInfrastructureForm";
import useInfrastructureSubmit from "./hooks/useInfrastructureSubmit";
const CreateInfrastructureMonitor = () => { const CreateInfrastructureMonitor = () => {
const { user } = useSelector((state) => state.auth);
const { monitorId } = useParams(); const { monitorId } = useParams();
const isCreate = typeof monitorId === "undefined"; const isCreate = typeof monitorId === "undefined";
@@ -42,39 +35,29 @@ const CreateInfrastructureMonitor = () => {
const { t } = useTranslation(); const { t } = useTranslation();
// State // State
const [errors, setErrors] = useState({});
const [https, setHttps] = useState(false); const [https, setHttps] = useState(false);
const [isOpen, setIsOpen] = useState(false);
const [updateTrigger, setUpdateTrigger] = 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 // Fetch monitor details if editing
const { statusColor, pagespeedStatusMsg, determineState } = useMonitorUtils();
const [monitor, isLoading] = useFetchHardwareMonitorById({ const [monitor, isLoading] = useFetchHardwareMonitorById({
monitorId, monitorId,
updateTrigger, updateTrigger,
}); });
const [createMonitor, isCreating] = useCreateMonitor();
const [deleteMonitor, isDeleting] = useDeleteMonitor(); const [deleteMonitor, isDeleting] = useDeleteMonitor();
const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings(); const [globalSettings, globalSettingsLoading] = useFetchGlobalSettings();
const [notifications, notificationsAreLoading] = useGetNotificationsByTeamId(); const [notifications, notificationsAreLoading] = useGetNotificationsByTeamId();
const [pauseMonitor, isPausing] = usePauseMonitor(); 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 = [ const FREQUENCIES = [
{ _id: 0.25, name: t("time.fifteenSeconds") }, { _id: 0.25, name: t("time.fifteenSeconds") },
@@ -93,157 +76,27 @@ const CreateInfrastructureMonitor = () => {
{ name: "Configure", path: `/infrastructure/configure/${monitorId}` }, { 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 // Populate form fields if editing
useEffect(() => { useEffect(() => {
if (isCreate) { if (isCreate) {
if (globalSettingsLoading) return; if (globalSettingsLoading) return;
const gt = globalSettings?.data?.settings?.globalThresholds || {};
setHttps(false); setHttps(false);
initializeInfrastructureMonitorForCreate(globalSettings);
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: "",
});
} else if (monitor) { } else if (monitor) {
const { thresholds = {} } = monitor;
setHttps(monitor.url.startsWith("https")); setHttps(monitor.url.startsWith("https"));
initializeInfrastructureMonitorForUpdate(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 || "",
});
} }
}, [isCreate, monitor, globalSettings, globalSettingsLoading]); }, [isCreate, monitor, globalSettings, globalSettingsLoading]);
// Handlers // Handlers
const onSubmit = async (event) => { const onSubmit = async (event) => {
event.preventDefault(); event.preventDefault();
const form = buildForm(infrastructureMonitor, https);
// Build the form const error = validateForm(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,
});
if (error) { 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; return;
} }
submitInfrastructureForm(infrastructureMonitor, form, isCreate, monitorId);
// 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" });
}; };
const triggerUpdate = () => { const triggerUpdate = () => {
@@ -252,28 +105,8 @@ const CreateInfrastructureMonitor = () => {
const onChange = (event) => { const onChange = (event) => {
const { value, name } = event.target; const { value, name } = event.target;
setInfrastructureMonitor({ onChangeForm(name, value);
...infrastructureMonitor, validateField(name, value);
[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,
});
}; };
const handlePause = async () => { const handlePause = async () => {
@@ -336,105 +169,19 @@ const CreateInfrastructureMonitor = () => {
)} )}
</Typography> </Typography>
{!isCreate && ( {!isCreate && (
<Stack <MonitorStatusHeader
direction="row" monitor={monitor}
alignItems="center" infrastructureMonitor={infrastructureMonitor}
height="fit-content" />
gap={theme.spacing(2)}
>
<Tooltip
title={pagespeedStatusMsg[determineState(monitor)]}
disableInteractive
slotProps={{
popper: {
modifiers: [
{
name: "offset",
options: { offset: [0, -8] },
},
],
},
}}
>
<Box>
<PulseDot color={statusColor[determineState(monitor)]} />
</Box>
</Tooltip>
<Typography
component="h2"
variant="monitorUrl"
>
{infrastructureMonitor.url?.replace(/^https?:\/\//, "") || "..."}
</Typography>
<Typography
position="relative"
variant="body2"
ml={theme.spacing(6)}
mt={theme.spacing(1)}
sx={{
"&:before": {
position: "absolute",
content: `""`,
width: theme.spacing(2),
height: theme.spacing(2),
borderRadius: "50%",
backgroundColor: theme.palette.primary.contrastTextTertiary,
opacity: 0.8,
left: theme.spacing(-5),
top: "50%",
transform: "translateY(-50%)",
},
}}
>
{t("editing")}
</Typography>
</Stack>
)} )}
</Box> </Box>
{!isCreate && ( {!isCreate && (
<Box <MonitorActionButtons
alignSelf="flex-end" monitor={monitor}
ml="auto" isBusy={isBusy}
> handlePause={handlePause}
<Button handleRemove={handleRemove}
onClick={handlePause} />
loading={isBusy}
variant="contained"
color="secondary"
sx={{
pl: theme.spacing(4),
pr: theme.spacing(6),
"& svg": {
mr: theme.spacing(2),
"& path": {
stroke: theme.palette.primary.contrastTextTertiary,
strokeWidth: 0.1,
},
},
}}
>
{monitor?.isActive ? (
<>
<PauseCircleOutlineIcon />
{t("pause")}
</>
) : (
<>
<PlayCircleOutlineRoundedIcon />
{t("resume")}
</>
)}
</Button>
<Button
loading={isBusy}
variant="contained"
color="error"
onClick={() => setIsOpen(true)}
sx={{ ml: theme.spacing(6) }}
>
{t("remove")}
</Button>
</Box>
)} )}
</Stack> </Stack>
<ConfigBox> <ConfigBox>
@@ -534,58 +281,12 @@ const CreateInfrastructureMonitor = () => {
setNotifications={infrastructureMonitor.notifications} setNotifications={infrastructureMonitor.notifications}
/> />
</ConfigBox> </ConfigBox>
<ConfigBox> <CustomAlertsSection
<Box> errors={errors}
<Typography onChange={onChange}
component="h2" infrastructureMonitor={infrastructureMonitor}
variant="h2" handleCheckboxChange={handleCheckboxChange}
> />
{t("infrastructureCustomizeAlerts")}
</Typography>
<Typography component="p">
{t("infrastructureAlertNotificationDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(6)}>
{METRICS.map((metric) => {
return (
<CustomThreshold
key={metric}
infrastructureMonitor={infrastructureMonitor}
errors={errors}
checkboxId={metric}
checkboxName={metric}
checkboxLabel={
metric !== "cpu"
? capitalizeFirstLetter(metric)
: metric.toUpperCase()
}
onCheckboxChange={handleCheckboxChange}
isChecked={infrastructureMonitor[metric]}
fieldId={METRIC_PREFIX + metric}
fieldName={METRIC_PREFIX + metric}
fieldValue={String(infrastructureMonitor[METRIC_PREFIX + metric])}
onFieldChange={onChange}
alertUnit={metric == "temperature" ? "°C" : "%"}
/>
);
})}
{/* Error text */}
{hasAlertError(errors) && (
<Typography
component="span"
className="input-error"
color={theme.palette.error.main}
mt={theme.spacing(2)}
sx={{
opacity: 0.8,
}}
>
{getAlertError(errors)}
</Typography>
)}
</Stack>
</ConfigBox>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography <Typography
@@ -620,17 +321,6 @@ const CreateInfrastructureMonitor = () => {
</Button> </Button>
</Stack> </Stack>
</Stack> </Stack>
{!isCreate && (
<Dialog
open={isOpen}
theme={theme}
title={t("deleteDialogTitle")}
description={t("deleteDialogDescription")}
onCancel={() => setIsOpen(false)}
confirmationButtonLabel={t("delete")}
onConfirm={handleRemove}
/>
)}
</Box> </Box>
); );
}; };
@@ -1,8 +1,9 @@
import PropTypes from "prop-types"; import PropTypes from "prop-types";
import { useState, useEffect } from "react"; 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 { useTranslation } from "react-i18next";
import { useTheme } from "@emotion/react"; import { useTheme } from "@emotion/react";
import Select from "../../../../../Components/Inputs/Select";
import NetworkStatBoxes from "./NetworkStatBoxes"; import NetworkStatBoxes from "./NetworkStatBoxes";
import NetworkCharts from "./NetworkCharts"; import NetworkCharts from "./NetworkCharts";
import MonitorTimeFrameHeader from "../../../../../Components/MonitorTimeFrameHeader"; import MonitorTimeFrameHeader from "../../../../../Components/MonitorTimeFrameHeader";
@@ -63,27 +64,17 @@ const Network = ({ net, checks, isLoading, dateRange, setDateRange }) => {
gap={theme.spacing(4)} gap={theme.spacing(4)}
> >
{availableInterfaces.length > 0 && ( {availableInterfaces.length > 0 && (
<FormControl <Select
variant="outlined" name="networkInterface"
size="small" label={t("networkInterface")}
value={selectedInterface}
onChange={(e) => setSelectedInterface(e.target.value)}
items={availableInterfaces.map((interfaceName) => ({
_id: interfaceName,
name: interfaceName,
}))}
sx={{ minWidth: 200 }} sx={{ minWidth: 200 }}
> />
<InputLabel>{t("networkInterface")}</InputLabel>
<Select
value={selectedInterface}
onChange={(e) => setSelectedInterface(e.target.value)}
label={t("networkInterface")}
>
{availableInterfaces.map((interfaceName) => (
<MenuItem
key={interfaceName}
value={interfaceName}
>
{interfaceName}
</MenuItem>
))}
</Select>
</FormControl>
)} )}
<MonitorTimeFrameHeader <MonitorTimeFrameHeader
isLoading={isLoading} isLoading={isLoading}
+1 -21
View File
@@ -51,34 +51,14 @@ const Logs = () => {
]; ];
return ( return (
<Stack gap={theme.spacing(4)}> <Stack gap={theme.spacing(4)}>
<Box
sx={{
position: "sticky",
top: theme.spacing(17),
backdropFilter: "blur(10px)",
paddingY: theme.spacing(5),
paddingLeft: theme.spacing(6),
}}
>
<Typography variant="h2">{t("logsPage.description")}</Typography>
</Box>
<Divider
color={theme.palette.accent.main}
sx={{
position: "sticky",
top: theme.spacing(33),
backdropFilter: "blur(10px)",
}}
/>
<Stack <Stack
direction="row" direction="row"
alignItems="center" alignItems="center"
gap={theme.spacing(4)} gap={theme.spacing(4)}
mt={theme.spacing(10)}
sx={{ sx={{
position: "sticky", position: "sticky",
top: theme.spacing(34), top: theme.spacing(17),
backdropFilter: "blur(10px)", backdropFilter: "blur(10px)",
paddingTop: theme.spacing(4), paddingTop: theme.spacing(4),
paddingLeft: theme.spacing(6), paddingLeft: theme.spacing(6),
-2
View File
@@ -29,8 +29,6 @@ const QueueDetails = () => {
return ( return (
<Stack gap={theme.spacing(4)}> <Stack gap={theme.spacing(4)}>
<Typography variant="h2">{t("queuePage.metricsTable.title")}</Typography>
<Divider color={theme.palette.accent.main} />
<Stack <Stack
gap={theme.spacing(20)} gap={theme.spacing(20)}
mt={theme.spacing(10)} mt={theme.spacing(10)}
+1 -1
View File
@@ -15,7 +15,7 @@ const Logs = () => {
const theme = useTheme(); const theme = useTheme();
// Local state // Local state
const [value, setValue] = useState(2); const [value, setValue] = useState(0);
// Handlers // Handlers
const handleChange = (event, newValue) => { const handleChange = (event, newValue) => {
@@ -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 (
<Select
value={getIdByValue(configSelection, valueSelect)}
onChange={(event) => {
const newValue = getValueById(configSelection, event.target.value);
onChange(newValue);
}}
items={configSelection}
{...props}
/>
);
};
ConfigSelect.propTypes = {
configSelection: PropTypes.array.isRequired,
valueSelect: PropTypes.string.isRequired,
onChange: PropTypes.func.isRequired,
};
export default ConfigSelect;
@@ -0,0 +1,64 @@
import { networkService } from "../../../../main";
import dayjs from "dayjs";
import {
MS_PER_SECOND,
MS_PER_MINUTE,
MS_PER_HOUR,
MS_PER_DAY,
MS_PER_WEEK,
} from "../../../../Utils/timeUtils";
const useMaintenanceActions = () => {
const MS_LOOKUP = {
seconds: MS_PER_SECOND,
minutes: MS_PER_MINUTE,
hours: MS_PER_HOUR,
days: MS_PER_DAY,
weeks: MS_PER_WEEK,
};
const REPEAT_LOOKUP = {
none: 0,
daily: MS_PER_DAY,
weekly: MS_PER_DAY * 7,
};
const handleSubmitForm = async (maintenanceWindowId, form) => {
const start = dayjs(form.startDate)
.set("hour", form.startTime.hour())
.set("minute", form.startTime.minute());
const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit];
const durationInMs = form.duration * MS_MULTIPLIER;
const end = start.add(durationInMs);
const repeat = REPEAT_LOOKUP[form.repeat];
const submit = {
monitors: form.monitors.map((monitor) => monitor._id),
name: form.name,
start: start.toISOString(),
end: end.toISOString(),
repeat,
};
if (repeat === 0) {
submit.expiry = end;
}
const requestConfig = { maintenanceWindow: submit };
if (maintenanceWindowId !== undefined) {
requestConfig.maintenanceWindowId = maintenanceWindowId;
}
const request =
maintenanceWindowId === undefined
? networkService.createMaintenanceWindow(requestConfig)
: networkService.editMaintenanceWindow(requestConfig);
return request;
};
return {
handleSubmitForm,
};
};
export default useMaintenanceActions;
@@ -0,0 +1,74 @@
import { networkService } from "../../../../main";
import dayjs from "dayjs";
import {
MS_PER_SECOND,
MS_PER_MINUTE,
MS_PER_HOUR,
MS_PER_DAY,
MS_PER_WEEK,
} from "../../../../Utils/timeUtils";
const useMaintenanceData = () => {
const REVERSE_REPEAT_LOOKUP = {
0: "none",
[MS_PER_DAY]: "daily",
[MS_PER_WEEK]: "weekly",
};
const getDurationAndUnit = (durationInMs) => {
if (durationInMs % MS_PER_DAY === 0) {
return {
duration: (durationInMs / MS_PER_DAY).toString(),
durationUnit: "days",
};
} else if (durationInMs % MS_PER_HOUR === 0) {
return {
duration: (durationInMs / MS_PER_HOUR).toString(),
durationUnit: "hours",
};
} else if (durationInMs % MS_PER_MINUTE === 0) {
return {
duration: (durationInMs / MS_PER_MINUTE).toString(),
durationUnit: "minutes",
};
} else {
return {
duration: (durationInMs / MS_PER_SECOND).toString(),
durationUnit: "seconds",
};
}
};
const fetchMonitorsMaintenance = async () => {
const response = await networkService.getMonitorsByTeamId({
limit: null,
types: ["http", "ping", "pagespeed", "port"],
});
const fetchedMonitors = response.data.data.monitors;
return fetchedMonitors;
};
const initializeMaintenanceForEdit = async (maintenanceWindowId, monitorList) => {
const res = await networkService.getMaintenanceWindowById({
maintenanceWindowId: maintenanceWindowId,
});
const maintenanceWindow = res.data.data;
const { name, start, end, repeat, monitorId } = maintenanceWindow;
const startTime = dayjs(start);
const endTime = dayjs(end);
const durationInMs = endTime.diff(startTime, "milliseconds").toString();
const { duration, durationUnit } = getDurationAndUnit(durationInMs);
const monitor = monitorList.find((monitor) => monitor._id === monitorId);
const maintenanceWindowInformation = {
name,
repeat: REVERSE_REPEAT_LOOKUP[repeat],
startDate: startTime,
startTime,
duration,
durationUnit,
monitors: monitor ? [monitor] : [],
};
return maintenanceWindowInformation;
};
return { fetchMonitorsMaintenance, initializeMaintenanceForEdit };
};
export default useMaintenanceData;
@@ -10,71 +10,21 @@ import { MobileTimePicker } from "@mui/x-date-pickers/MobileTimePicker";
import { maintenanceWindowValidation } from "../../../Validation/validation"; import { maintenanceWindowValidation } from "../../../Validation/validation";
import { createToast } from "../../../Utils/toastUtils"; import { createToast } from "../../../Utils/toastUtils";
import MonitorList from "./Components/MonitorList"; import MonitorList from "./Components/MonitorList";
import Checkbox from "../../../Components/Inputs/Checkbox"; import ConfigSelect from "./Components/ConfigSelect";
import useMaintenanceData from "./hooks/useMaintenanceData";
import useMaintenanceActions from "./hooks/useMaintenanceActions";
import dayjs from "dayjs";
import Select from "../../../Components/Inputs/Select";
import TextInput from "../../../Components/Inputs/TextInput"; import TextInput from "../../../Components/Inputs/TextInput";
import Breadcrumbs from "../../../Components/Breadcrumbs"; import Breadcrumbs from "../../../Components/Breadcrumbs";
import CalendarIcon from "../../../assets/icons/calendar.svg?react"; import CalendarIcon from "../../../assets/icons/calendar.svg?react";
import "./index.css"; import "./index.css";
import Search from "../../../Components/Inputs/Search"; import Search from "../../../Components/Inputs/Search";
import { networkService } from "../../../main";
import { logger } from "../../../Utils/Logger"; import { logger } from "../../../Utils/Logger";
import {
MS_PER_SECOND,
MS_PER_MINUTE,
MS_PER_HOUR,
MS_PER_DAY,
MS_PER_WEEK,
} from "../../../Utils/timeUtils";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import { buildErrors, hasValidationErrors } from "../../../Validation/error"; import { buildErrors, hasValidationErrors } from "../../../Validation/error";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import dayjs from "dayjs";
const getDurationAndUnit = (durationInMs) => {
if (durationInMs % MS_PER_DAY === 0) {
return {
duration: (durationInMs / MS_PER_DAY).toString(),
durationUnit: "days",
};
} else if (durationInMs % MS_PER_HOUR === 0) {
return {
duration: (durationInMs / MS_PER_HOUR).toString(),
durationUnit: "hours",
};
} else if (durationInMs % MS_PER_MINUTE === 0) {
return {
duration: (durationInMs / MS_PER_MINUTE).toString(),
durationUnit: "minutes",
};
} else {
return {
duration: (durationInMs / MS_PER_SECOND).toString(),
durationUnit: "seconds",
};
}
};
const MS_LOOKUP = {
seconds: MS_PER_SECOND,
minutes: MS_PER_MINUTE,
hours: MS_PER_HOUR,
days: MS_PER_DAY,
weeks: MS_PER_WEEK,
};
const REPEAT_LOOKUP = {
none: 0,
daily: MS_PER_DAY,
weekly: MS_PER_DAY * 7,
};
const REVERSE_REPEAT_LOOKUP = {
0: "none",
[MS_PER_DAY]: "daily",
[MS_PER_WEEK]: "weekly",
};
const repeatConfig = [ const repeatConfig = [
{ _id: 0, name: "Don't repeat", value: "none" }, { _id: 0, name: "Don't repeat", value: "none" },
@@ -96,24 +46,10 @@ const durationConfig = [
}, },
]; ];
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;
};
const CreateMaintenance = () => { const CreateMaintenance = () => {
const { maintenanceWindowId } = useParams(); const { maintenanceWindowId } = useParams();
const { handleSubmitForm } = useMaintenanceActions();
const { fetchMonitorsMaintenance, initializeMaintenanceForEdit } = useMaintenanceData();
const navigate = useNavigate(); const navigate = useNavigate();
const theme = useTheme(); const theme = useTheme();
const { t } = useTranslation(); const { t } = useTranslation();
@@ -121,6 +57,7 @@ const CreateMaintenance = () => {
const [monitors, setMonitors] = useState([]); const [monitors, setMonitors] = useState([]);
const [search, setSearch] = useState(""); const [search, setSearch] = useState("");
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [errors, setErrors] = useState({});
const [form, setForm] = useState({ const [form, setForm] = useState({
repeat: "none", repeat: "none",
startDate: dayjs(), startDate: dayjs(),
@@ -130,43 +67,35 @@ const CreateMaintenance = () => {
name: "", name: "",
monitors: [], monitors: [],
}); });
const [errors, setErrors] = useState({}); const handleFormChange = (key, value) => {
setForm((prev) => ({ ...prev, [key]: value }));
const { error } = maintenanceWindowValidation.validate(
{ [key]: value },
{ abortEarly: false }
);
setErrors((prev) => {
return buildErrors(prev, key, error);
});
};
useEffect(() => { useEffect(() => {
const fetchMonitors = async () => { const fetchMonitors = async () => {
setIsLoading(true); setIsLoading(true);
try { try {
const response = await networkService.getMonitorsByTeamId({ const fetchedMonitors = await fetchMonitorsMaintenance();
limit: null, setMonitors(fetchedMonitors);
types: ["http", "ping", "pagespeed", "port"],
});
const monitors = response.data.data.monitors;
setMonitors(monitors);
if (maintenanceWindowId === undefined) { if (maintenanceWindowId === undefined) {
return; return;
} }
const maintenanceWindowInformation = await initializeMaintenanceForEdit(
const res = await networkService.getMaintenanceWindowById({ maintenanceWindowId,
maintenanceWindowId: maintenanceWindowId, fetchedMonitors
}); );
const maintenanceWindow = res.data.data; setForm((prev) => ({
const { name, start, end, repeat, monitorId } = maintenanceWindow; ...prev,
const startTime = dayjs(start); ...maintenanceWindowInformation,
const endTime = dayjs(end); }));
const durationInMs = endTime.diff(startTime, "milliseconds").toString();
const { duration, durationUnit } = getDurationAndUnit(durationInMs);
const monitor = monitors.find((monitor) => monitor._id === monitorId);
setForm({
...form,
name,
repeat: REVERSE_REPEAT_LOOKUP[repeat],
startDate: startTime,
startTime,
duration,
durationUnit,
monitors: monitor ? [monitor] : [],
});
} catch (error) { } catch (error) {
createToast({ body: "Failed to fetch data" }); createToast({ body: "Failed to fetch data" });
logger.error("Failed to fetch monitors", error); logger.error("Failed to fetch monitors", error);
@@ -177,90 +106,9 @@ const CreateMaintenance = () => {
fetchMonitors(); fetchMonitors();
}, [user]); }, [user]);
const handleSearch = (value) => {
setSearch(value);
};
const handleSelectMonitors = (monitors) => {
setForm({ ...form, monitors });
const { error } = maintenanceWindowValidation.validate(
{ monitors },
{ abortEarly: false }
);
setErrors((prev) => {
return buildErrors(prev, "monitors", error);
});
};
const handleFormChange = (key, value) => {
setForm({ ...form, [key]: value });
const { error } = maintenanceWindowValidation.validate(
{ [key]: value },
{ abortEarly: false }
);
setErrors((prev) => {
return buildErrors(prev, key, error);
});
};
const handleTimeChange = (key, newTime) => {
setForm({ ...form, [key]: newTime });
const { error } = maintenanceWindowValidation.validate(
{ [key]: newTime },
{ abortEarly: false }
);
setErrors((prev) => {
return buildErrors(prev, key, error);
});
};
const handleMonitorsChange = (selected) => {
setForm((prev) => ({ ...prev, monitors: selected }));
const { error } = maintenanceWindowValidation.validate(
{ monitors: selected },
{ abortEarly: false }
);
setErrors((prev) => {
return buildErrors(prev, "monitors", error);
});
};
const handleSubmit = async () => { const handleSubmit = async () => {
if (hasValidationErrors(form, maintenanceWindowValidation, setErrors)) return; if (hasValidationErrors(form, maintenanceWindowValidation, setErrors)) return;
// Build timestamp for maintenance window from startDate and startTime const request = handleSubmitForm(maintenanceWindowId, form);
const start = dayjs(form.startDate)
.set("hour", form.startTime.hour())
.set("minute", form.startTime.minute());
// Build end timestamp for maintenance window
const MS_MULTIPLIER = MS_LOOKUP[form.durationUnit];
const durationInMs = form.duration * MS_MULTIPLIER;
const end = start.add(durationInMs);
// Get repeat value in milliseconds
const repeat = REPEAT_LOOKUP[form.repeat];
const submit = {
monitors: form.monitors.map((monitor) => monitor._id),
name: form.name,
start: start.toISOString(),
end: end.toISOString(),
repeat,
};
if (repeat === 0) {
submit.expiry = end;
}
const requestConfig = { maintenanceWindow: submit };
if (maintenanceWindowId !== undefined) {
requestConfig.maintenanceWindowId = maintenanceWindowId;
}
const request =
maintenanceWindowId === undefined
? networkService.createMaintenanceWindow(requestConfig)
: networkService.editMaintenanceWindow(requestConfig);
try { try {
setIsLoading(true); setIsLoading(true);
await request; await request;
@@ -353,18 +201,13 @@ const CreateMaintenance = () => {
error={errors["name"] ? true : false} error={errors["name"] ? true : false}
helperText={errors["name"]} helperText={errors["name"]}
/> />
<Select <ConfigSelect
id="repeat" id="repeat"
name="maintenance-repeat" name="maintenance-repeat"
label={t("maintenanceRepeat")} label={t("maintenanceRepeat")}
value={getIdByValue(repeatConfig, form.repeat)} valueSelect={form.repeat}
onChange={(event) => { configSelection={repeatConfig}
handleFormChange( onChange={(value) => handleFormChange("repeat", value)}
"repeat",
getValueById(repeatConfig, event.target.value)
);
}}
items={repeatConfig}
/> />
<Stack> <Stack>
<LocalizationProvider dateAdapter={AdapterDayjs}> <LocalizationProvider dateAdapter={AdapterDayjs}>
@@ -437,7 +280,7 @@ const CreateMaintenance = () => {
}} }}
sx={{}} sx={{}}
onChange={(newDate) => { onChange={(newDate) => {
handleTimeChange("startDate", newDate); handleFormChange("startDate", newDate);
}} }}
error={errors["startDate"]} error={errors["startDate"]}
/> />
@@ -453,7 +296,7 @@ const CreateMaintenance = () => {
> >
{t("startTime")} {t("startTime")}
</Typography> </Typography>
<Typography>{t("timeZoneInfo")}</Typography> <Typography> {t("timeZoneInfo")} </Typography>
</Box> </Box>
<Stack gap={theme.spacing(15)}> <Stack gap={theme.spacing(15)}>
<LocalizationProvider dateAdapter={AdapterDayjs}> <LocalizationProvider dateAdapter={AdapterDayjs}>
@@ -462,7 +305,7 @@ const CreateMaintenance = () => {
label={t("startTime")} label={t("startTime")}
value={form.startTime} value={form.startTime}
onChange={(newTime) => { onChange={(newTime) => {
handleTimeChange("startTime", newTime); handleFormChange("startTime", newTime);
}} }}
slotProps={{ slotProps={{
nextIconButton: { sx: { ml: theme.spacing(2) } }, nextIconButton: { sx: { ml: theme.spacing(2) } },
@@ -508,16 +351,11 @@ const CreateMaintenance = () => {
error={errors["duration"] ? true : false} error={errors["duration"] ? true : false}
helperText={errors["duration"]} helperText={errors["duration"]}
/> />
<Select <ConfigSelect
id="durationUnit" id="durationUnit"
value={getIdByValue(durationConfig, form.durationUnit)} valueSelect={form.durationUnit}
items={durationConfig} configSelection={durationConfig}
onChange={(event) => { onChange={(value) => handleFormChange("durationUnit", value)}
handleFormChange(
"durationUnit",
getValueById(durationConfig, event.target.value)
);
}}
error={errors["durationUnit"]} error={errors["durationUnit"]}
/> />
</Stack> </Stack>
@@ -545,7 +383,9 @@ const CreateMaintenance = () => {
inputValue={search} inputValue={search}
value={form.monitors} value={form.monitors}
handleInputChange={setSearch} handleInputChange={setSearch}
handleChange={handleMonitorsChange} handleChange={(selected) => {
handleFormChange("monitors", selected);
}}
error={errors["monitors"]} error={errors["monitors"]}
/> />
<MonitorList <MonitorList
@@ -89,8 +89,6 @@ const CreateNotifications = () => {
error.details.forEach((err) => { error.details.forEach((err) => {
newErrors[err.path[0]] = err.message; newErrors[err.path[0]] = err.message;
}); });
console.log(JSON.stringify(newErrors));
console.log(JSON.stringify(form, null, 2));
createToast({ body: Object.values(newErrors)[0] }); createToast({ body: Object.values(newErrors)[0] });
setErrors(newErrors); setErrors(newErrors);
return; return;
+4 -4
View File
@@ -9,7 +9,7 @@ export const NOTIFICATION_TYPES = [
export const TITLE_MAP = { export const TITLE_MAP = {
email: "createNotifications.emailSettings.title", email: "createNotifications.emailSettings.title",
slack: "createNotifications.slackSettings.title", slack: "createNotifications.slackSettings.title",
pager_duty: "createNotifications.pagerDutySettings.title", pager_duty: "createNotifications.pagerdutySettings.title",
webhook: "createNotifications.webhookSettings.title", webhook: "createNotifications.webhookSettings.title",
discord: "createNotifications.discordSettings.title", discord: "createNotifications.discordSettings.title",
}; };
@@ -17,7 +17,7 @@ export const TITLE_MAP = {
export const DESCRIPTION_MAP = { export const DESCRIPTION_MAP = {
email: "createNotifications.emailSettings.description", email: "createNotifications.emailSettings.description",
slack: "createNotifications.slackSettings.description", slack: "createNotifications.slackSettings.description",
pager_duty: "createNotifications.pagerDutySettings.description", pager_duty: "createNotifications.pagerdutySettings.description",
webhook: "createNotifications.webhookSettings.description", webhook: "createNotifications.webhookSettings.description",
discord: "createNotifications.discordSettings.description", discord: "createNotifications.discordSettings.description",
}; };
@@ -25,7 +25,7 @@ export const DESCRIPTION_MAP = {
export const LABEL_MAP = { export const LABEL_MAP = {
email: "createNotifications.emailSettings.emailLabel", email: "createNotifications.emailSettings.emailLabel",
slack: "createNotifications.slackSettings.webhookLabel", slack: "createNotifications.slackSettings.webhookLabel",
pager_duty: "createNotifications.pagerDutySettings.integrationKeyLabel", pager_duty: "createNotifications.pagerdutySettings.integrationKeyLabel",
webhook: "createNotifications.webhookSettings.webhookLabel", webhook: "createNotifications.webhookSettings.webhookLabel",
discord: "createNotifications.discordSettings.webhookLabel", discord: "createNotifications.discordSettings.webhookLabel",
}; };
@@ -33,7 +33,7 @@ export const LABEL_MAP = {
export const PLACEHOLDER_MAP = { export const PLACEHOLDER_MAP = {
email: "createNotifications.emailSettings.emailPlaceholder", email: "createNotifications.emailSettings.emailPlaceholder",
slack: "createNotifications.slackSettings.webhookPlaceholder", slack: "createNotifications.slackSettings.webhookPlaceholder",
pager_duty: "createNotifications.pagerDutySettings.integrationKeyPlaceholder", pager_duty: "createNotifications.pagerdutySettings.integrationKeyPlaceholder",
webhook: "createNotifications.webhookSettings.webhookPlaceholder", webhook: "createNotifications.webhookSettings.webhookPlaceholder",
discord: "createNotifications.discordSettings.webhookPlaceholder", discord: "createNotifications.discordSettings.webhookPlaceholder",
}; };
+1 -1
View File
@@ -152,7 +152,7 @@ const PageSpeedSetup = () => {
}); });
const { error } = monitorValidation.validate( const { error } = monitorValidation.validate(
{ [name]: value }, { [name]: value, type: monitor.type },
{ abortEarly: false } { abortEarly: false }
); );
setErrors((prev) => ({ setErrors((prev) => ({
@@ -29,7 +29,7 @@ const PieChartLegend = ({ audits }) => {
: theme.palette.tertiary.main; : theme.palette.tertiary.main;
// Find the position where the number ends and the unit begins // 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 value;
let unit; let unit;
if (match) { if (match) {
+41 -15
View File
@@ -44,20 +44,6 @@ import {
useFetchMonitorGames, useFetchMonitorGames,
} from "../../../Hooks/monitorHooks"; } from "../../../Hooks/monitorHooks";
/**
* Parses a URL string and returns a URL object.
*
* @param {string} url - The URL string to parse.
* @returns {URL} - The parsed URL object if valid, otherwise an empty string.
*/
const parseUrl = (url) => {
try {
return new URL(url);
} catch (error) {
return null;
}
};
/** /**
* Create page renders monitor creation or configuration views. * Create page renders monitor creation or configuration views.
* @component * @component
@@ -69,6 +55,8 @@ const UptimeCreate = ({ isClone = false }) => {
// States // States
const [monitor, setMonitor] = useState({ const [monitor, setMonitor] = useState({
type: "http", type: "http",
statusWindowSize: 5,
statusWindowThreshold: 60,
matchMethod: "equal", matchMethod: "equal",
expectedValue: "", expectedValue: "",
jsonPath: "", jsonPath: "",
@@ -191,7 +179,10 @@ const UptimeCreate = ({ isClone = false }) => {
? `http${https ? "s" : ""}://` + monitor.url ? `http${https ? "s" : ""}://` + monitor.url
: monitor.url, : monitor.url,
name: monitor.name || monitor.url.substring(0, 50), name: monitor.name || monitor.url.substring(0, 50),
statusWindowSize: monitor.statusWindowSize,
statusWindowThreshold: monitor.statusWindowThreshold,
type: monitor.type, type: monitor.type,
port: port:
monitor.type === "port" || monitor.type === "game" ? monitor.port : undefined, monitor.type === "port" || monitor.type === "game" ? monitor.port : undefined,
interval: monitor.interval, interval: monitor.interval,
@@ -206,6 +197,8 @@ const UptimeCreate = ({ isClone = false }) => {
_id: monitor._id, _id: monitor._id,
url: monitor.url, url: monitor.url,
name: monitor.name || monitor.url.substring(0, 50), name: monitor.name || monitor.url.substring(0, 50),
statusWindowSize: monitor.statusWindowSize,
statusWindowThreshold: monitor.statusWindowThreshold,
type: monitor.type, type: monitor.type,
matchMethod: monitor.matchMethod, matchMethod: monitor.matchMethod,
expectedValue: monitor.expectedValue, expectedValue: monitor.expectedValue,
@@ -299,7 +292,7 @@ const UptimeCreate = ({ isClone = false }) => {
const isBusy = isLoading || isCreating || isDeleting || isUpdating || isPausing; const isBusy = isLoading || isCreating || isDeleting || isUpdating || isPausing;
const displayInterval = monitor?.interval / MS_PER_MINUTE || 1; const displayInterval = monitor?.interval / MS_PER_MINUTE || 1;
const parsedUrl = parseUrl(monitor?.url); const parsedUrl = monitor?.url;
const protocol = parsedUrl?.protocol?.replace(":", "") || ""; const protocol = parsedUrl?.protocol?.replace(":", "") || "";
useEffect(() => { useEffect(() => {
@@ -616,6 +609,39 @@ const UptimeCreate = ({ isClone = false }) => {
/> />
</Stack> </Stack>
</ConfigBox> </ConfigBox>
<ConfigBox>
<Box>
<Typography
component="h2"
variant="h2"
>
{t("createMonitorPage.incidentConfigTitle")}
</Typography>
<Typography component="p">
{t("createMonitorPage.incidentConfigDescription")}
</Typography>
</Box>
<Stack gap={theme.spacing(20)}>
<TextInput
name="statusWindowSize"
label={t("createMonitorPage.incidentConfigStatusWindowLabel")}
type="number"
value={monitor.statusWindowSize}
onChange={onChange}
error={errors["statusWindowSize"] ? true : false}
helperText={errors["statusWindowSize"]}
/>
<TextInput
name="statusWindowThreshold"
label={t("createMonitorPage.incidentConfigStatusWindowThresholdLabel")}
type="number"
value={monitor.statusWindowThreshold}
onChange={onChange}
error={errors["statusWindowThreshold"] ? true : false}
helperText={errors["statusWindowThreshold"]}
/>
</Stack>
</ConfigBox>
<ConfigBox> <ConfigBox>
<Box> <Box>
<Typography <Typography
+4
View File
@@ -564,6 +564,10 @@ const baseTheme = (palette) => ({
"& .MuiSelect-icon": { "& .MuiSelect-icon": {
color: theme.palette.primary.contrastTextSecondary, // Dropdown + color color: theme.palette.primary.contrastTextSecondary, // Dropdown + color
}, },
"& .MuiSelect-select": {
display: "flex",
alignItems: "center",
},
"&:hover": { "&:hover": {
backgroundColor: theme.palette.primary.main, // Background on hover backgroundColor: theme.palette.primary.main, // Background on hover
}, },
+45 -15
View File
@@ -115,6 +115,16 @@ const monitorValidation = joi.object({
_id: joi.string(), _id: joi.string(),
userId: joi.string(), userId: joi.string(),
teamId: joi.string(), teamId: joi.string(),
statusWindowSize: joi.number().min(1).max(20).default(5).messages({
"number.base": "Status window size must be a number.",
"number.min": "Status window size must be at least 1.",
"number.max": "Status window size must be at most 20.",
}),
statusWindowThreshold: joi.number().min(1).max(100).default(60).messages({
"number.base": "Incident percentage must be a number.",
"number.min": "Incident percentage must be at least 1.",
"number.max": "Incident percentage must be at most 100.",
}),
url: joi.when("type", { url: joi.when("type", {
is: "docker", is: "docker",
then: joi then: joi
@@ -150,6 +160,11 @@ const monitorValidation = joi.object({
// can be replaced by a shortest alternative // can be replaced by a shortest alternative
// (?![-_])(?:[-\\w\\u00a1-\\uffff]{0,63}[^-_]\\.)+ // (?![-_])(?:[-\\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]" +
"[a-z0-9\\u00a1-\\uffff_-]{0,62}" + "[a-z0-9\\u00a1-\\uffff_-]{0,62}" +
@@ -159,6 +174,7 @@ const monitorValidation = joi.object({
// TLD identifier name, may end with dot // TLD identifier name, may end with dot
"(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" + "(?:[a-z\\u00a1-\\uffff]{2,}\\.?)" +
")" + ")" +
")" +
// port number (optional) // port number (optional)
"(?::\\d{2,5})?" + "(?::\\d{2,5})?" +
// resource path (optional) // resource path (optional)
@@ -439,21 +455,35 @@ const notificationValidation = joi.object({
}), }),
address: joi.when("type", { address: joi.when("type", {
is: "email", switch: [
then: joi {
.string() is: "email",
.email({ tlds: { allow: false } }) then: joi
.required() .string()
.messages({ .email({ tlds: { allow: false } })
"string.empty": "E-mail address cannot be empty", .required()
"any.required": "E-mail address is required", .messages({
"string.email": "Please enter a valid e-mail address", "string.empty": "E-mail address cannot be empty",
}), "any.required": "E-mail address is required",
otherwise: joi.string().uri().required().messages({ "string.email": "Please enter a valid e-mail address",
"string.empty": "Webhook URL cannot be empty", }),
"any.required": "Webhook URL is required", },
"string.uri": "Please enter a valid Webhook URL", {
}), 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",
}),
},
],
}), }),
}); });
+55 -49
View File
@@ -1,18 +1,4 @@
{ {
"ClickUpload": "Click to upload",
"DeleteAccountButton": "Remove account",
"DeleteAccountTitle": "Remove account",
"DeleteAccountWarning": "Removing your account means you won't be able to sign in again and all your data will be removed. This isn't reversible.",
"DeleteDescriptionText": "This will remove the account and all associated data from the server. This isn't reversible.",
"DeleteWarningTitle": "Really remove this account?",
"DragandDrop": "drag and drop",
"EmailDescriptionText": "This is your current email address — it cannot be changed.",
"FirstName": "First name",
"LastName": "Last name",
"MaxSize": "Maximum Size",
"PhotoDescriptionText": "This photo will be displayed in your profile page.",
"SupportedFormats": "Supported formats",
"YourPhoto": "Profile photo",
"aboutus": "About Us", "aboutus": "About Us",
"access": "Access", "access": "Access",
"actions": "Actions", "actions": "Actions",
@@ -202,13 +188,6 @@
"avgCpuTemperature": "Average CPU Temperature", "avgCpuTemperature": "Average CPU Temperature",
"bar": "Bar", "bar": "Bar",
"basicInformation": "Basic Information", "basicInformation": "Basic Information",
"bytesPerSecond": "Bytes per second",
"bytesReceived": "Bytes Received",
"bytesSent": "Bytes Sent",
"dataReceived": "Data Received",
"dataSent": "Data Sent",
"dataRate": "Data Rate",
"rate": "Rate",
"bulkImport": { "bulkImport": {
"fallbackPage": "Import a file to upload a list of servers in bulk", "fallbackPage": "Import a file to upload a list of servers in bulk",
"invalidFileType": "Invalid file type", "invalidFileType": "Invalid file type",
@@ -222,17 +201,21 @@
"uploadSuccess": "Monitors created successfully!", "uploadSuccess": "Monitors created successfully!",
"validationFailed": "Validation failed" "validationFailed": "Validation failed"
}, },
"bytesPerSecond": "Bytes per second",
"bytesReceived": "Bytes Received",
"bytesSent": "Bytes Sent",
"cancel": "Cancel", "cancel": "Cancel",
"checkFormError": "Please check the form for errors.", "checkFormError": "Please check the form for errors.",
"checkFrequency": "Check frequency", "checkFrequency": "Check frequency",
"chooseGame": "Choose game",
"checkHooks": { "checkHooks": {
"failureResolveAll": "Failed to resolve all incidents.", "failureResolveAll": "Failed to resolve all incidents.",
"failureResolveMonitor": "Failed to resolve monitor incidents.", "failureResolveMonitor": "Failed to resolve monitor incidents.",
"failureResolveOne": "Failed to resolve incident." "failureResolveOne": "Failed to resolve incident."
}, },
"checkingEvery": "Checking every", "checkingEvery": "Checking every",
"chooseGame": "Choose game",
"city": "CITY", "city": "CITY",
"ClickUpload": "Click to upload",
"common": { "common": {
"appName": "Checkmate", "appName": "Checkmate",
"buttons": { "buttons": {
@@ -262,6 +245,12 @@
"createMaintenance": "Create maintenance", "createMaintenance": "Create maintenance",
"createMaintenanceWindow": "Create maintenance window", "createMaintenanceWindow": "Create maintenance window",
"createMonitor": "Create monitor", "createMonitor": "Create monitor",
"createMonitorPage": {
"incidentConfigDescription": "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.",
"incidentConfigStatusWindowLabel": "How many checks should be in the sliding window?",
"incidentConfigStatusWindowThresholdLabel": "What percentage of checks in the sliding window fail/succeed before monitor status changes?",
"incidentConfigTitle": "Incidents"
},
"createNew": "Create new", "createNew": "Create new",
"createNotifications": { "createNotifications": {
"dialogDeleteConfirm": "Delete", "dialogDeleteConfirm": "Delete",
@@ -311,13 +300,22 @@
} }
}, },
"createYour": "Create your", "createYour": "Create your",
"dataRate": "Data Rate",
"dataReceived": "Data Received",
"dataSent": "Data Sent",
"date&Time": "Date & Time", "date&Time": "Date & Time",
"delete": "Delete", "delete": "Delete",
"DeleteAccountButton": "Remove account",
"DeleteAccountTitle": "Remove account",
"DeleteAccountWarning": "Removing your account means you won't be able to sign in again and all your data will be removed. This isn't reversible.",
"DeleteDescriptionText": "This will remove the account and all associated data from the server. This isn't reversible.",
"deleteDialogDescription": "Once deleted, this monitor cannot be retrieved.", "deleteDialogDescription": "Once deleted, this monitor cannot be retrieved.",
"deleteDialogTitle": "Do you really want to delete this monitor?", "deleteDialogTitle": "Do you really want to delete this monitor?",
"deleteStatusPage": "Do you want to delete this status page?", "deleteStatusPage": "Do you want to delete this status page?",
"deleteStatusPageConfirm": "Yes, delete status page", "deleteStatusPageConfirm": "Yes, delete status page",
"deleteStatusPageDescription": "Once deleted, your status page cannot be retrieved.", "deleteStatusPageDescription": "Once deleted, your status page cannot be retrieved.",
"DeleteWarningTitle": "Really remove this account?",
"details": "Details",
"diagnosticsPage": { "diagnosticsPage": {
"diagnosticDescription": "System diagnostics", "diagnosticDescription": "System diagnostics",
"gauges": { "gauges": {
@@ -341,15 +339,6 @@
}, },
"disk": "Disk", "disk": "Disk",
"diskUsage": "Disk Usage", "diskUsage": "Disk Usage",
"drops": "Drops",
"errors": "Errors",
"errorsIn": "Errors In",
"errorsOut": "Errors Out",
"networkErrors": "Network Errors",
"networkDrops": "Network Drops",
"networkInterface": "Network Interface",
"selectInterface": "Select Interface",
"details": "Details",
"displayName": "Display name", "displayName": "Display name",
"distributedRightCategoryTitle": "Monitor", "distributedRightCategoryTitle": "Monitor",
"distributedStatusHeaderText": "Real-time, real-device coverage", "distributedStatusHeaderText": "Real-time, real-device coverage",
@@ -395,6 +384,8 @@
"distributedUptimeStatusUptLogo": "Upt Logo", "distributedUptimeStatusUptLogo": "Upt Logo",
"dockerContainerMonitoring": "Docker container monitoring", "dockerContainerMonitoring": "Docker container monitoring",
"dockerContainerMonitoringDescription": "Check whether your Docker container is running or not.", "dockerContainerMonitoringDescription": "Check whether your Docker container is running or not.",
"DragandDrop": "drag and drop",
"drops": "Drops",
"duration": "Duration", "duration": "Duration",
"edit": "Edit", "edit": "Edit",
"editing": "Editing...", "editing": "Editing...",
@@ -417,6 +408,7 @@
"validationErrors": "Validation errors" "validationErrors": "Validation errors"
} }
}, },
"EmailDescriptionText": "This is your current email address — it cannot be changed.",
"emailSent": "Email sent successfully", "emailSent": "Email sent successfully",
"errorInvalidFieldId": "Invalid field ID provided", "errorInvalidFieldId": "Invalid field ID provided",
"errorInvalidTypeId": "Invalid notification type provided", "errorInvalidTypeId": "Invalid notification type provided",
@@ -434,6 +426,9 @@
} }
} }
}, },
"errors": "Errors",
"errorsIn": "Errors In",
"errorsOut": "Errors Out",
"expectedValue": "Expected value", "expectedValue": "Expected value",
"export": { "export": {
"failed": "Failed to export monitors", "failed": "Failed to export monitors",
@@ -442,9 +437,12 @@
}, },
"failedToSendEmail": "Failed to send email", "failedToSendEmail": "Failed to send email",
"features": "Features", "features": "Features",
"FirstName": "First name",
"frequency": "Frequency", "frequency": "Frequency",
"friendlyNameInput": "Friendly name", "friendlyNameInput": "Friendly name",
"friendlyNamePlaceholder": "Maintenance at __ : __ for ___ minutes", "friendlyNamePlaceholder": "Maintenance at __ : __ for ___ minutes",
"gameServerMonitoring": "Game Server Monitoring",
"gameServerMonitoringDescription": "Check whether your game server is running or not",
"gb": "GB", "gb": "GB",
"general": { "general": {
"noOptionsFound": "No {{unit}} found" "noOptionsFound": "No {{unit}} found"
@@ -520,6 +518,7 @@
"invalidFileFormat": "Unsupported file format!", "invalidFileFormat": "Unsupported file format!",
"invalidFileSize": "File size is too large!", "invalidFileSize": "File size is too large!",
"inviteNoTokenFound": "No invite token found", "inviteNoTokenFound": "No invite token found",
"LastName": "Last name",
"loginHere": "Login here", "loginHere": "Login here",
"logsPage": { "logsPage": {
"description": "System logs - last 1000 lines", "description": "System logs - last 1000 lines",
@@ -571,6 +570,7 @@
"regexPlaceholder": "^(success|ok)$", "regexPlaceholder": "^(success|ok)$",
"text": "Match Method" "text": "Match Method"
}, },
"MaxSize": "Maximum Size",
"mb": "MB", "mb": "MB",
"mem": "Mem", "mem": "Mem",
"memory": "Memory", "memory": "Memory",
@@ -632,6 +632,11 @@
"namePlaceholder": "My Container", "namePlaceholder": "My Container",
"placeholder": "abcd1234" "placeholder": "abcd1234"
}, },
"game": {
"label": "URL to monitor",
"namePlaceholder": "localhost:5173",
"placeholder": "localhost"
},
"http": { "http": {
"label": "URL to monitor", "label": "URL to monitor",
"namePlaceholder": "Google", "namePlaceholder": "Google",
@@ -646,16 +651,14 @@
"label": "URL to monitor", "label": "URL to monitor",
"namePlaceholder": "Localhost:5173", "namePlaceholder": "Localhost:5173",
"placeholder": "localhost" "placeholder": "localhost"
},
"game": {
"label": "URL to monitor",
"namePlaceholder": "localhost:5173",
"placeholder": "localhost"
} }
}, },
"ms": "ms", "ms": "ms",
"navControls": "Controls", "navControls": "Controls",
"network": "Network", "network": "Network",
"networkDrops": "Network Drops",
"networkErrors": "Network Errors",
"networkInterface": "Network Interface",
"nextWindow": "Next window", "nextWindow": "Next window",
"noNetworkStatsAvailable": "No network stats available.", "noNetworkStatsAvailable": "No network stats available.",
"notFoundButton": "Go to the main dashboard", "notFoundButton": "Go to the main dashboard",
@@ -739,6 +742,10 @@
"notifySMS": "Notify via SMS (coming soon)", "notifySMS": "Notify via SMS (coming soon)",
"now": "Now", "now": "Now",
"os": "OS", "os": "OS",
"packetsPerSecond": "Packets per second",
"packetsReceived": "Packets Received",
"packetsReceivedRate": "Packets Received Rate",
"packetsSent": "Packets Sent",
"pageSpeed": { "pageSpeed": {
"fallback": { "fallback": {
"actionButton": "Let's create your first PageSpeed monitor!", "actionButton": "Let's create your first PageSpeed monitor!",
@@ -769,17 +776,12 @@
"passwordRequirements": "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character.", "passwordRequirements": "New password must contain at least 8 characters and must have at least one uppercase letter, one lowercase letter, one number and one special character.",
"saving": "Saving..." "saving": "Saving..."
}, },
"packetsPerSecond": "Packets per second",
"packetsReceived": "Packets Received",
"packetsReceivedRate": "Packets Received Rate",
"packetsSent": "Packets Sent",
"pause": "Pause", "pause": "Pause",
"PhotoDescriptionText": "This photo will be displayed in your profile page.",
"pingMonitoring": "Ping monitoring", "pingMonitoring": "Ping monitoring",
"pingMonitoringDescription": "Check whether your server is available or not.", "pingMonitoringDescription": "Check whether your server is available or not.",
"portMonitoring": "Port monitoring", "portMonitoring": "Port monitoring",
"portMonitoringDescription": "Check whether your port is open or not.", "portMonitoringDescription": "Check whether your port is open or not.",
"gameServerMonitoring": "Game Server Monitoring",
"gameServerMonitoringDescription": "Check whether your game server is running or not",
"portToMonitor": "Port to monitor", "portToMonitor": "Port to monitor",
"publicLink": "Public link", "publicLink": "Public link",
"publicURL": "Public URL", "publicURL": "Public URL",
@@ -814,6 +816,7 @@
"refreshButton": "Refresh", "refreshButton": "Refresh",
"title": "Queue" "title": "Queue"
}, },
"rate": "Rate",
"remove": "Remove", "remove": "Remove",
"removeLogo": "Remove Logo", "removeLogo": "Remove Logo",
"repeat": "Repeat", "repeat": "Repeat",
@@ -829,6 +832,7 @@
}, },
"save": "Save", "save": "Save",
"selectAll": "Select all", "selectAll": "Select all",
"selectInterface": "Select Interface",
"sendTestNotifications": "Send test notifications", "sendTestNotifications": "Send test notifications",
"seperateEmails": "You can separate multiple emails with a comma", "seperateEmails": "You can separate multiple emails with a comma",
"settingsAppearance": "Appearance", "settingsAppearance": "Appearance",
@@ -873,6 +877,10 @@
"title": "Email", "title": "Email",
"toastEmailRequiredFieldsError": "Email address, host, port and password are required" "toastEmailRequiredFieldsError": "Email address, host, port and password are required"
}, },
"globalThresholds": {
"description": "Configure global CPU, Memory, Disk, and Temperature thresholds. If a value is provided, it will automatically be enabled for monitoring.",
"title": "Global Thresholds"
},
"pageSpeedSettings": { "pageSpeedSettings": {
"description": "Enter your Google PageSpeed API key to enable Google PageSpeed monitoring. Click Reset to update the key.", "description": "Enter your Google PageSpeed API key to enable Google PageSpeed monitoring. Click Reset to update the key.",
"labelApiKey": "PageSpeed API key", "labelApiKey": "PageSpeed API key",
@@ -905,10 +913,6 @@
"title": "Display timezone" "title": "Display timezone"
}, },
"title": "Settings", "title": "Settings",
"globalThresholds": {
"title": "Global Thresholds",
"description": "Configure global CPU, Memory, Disk, and Temperature thresholds. If a value is provided, it will automatically be enabled for monitoring."
},
"uiSettings": { "uiSettings": {
"description": "Switch between light and dark mode, or change user interface language.", "description": "Switch between light and dark mode, or change user interface language.",
"labelLanguage": "Language", "labelLanguage": "Language",
@@ -985,6 +989,7 @@
"statusPageStatusNotPublic": "This status page is not public.", "statusPageStatusNotPublic": "This status page is not public.",
"statusPageStatusServiceStatus": "Service status", "statusPageStatusServiceStatus": "Service status",
"submit": "Submit", "submit": "Submit",
"SupportedFormats": "Supported formats",
"teamPanel": { "teamPanel": {
"cancel": "Cancel", "cancel": "Cancel",
"email": "Email", "email": "Email",
@@ -1040,10 +1045,10 @@
"uptimeCreateJsonPathQuery": "for query language documentation.", "uptimeCreateJsonPathQuery": "for query language documentation.",
"uptimeGeneralInstructions": { "uptimeGeneralInstructions": {
"docker": "Enter the Docker ID of your container. Docker IDs must be the full 64 char Docker ID. You can run docker inspect <short_id> to get the full container ID.", "docker": "Enter the Docker ID of your container. Docker IDs must be the full 64 char Docker ID. You can run docker inspect <short_id> 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.", "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.", "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.", "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."
}, },
"uptimeMonitor": { "uptimeMonitor": {
"fallback": { "fallback": {
@@ -1066,5 +1071,6 @@
"websiteMonitoring": "Website monitoring", "websiteMonitoring": "Website monitoring",
"websiteMonitoringDescription": "Use HTTP(s) to monitor your website or API endpoint.", "websiteMonitoringDescription": "Use HTTP(s) to monitor your website or API endpoint.",
"whenNewIncident": "When there is a new incident,", "whenNewIncident": "When there is a new incident,",
"window": "window" "window": "window",
"YourPhoto": "Profile photo"
} }
+1 -1
View File
@@ -5,7 +5,7 @@ import { execSync } from "child_process";
export default defineConfig(({ mode }) => { export default defineConfig(({ mode }) => {
const env = loadEnv(mode, process.cwd(), ""); const env = loadEnv(mode, process.cwd(), "");
let version = "3.0-beta"; let version = "3.1-beta";
return { return {
base: "/", base: "/",
+1 -1
View File
@@ -5,7 +5,7 @@ cd "$(dirname "$0")"
cd ../.. cd ../..
# Define service names and their corresponding Dockerfiles in parallel arrays # Define service names and their corresponding Dockerfiles in parallel arrays
services=("mono_mongo" "mono_redis" "mono_server") services=("mono_mongo" "mono_server")
dockerfiles=( dockerfiles=(
"./docker/dist-mono/mongoDB.Dockerfile" "./docker/dist-mono/mongoDB.Dockerfile"
"./docker/dist-mono/server.Dockerfile" "./docker/dist-mono/server.Dockerfile"
+155 -35
View File
@@ -21,6 +21,7 @@
"express": "^4.19.2", "express": "^4.19.2",
"express-rate-limit": "8.0.1", "express-rate-limit": "8.0.1",
"gamedig": "^5.3.1", "gamedig": "^5.3.1",
"got": "14.4.7",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"helmet": "^8.0.0", "helmet": "^8.0.0",
"ioredis": "^5.4.2", "ioredis": "^5.4.2",
@@ -1137,6 +1138,12 @@
"hasInstallScript": true, "hasInstallScript": true,
"license": "Apache-2.0" "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": { "node_modules/@sideway/address": {
"version": "4.1.5", "version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
@@ -1159,12 +1166,12 @@
"license": "BSD-3-Clause" "license": "BSD-3-Clause"
}, },
"node_modules/@sindresorhus/is": { "node_modules/@sindresorhus/is": {
"version": "5.6.0", "version": "7.0.2",
"resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.2.tgz",
"integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", "integrity": "sha512-d9xRovfKNz1SKieM0qJdO+PQonjnnIfSNWfHYnBSJ9hkjm0ZPw6HlxscDXYstp3z+7V2GOFHc+J0CYrYTjqCJw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=14.16" "node": ">=18"
}, },
"funding": { "funding": {
"url": "https://github.com/sindresorhus/is?sponsor=1" "url": "https://github.com/sindresorhus/is?sponsor=1"
@@ -1821,21 +1828,21 @@
} }
}, },
"node_modules/cacheable-request": { "node_modules/cacheable-request": {
"version": "10.2.14", "version": "12.0.1",
"resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-12.0.1.tgz",
"integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", "integrity": "sha512-Yo9wGIQUaAfIbk+qY0X4cDQgCosecfBe3V9NSyeY4qPC2SAkbCS4Xj79VP8WOzitpJUZKc/wsRCYF5ariDIwkg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/http-cache-semantics": "^4.0.2", "@types/http-cache-semantics": "^4.0.4",
"get-stream": "^6.0.1", "get-stream": "^9.0.1",
"http-cache-semantics": "^4.1.1", "http-cache-semantics": "^4.1.1",
"keyv": "^4.5.3", "keyv": "^4.5.4",
"mimic-response": "^4.0.0", "mimic-response": "^4.0.0",
"normalize-url": "^8.0.0", "normalize-url": "^8.0.1",
"responselike": "^3.0.0" "responselike": "^3.0.0"
}, },
"engines": { "engines": {
"node": ">=14.16" "node": ">=18"
} }
}, },
"node_modules/call-bind-apply-helpers": { "node_modules/call-bind-apply-helpers": {
@@ -3750,12 +3757,12 @@
} }
}, },
"node_modules/form-data-encoder": { "node_modules/form-data-encoder": {
"version": "2.1.4", "version": "4.1.0",
"resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-4.1.0.tgz",
"integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", "integrity": "sha512-G6NsmEW15s0Uw9XnCg+33H3ViYRyiM0hMrMhhqQOR8NFc5GhYrI+6I3u7OTw7b91J2g8rtvMBZJDbcGb2YUniw==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">= 14.17" "node": ">= 18"
} }
}, },
"node_modules/formdata-polyfill": { "node_modules/formdata-polyfill": {
@@ -3840,6 +3847,82 @@
"node": ">=16.20.0" "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": { "node_modules/gamedig/node_modules/iconv-lite": {
"version": "0.6.3", "version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
@@ -3852,6 +3935,15 @@
"node": ">=0.10.0" "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": { "node_modules/gaxios": {
"version": "6.7.1", "version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
@@ -3930,12 +4022,28 @@
} }
}, },
"node_modules/get-stream": { "node_modules/get-stream": {
"version": "6.0.1", "version": "9.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz",
"integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", "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", "license": "MIT",
"engines": { "engines": {
"node": ">=10" "node": ">=18"
}, },
"funding": { "funding": {
"url": "https://github.com/sponsors/sindresorhus" "url": "https://github.com/sponsors/sindresorhus"
@@ -4024,28 +4132,40 @@
} }
}, },
"node_modules/got": { "node_modules/got": {
"version": "13.0.0", "version": "14.4.7",
"resolved": "https://registry.npmjs.org/got/-/got-13.0.0.tgz", "resolved": "https://registry.npmjs.org/got/-/got-14.4.7.tgz",
"integrity": "sha512-XfBk1CxOOScDcMr9O1yKkNaQyy865NbYs+F7dr4H0LZMVgCj2Le59k6PqbNHoL5ToeaEQUYh6c6yMfVcc6SJxA==", "integrity": "sha512-DI8zV1231tqiGzOiOzQWDhsBmncFW7oQDH6Zgy6pDPrqJuVZMtoSgPLLsBZQj8Jg4JFfwoOsDA8NGtLQLnIx2g==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@sindresorhus/is": "^5.2.0", "@sindresorhus/is": "^7.0.1",
"@szmarczak/http-timer": "^5.0.1", "@szmarczak/http-timer": "^5.0.1",
"cacheable-lookup": "^7.0.0", "cacheable-lookup": "^7.0.0",
"cacheable-request": "^10.2.8", "cacheable-request": "^12.0.1",
"decompress-response": "^6.0.0", "decompress-response": "^6.0.0",
"form-data-encoder": "^2.1.2", "form-data-encoder": "^4.0.2",
"get-stream": "^6.0.1", "http2-wrapper": "^2.2.1",
"http2-wrapper": "^2.1.10",
"lowercase-keys": "^3.0.0", "lowercase-keys": "^3.0.0",
"p-cancelable": "^3.0.0", "p-cancelable": "^4.0.1",
"responselike": "^3.0.0" "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": { "engines": {
"node": ">=16" "node": ">=16"
}, },
"funding": { "funding": {
"url": "https://github.com/sindresorhus/got?sponsor=1" "url": "https://github.com/sponsors/sindresorhus"
} }
}, },
"node_modules/handlebars": { "node_modules/handlebars": {
@@ -6350,12 +6470,12 @@
} }
}, },
"node_modules/p-cancelable": { "node_modules/p-cancelable": {
"version": "3.0.0", "version": "4.0.1",
"resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-4.0.1.tgz",
"integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", "integrity": "sha512-wBowNApzd45EIKdO1LaU+LrMBwAcjfPaYtVzV3lmfM3gf8Z4CHZsiIqlM8TZZ8okYvh5A1cP6gTfCRQtwUpaUg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=12.20" "node": ">=14.16"
} }
}, },
"node_modules/p-limit": { "node_modules/p-limit": {
+1
View File
@@ -28,6 +28,7 @@
"express": "^4.19.2", "express": "^4.19.2",
"express-rate-limit": "8.0.1", "express-rate-limit": "8.0.1",
"gamedig": "^5.3.1", "gamedig": "^5.3.1",
"got": "14.4.7",
"handlebars": "^4.7.8", "handlebars": "^4.7.8",
"helmet": "^8.0.0", "helmet": "^8.0.0",
"ioredis": "^5.4.2", "ioredis": "^5.4.2",
+19 -16
View File
@@ -19,8 +19,10 @@ import MaintenanceWindowService from "../service/business/maintenanceWindowServi
import MonitorService from "../service/business/monitorService.js"; import MonitorService from "../service/business/monitorService.js";
import papaparse from "papaparse"; import papaparse from "papaparse";
import axios from "axios"; import axios from "axios";
import got from "got";
import ping from "ping"; import ping from "ping";
import http from "http"; import http from "http";
import https from "https";
import Docker from "dockerode"; import Docker from "dockerode";
import net from "net"; import net from "net";
import fs from "fs"; import fs from "fs";
@@ -32,6 +34,8 @@ import mjml2html from "mjml";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import crypto from "crypto"; import crypto from "crypto";
import { games } from "gamedig"; import { games } from "gamedig";
import jmespath from "jmespath";
import { GameDig } from "gamedig";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { ObjectId } from "mongodb"; import { ObjectId } from "mongodb";
@@ -43,8 +47,6 @@ import { ParseBoolean } from "../utils/utils.js";
// Models // Models
import Check from "../db/models/Check.js"; 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 Monitor from "../db/models/Monitor.js";
import User from "../db/models/User.js"; import User from "../db/models/User.js";
import InviteToken from "../db/models/InviteToken.js"; import InviteToken from "../db/models/InviteToken.js";
@@ -52,7 +54,6 @@ import StatusPage from "../db/models/StatusPage.js";
import Team from "../db/models/Team.js"; import Team from "../db/models/Team.js";
import MaintenanceWindow from "../db/models/MaintenanceWindow.js"; import MaintenanceWindow from "../db/models/MaintenanceWindow.js";
import MonitorStats from "../db/models/MonitorStats.js"; import MonitorStats from "../db/models/MonitorStats.js";
import NetworkCheck from "../db/models/NetworkCheck.js";
import Notification from "../db/models/Notification.js"; import Notification from "../db/models/Notification.js";
import RecoveryToken from "../db/models/RecoveryToken.js"; import RecoveryToken from "../db/models/RecoveryToken.js";
import AppSettings from "../db/models/AppSettings.js"; import AppSettings from "../db/models/AppSettings.js";
@@ -61,12 +62,9 @@ import InviteModule from "../db/mongo/modules/inviteModule.js";
import CheckModule from "../db/mongo/modules/checkModule.js"; import CheckModule from "../db/mongo/modules/checkModule.js";
import StatusPageModule from "../db/mongo/modules/statusPageModule.js"; import StatusPageModule from "../db/mongo/modules/statusPageModule.js";
import UserModule from "../db/mongo/modules/userModule.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 MaintenanceWindowModule from "../db/mongo/modules/maintenanceWindowModule.js";
import MonitorModule from "../db/mongo/modules/monitorModule.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 NotificationModule from "../db/mongo/modules/notificationModule.js";
import PageSpeedCheckModule from "../db/mongo/modules/pageSpeedCheckModule.js";
import RecoveryModule from "../db/mongo/modules/recoveryModule.js"; import RecoveryModule from "../db/mongo/modules/recoveryModule.js";
import SettingsModule from "../db/mongo/modules/settingsModule.js"; import SettingsModule from "../db/mongo/modules/settingsModule.js";
@@ -80,18 +78,15 @@ export const initializeServices = async ({ logger, envSettings, settingsService
const stringService = new StringService(translationService); const stringService = new StringService(translationService);
// Create DB // 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 inviteModule = new InviteModule({ InviteToken, crypto, stringService });
const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService }); const statusPageModule = new StatusPageModule({ StatusPage, NormalizeData, stringService });
const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService }); const userModule = new UserModule({ User, Team, GenerateAvatarImage, ParseBoolean, stringService });
const hardwareCheckModule = new HardwareCheckModule({ HardwareCheck, Monitor, logger });
const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow }); const maintenanceWindowModule = new MaintenanceWindowModule({ MaintenanceWindow });
const monitorModule = new MonitorModule({ const monitorModule = new MonitorModule({
Monitor, Monitor,
MonitorStats, MonitorStats,
Check, Check,
PageSpeedCheck,
HardwareCheck,
stringService, stringService,
fs, fs,
path, path,
@@ -100,9 +95,7 @@ export const initializeServices = async ({ logger, envSettings, settingsService
NormalizeData, NormalizeData,
NormalizeDataUptimeDetails, NormalizeDataUptimeDetails,
}); });
const networkCheckModule = new NetworkCheckModule({ NetworkCheck });
const notificationModule = new NotificationModule({ Notification, Monitor }); const notificationModule = new NotificationModule({ Notification, Monitor });
const pageSpeedCheckModule = new PageSpeedCheckModule({ PageSpeedCheck });
const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService }); const recoveryModule = new RecoveryModule({ User, RecoveryToken, crypto, stringService });
const settingsModule = new SettingsModule({ AppSettings }); const settingsModule = new SettingsModule({ AppSettings });
@@ -113,19 +106,29 @@ export const initializeServices = async ({ logger, envSettings, settingsService
inviteModule, inviteModule,
statusPageModule, statusPageModule,
userModule, userModule,
hardwareCheckModule,
maintenanceWindowModule, maintenanceWindowModule,
monitorModule, monitorModule,
networkCheckModule,
notificationModule, notificationModule,
pageSpeedCheckModule,
recoveryModule, recoveryModule,
settingsModule, settingsModule,
}); });
await db.connect(); 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 emailService = new EmailService(settingsService, fs, path, compile, mjml2html, nodemailer, logger);
const bufferService = new BufferService({ db, logger, envSettings }); const bufferService = new BufferService({ db, logger, envSettings });
const statusService = new StatusService({ db, logger, buffer: bufferService }); const statusService = new StatusService({ db, logger, buffer: bufferService });
+166 -89
View File
@@ -1,99 +1,176 @@
import mongoose from "mongoose"; import mongoose from "mongoose";
const BaseCheckSchema = mongoose.Schema({ const cpuSchema = mongoose.Schema({
/** physical_core: { type: Number, default: 0 },
* Reference to the associated Monitor document. logical_core: { type: Number, default: 0 },
* frequency: { type: Number, default: 0 },
* @type {mongoose.Schema.Types.ObjectId} temperature: { type: [Number], default: [] },
*/ free_percent: { type: Number, default: 0 },
monitorId: { usage_percent: { type: Number, default: 0 },
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,
},
/**
* 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 memorySchema = mongoose.Schema({
* Check Schema for MongoDB collection. total_bytes: { type: Number, default: 0 },
* available_bytes: { type: Number, default: 0 },
* Represents a check associated with a monitor, storing information used_bytes: { type: Number, default: 0 },
* about the status and response of a particular check event. usage_percent: { type: Number, default: 0 },
*/ });
const CheckSchema = mongoose.Schema({ ...BaseCheckSchema.obj }, { timestamps: true });
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({ updatedAt: 1 });
CheckSchema.index({ monitorId: 1, updatedAt: 1 }); CheckSchema.index({ monitorId: 1, updatedAt: 1 });
CheckSchema.index({ monitorId: 1, updatedAt: -1 }); CheckSchema.index({ monitorId: 1, updatedAt: -1 });
CheckSchema.index({ teamId: 1, updatedAt: -1 }); CheckSchema.index({ teamId: 1, updatedAt: -1 });
export default mongoose.model("Check", CheckSchema); export default mongoose.model("Check", CheckSchema);
export { BaseCheckSchema };
-100
View File
@@ -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);
+14 -16
View File
@@ -1,6 +1,4 @@
import mongoose from "mongoose"; import mongoose from "mongoose";
import HardwareCheck from "./HardwareCheck.js";
import PageSpeedCheck from "./PageSpeedCheck.js";
import Check from "./Check.js"; import Check from "./Check.js";
import MonitorStats from "./MonitorStats.js"; import MonitorStats from "./MonitorStats.js";
import StatusPage from "./StatusPage.js"; import StatusPage from "./StatusPage.js";
@@ -30,6 +28,18 @@ const MonitorSchema = mongoose.Schema(
type: Boolean, type: Boolean,
default: undefined, default: undefined,
}, },
statusWindow: {
type: [Boolean],
default: [],
},
statusWindowSize: {
type: Number,
default: 5,
},
statusWindowThreshold: {
type: Number,
default: 0.6,
},
type: { type: {
type: String, type: String,
required: true, required: true,
@@ -133,13 +143,7 @@ MonitorSchema.pre("findOneAndDelete", async function (next) {
throw new Error("Monitor not found"); throw new Error("Monitor not found");
} }
if (doc?.type === "pagespeed") { await Check.deleteMany({ monitorId: doc._id });
await PageSpeedCheck.deleteMany({ monitorId: doc._id });
} else if (doc?.type === "hardware") {
await HardwareCheck.deleteMany({ monitorId: doc._id });
} else {
await Check.deleteMany({ monitorId: doc._id });
}
// Deal with status pages // Deal with status pages
await StatusPage.updateMany({ monitors: doc?._id }, { $pull: { monitors: doc?._id } }); await StatusPage.updateMany({ monitors: doc?._id }, { $pull: { monitors: doc?._id } });
@@ -156,13 +160,7 @@ MonitorSchema.pre("deleteMany", async function (next) {
const monitors = await this.model.find(filter).select(["_id", "type"]).lean(); const monitors = await this.model.find(filter).select(["_id", "type"]).lean();
for (const monitor of monitors) { for (const monitor of monitors) {
if (monitor.type === "pagespeed") { await Check.deleteMany({ monitorId: monitor._id });
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 StatusPage.updateMany({ monitors: monitor._id }, { $pull: { monitors: monitor._id } }); await StatusPage.updateMany({ monitors: monitor._id }, { $pull: { monitors: monitor._id } });
await MonitorStats.deleteMany({ monitorId: monitor._id.toString() }); await MonitorStats.deleteMany({ monitorId: monitor._id.toString() });
} }
-4
View File
@@ -40,10 +40,6 @@ const MonitorStatsSchema = new mongoose.Schema(
type: Number, type: Number,
default: 0, default: 0,
}, },
uptBurnt: {
type: mongoose.Schema.Types.Decimal128,
required: false,
},
}, },
{ timestamps: true } { timestamps: true }
); );
-46
View File
@@ -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);
-83
View File
@@ -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<PageSpeedCheck>} LighthouseMetricsModel
*/
PageSpeedCheck.index({ createdAt: 1 });
PageSpeedCheck.index({ monitorId: 1, createdAt: 1 });
PageSpeedCheck.index({ monitorId: 1, createdAt: -1 });
export default mongoose.model("PageSpeedCheck", PageSpeedCheck);
+2 -26
View File
@@ -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 { try {
status = status === "true" ? true : status === "false" ? false : undefined; status = status === "true" ? true : status === "false" ? false : undefined;
page = parseInt(page); page = parseInt(page);
@@ -79,19 +79,7 @@ class CheckModule {
skip = page * rowsPerPage; skip = page * rowsPerPage;
} }
const checkModels = { const checks = await this.Check.aggregate([
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([
{ $match: matchStage }, { $match: matchStage },
{ $sort: { createdAt: sortOrder } }, { $sort: { createdAt: sortOrder } },
{ {
@@ -166,18 +154,6 @@ class CheckModule {
const aggregatePipeline = [ const aggregatePipeline = [
{ $match: matchStage }, { $match: matchStage },
{
$unionWith: {
coll: "hardwarechecks",
pipeline: [{ $match: matchStage }],
},
},
{
$unionWith: {
coll: "pagespeedchecks",
pipeline: [{ $match: matchStage }],
},
},
{ $sort: { createdAt: sortOrder } }, { $sort: { createdAt: sortOrder } },
{ {
@@ -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;
+10 -36
View File
@@ -11,25 +11,10 @@ import {
const SERVICE_NAME = "monitorModule"; const SERVICE_NAME = "monitorModule";
class MonitorModule { class MonitorModule {
constructor({ constructor({ Monitor, MonitorStats, Check, stringService, fs, path, fileURLToPath, ObjectId, NormalizeData, NormalizeDataUptimeDetails }) {
Monitor,
MonitorStats,
Check,
PageSpeedCheck,
HardwareCheck,
stringService,
fs,
path,
fileURLToPath,
ObjectId,
NormalizeData,
NormalizeDataUptimeDetails,
}) {
this.Monitor = Monitor; this.Monitor = Monitor;
this.MonitorStats = MonitorStats; this.MonitorStats = MonitorStats;
this.Check = Check; this.Check = Check;
this.PageSpeedCheck = PageSpeedCheck;
this.HardwareCheck = HardwareCheck;
this.stringService = stringService; this.stringService = stringService;
this.fs = fs; this.fs = fs;
this.path = path; this.path = path;
@@ -38,15 +23,6 @@ class MonitorModule {
this.NormalizeData = NormalizeData; this.NormalizeData = NormalizeData;
this.NormalizeDataUptimeDetails = NormalizeDataUptimeDetails; 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 __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@@ -146,19 +122,18 @@ class MonitorModule {
}; };
//Helper //Helper
getMonitorChecks = async (monitorId, model, dateRange, sortOrder) => { getMonitorChecks = async (monitorId, dateRange, sortOrder) => {
const indexSpec = { const indexSpec = {
monitorId: 1, monitorId: 1,
createdAt: sortOrder, // This will be 1 or -1 updatedAt: sortOrder, // This will be 1 or -1
}; };
const [checksAll, checksForDateRange] = await Promise.all([ const [checksAll, checksForDateRange] = await Promise.all([
model.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(), this.Check.find({ monitorId }).sort({ createdAt: sortOrder }).hint(indexSpec).lean(),
model this.Check.find({
.find({ monitorId,
monitorId, createdAt: { $gte: dateRange.start, $lte: dateRange.end },
createdAt: { $gte: dateRange.start, $lte: dateRange.end }, })
})
.hint(indexSpec) .hint(indexSpec)
.lean(), .lean(),
]); ]);
@@ -295,9 +270,8 @@ class MonitorModule {
const sort = sortOrder === "asc" ? 1 : -1; const sort = sortOrder === "asc" ? 1 : -1;
// Get Checks for monitor in date range requested // Get Checks for monitor in date range requested
const model = this.CHECK_MODEL_LOOKUP[monitor.type];
const dates = this.getDateRange(dateRange); 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 // Build monitor stats
const monitorStats = { const monitorStats = {
@@ -339,7 +313,7 @@ class MonitorModule {
}; };
const dateString = formatLookup[dateRange]; 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]; const stats = hardwareStats[0];
@@ -5,12 +5,12 @@ const buildUptimeDetailsPipeline = (monitorId, dates, dateString) => {
{ {
$match: { $match: {
monitorId: new ObjectId(monitorId), monitorId: new ObjectId(monitorId),
createdAt: { $gte: dates.start, $lte: dates.end }, updatedAt: { $gte: dates.start, $lte: dates.end },
}, },
}, },
{ {
$sort: { $sort: {
createdAt: 1, updatedAt: 1,
}, },
}, },
{ {
@@ -173,6 +173,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
{ {
$match: { $match: {
monitorId: monitor._id, monitorId: monitor._id,
type: "hardware",
createdAt: { $gte: dates.start, $lte: dates.end }, createdAt: { $gte: dates.start, $lte: dates.end },
}, },
}, },
@@ -218,14 +219,14 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
{ {
$project: { $project: {
diskCount: { diskCount: {
$size: "$disk", $size: { $ifNull: ["$disk", []] },
}, },
netCount: { $size: "$net" }, netCount: { $size: { $ifNull: ["$net", []] } },
}, },
}, },
{ {
$lookup: { $lookup: {
from: "hardwarechecks", from: "checks",
let: { let: {
diskCount: "$diskCount", diskCount: "$diskCount",
netCount: "$netCount", netCount: "$netCount",
@@ -381,7 +382,7 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
}, },
net: { net: {
$map: { $map: {
input: { $range: [0, { $size: { $arrayElemAt: ["$net", 0] } }] }, input: { $range: [0, { $size: { $ifNull: [{ $arrayElemAt: ["$net", 0] }, []] } }] },
as: "netIndex", as: "netIndex",
in: { in: {
name: { name: {
@@ -409,7 +410,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
$arrayElemAt: [ $arrayElemAt: [
{ {
$map: { $map: {
input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, input: {
$arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }],
},
as: "iface", as: "iface",
in: "$$iface.bytes_sent", in: "$$iface.bytes_sent",
}, },
@@ -418,7 +421,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
], ],
}, },
tFirst: { $arrayElemAt: ["$updatedAts", 0] }, tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] }, tLast: {
$arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }],
},
}, },
in: { in: {
$cond: [ $cond: [
@@ -444,7 +449,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
$arrayElemAt: [ $arrayElemAt: [
{ {
$map: { $map: {
input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, input: {
$arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }],
},
as: "iface", as: "iface",
in: "$$iface.bytes_recv", in: "$$iface.bytes_recv",
}, },
@@ -453,7 +460,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
], ],
}, },
tFirst: { $arrayElemAt: ["$updatedAts", 0] }, tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] }, tLast: {
$arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }],
},
}, },
in: { in: {
$cond: [ $cond: [
@@ -479,7 +488,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
$arrayElemAt: [ $arrayElemAt: [
{ {
$map: { $map: {
input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, input: {
$arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }],
},
as: "iface", as: "iface",
in: "$$iface.packets_sent", in: "$$iface.packets_sent",
}, },
@@ -488,7 +499,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
], ],
}, },
tFirst: { $arrayElemAt: ["$updatedAts", 0] }, tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] }, tLast: {
$arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }],
},
}, },
in: { in: {
$cond: [ $cond: [
@@ -522,9 +535,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
$map: { $map: {
input: { input: {
$arrayElemAt: [ $arrayElemAt: [
"$net", { $ifNull: ["$net", []] },
{ {
$subtract: [{ $size: "$net" }, 1], $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1],
}, },
], ],
}, },
@@ -538,9 +551,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
tFirst: { $arrayElemAt: ["$updatedAts", 0] }, tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: { tLast: {
$arrayElemAt: [ $arrayElemAt: [
"$updatedAts", { $ifNull: ["$updatedAts", []] },
{ {
$subtract: [{ $size: "$updatedAts" }, 1], $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1],
}, },
], ],
}, },
@@ -566,7 +579,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
$arrayElemAt: [ $arrayElemAt: [
{ {
$map: { $map: {
input: { $arrayElemAt: ["$net", { $subtract: [{ $size: "$net" }, 1] }] }, input: {
$arrayElemAt: [{ $ifNull: ["$net", []] }, { $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1] }],
},
as: "iface", as: "iface",
in: "$$iface.err_in", in: "$$iface.err_in",
}, },
@@ -575,7 +590,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
], ],
}, },
tFirst: { $arrayElemAt: ["$updatedAts", 0] }, tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: { $arrayElemAt: ["$updatedAts", { $subtract: [{ $size: "$updatedAts" }, 1] }] }, tLast: {
$arrayElemAt: [{ $ifNull: ["$updatedAts", []] }, { $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1] }],
},
}, },
in: { in: {
$cond: [ $cond: [
@@ -609,9 +626,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
$map: { $map: {
input: { input: {
$arrayElemAt: [ $arrayElemAt: [
"$net", { $ifNull: ["$net", []] },
{ {
$subtract: [{ $size: "$net" }, 1], $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1],
}, },
], ],
}, },
@@ -625,9 +642,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
tFirst: { $arrayElemAt: ["$updatedAts", 0] }, tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: { tLast: {
$arrayElemAt: [ $arrayElemAt: [
"$updatedAts", { $ifNull: ["$updatedAts", []] },
{ {
$subtract: [{ $size: "$updatedAts" }, 1], $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1],
}, },
], ],
}, },
@@ -664,9 +681,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
$map: { $map: {
input: { input: {
$arrayElemAt: [ $arrayElemAt: [
"$net", { $ifNull: ["$net", []] },
{ {
$subtract: [{ $size: "$net" }, 1], $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1],
}, },
], ],
}, },
@@ -680,9 +697,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
tFirst: { $arrayElemAt: ["$updatedAts", 0] }, tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: { tLast: {
$arrayElemAt: [ $arrayElemAt: [
"$updatedAts", { $ifNull: ["$updatedAts", []] },
{ {
$subtract: [{ $size: "$updatedAts" }, 1], $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1],
}, },
], ],
}, },
@@ -719,9 +736,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
$map: { $map: {
input: { input: {
$arrayElemAt: [ $arrayElemAt: [
"$net", { $ifNull: ["$net", []] },
{ {
$subtract: [{ $size: "$net" }, 1], $subtract: [{ $size: { $ifNull: ["$net", []] } }, 1],
}, },
], ],
}, },
@@ -735,9 +752,9 @@ const buildHardwareDetailsPipeline = (monitor, dates, dateString) => {
tFirst: { $arrayElemAt: ["$updatedAts", 0] }, tFirst: { $arrayElemAt: ["$updatedAts", 0] },
tLast: { tLast: {
$arrayElemAt: [ $arrayElemAt: [
"$updatedAts", { $ifNull: ["$updatedAts", []] },
{ {
$subtract: [{ $size: "$updatedAts" }, 1], $subtract: [{ $size: { $ifNull: ["$updatedAts", []] } }, 1],
}, },
], ],
}, },
@@ -970,12 +987,7 @@ const buildMonitorsWithChecksByTeamIdPipeline = ({ matchStage, filter, page, row
// Add checks // Add checks
if (limit) { if (limit) {
let checksCollection = "checks"; const checksCollection = "checks";
if (type === "pagespeed") {
checksCollection = "pagespeedchecks";
} else if (type === "hardware") {
checksCollection = "hardwarechecks";
}
monitorsPipeline.push({ monitorsPipeline.push({
$lookup: { $lookup: {
from: checksCollection, from: checksCollection,
@@ -1040,12 +1052,8 @@ const buildFilteredMonitorsByTeamIdPipeline = ({ matchStage, filter, page, rowsP
// Add checks // Add checks
if (limit) { if (limit) {
let checksCollection = "checks"; const checksCollection = "checks";
if (type === "pagespeed") {
checksCollection = "pagespeedchecks";
} else if (type === "hardware") {
checksCollection = "hardwarechecks";
}
pipeline.push({ pipeline.push({
$lookup: { $lookup: {
from: checksCollection, from: checksCollection,
@@ -1159,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: { $addFields: {
@@ -1209,14 +1177,6 @@ const buildGetMonitorsByTeamIdPipeline = (req) => {
case: { $in: ["$type", ["http", "ping", "docker", "port", "game"]] }, case: { $in: ["$type", ["http", "ping", "docker", "port", "game"]] },
then: "$standardchecks", then: "$standardchecks",
}, },
{
case: { $eq: ["$type", "pagespeed"] },
then: "$pagespeedchecks",
},
{
case: { $eq: ["$type", "hardware"] },
then: "$hardwarechecks",
},
], ],
default: [], default: [],
}, },
@@ -1226,8 +1186,6 @@ const buildGetMonitorsByTeamIdPipeline = (req) => {
{ {
$project: { $project: {
standardchecks: 0, standardchecks: 0,
pagespeedchecks: 0,
hardwarechecks: 0,
}, },
}, },
], ],
@@ -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;
@@ -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;
+1 -1
View File
@@ -18,7 +18,7 @@ const startApp = async () => {
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const openApiSpec = JSON.parse(fs.readFileSync(path.join(__dirname, "../openapi.json"), "utf8")); const openApiSpec = JSON.parse(fs.readFileSync(path.join(__dirname, "../openapi.json"), "utf8"));
const frontendPath = path.join(__dirname, "public"); const frontendPath = path.join(__dirname, "..", "public");
// Create services // Create services
const settingsService = new SettingsService(AppSettings); const settingsService = new SettingsService(AppSettings);
+1 -2
View File
@@ -33,10 +33,9 @@ class CheckService {
throw this.errorService.createAuthorizationError(); 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({ const result = await this.db.checkModule.getChecksByMonitor({
monitorId, monitorId,
type,
sortOrder, sortOrder,
dateRange, dateRange,
filter, filter,
@@ -54,6 +54,7 @@ class SuperSimpleQueue {
id: monitorId.toString(), id: monitorId.toString(),
template: "monitor-job", template: "monitor-job",
repeat: monitor.interval, repeat: monitor.interval,
active: monitor.isActive,
data: monitor.toObject(), data: monitor.toObject(),
}); });
}; };
@@ -2,6 +2,16 @@ const SERVICE_NAME = "JobQueueHelper";
class SuperSimpleQueueHelper { class SuperSimpleQueueHelper {
static SERVICE_NAME = SERVICE_NAME; 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 }) { constructor({ db, logger, networkService, statusService, notificationService }) {
this.db = db; this.db = db;
this.logger = logger; this.logger = logger;
@@ -31,7 +41,8 @@ class SuperSimpleQueueHelper {
}); });
return; return;
} }
const networkResponse = await this.networkService.getStatus(monitor); const networkResponse = await this.networkService.requestStatus(monitor);
if (!networkResponse) { if (!networkResponse) {
throw new Error("No network response"); throw new Error("No network response");
} }
@@ -1,32 +1,13 @@
const SERVICE_NAME = "BufferService"; const SERVICE_NAME = "BufferService";
const TYPE_MAP = {
http: "checks",
ping: "checks",
port: "checks",
docker: "checks",
pagespeed: "pagespeedChecks",
hardware: "hardwareChecks",
game: "checks",
};
class BufferService { class BufferService {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
constructor({ db, logger, envSettings }) { 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.db = db;
this.logger = logger; this.logger = logger;
this.SERVICE_NAME = SERVICE_NAME; this.SERVICE_NAME = SERVICE_NAME;
this.buffers = { this.buffer = [];
checks: [],
pagespeedChecks: [],
hardwareChecks: [],
};
this.OPERATION_MAP = {
checks: this.db.checkModule.createChecks,
pagespeedChecks: this.db.pageSpeedCheckModule.createPageSpeedChecks,
hardwareChecks: this.db.hardwareCheckModule.createHardwareChecks,
};
this.scheduleNextFlush(); this.scheduleNextFlush();
this.logger.info({ this.logger.info({
message: `Buffer service initialized, flushing every ${this.BUFFER_TIMEOUT / 1000}s`, message: `Buffer service initialized, flushing every ${this.BUFFER_TIMEOUT / 1000}s`,
@@ -39,9 +20,9 @@ class BufferService {
return BufferService.SERVICE_NAME; return BufferService.SERVICE_NAME;
} }
addToBuffer({ check, type }) { addToBuffer({ check }) {
try { try {
this.buffers[TYPE_MAP[type]].push(check); this.buffer.push(check);
} catch (error) { } catch (error) {
this.logger.error({ this.logger.error({
message: error.message, message: error.message,
@@ -55,7 +36,7 @@ class BufferService {
scheduleNextFlush() { scheduleNextFlush() {
this.bufferTimer = setTimeout(async () => { this.bufferTimer = setTimeout(async () => {
try { try {
await this.flushBuffers(); await this.flushBuffer();
} catch (error) { } catch (error) {
this.logger.error({ this.logger.error({
message: `Error in flush cycle: ${error.message}`, message: `Error in flush cycle: ${error.message}`,
@@ -69,35 +50,24 @@ class BufferService {
} }
}, this.BUFFER_TIMEOUT); }, this.BUFFER_TIMEOUT);
} }
async flushBuffers() { async flushBuffer() {
let items = 0; let items = this.buffer.length;
for (const [bufferName, buffer] of Object.entries(this.buffers)) {
items += buffer.length; try {
const operation = this.OPERATION_MAP[bufferName]; await this.db.checkModule.createChecks(this.buffer);
if (!operation) { } catch (error) {
this.logger.error({ this.logger.error({
message: `No operation found for ${bufferName}`, message: error.message,
service: this.SERVICE_NAME, service: this.SERVICE_NAME,
method: "flushBuffers", method: "flushBuffer",
}); stack: error.stack,
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] = [];
} }
this.buffer = [];
this.logger.debug({ this.logger.debug({
message: `Flushed ${items} items`, message: `Flushed ${items} items`,
service: this.SERVICE_NAME, service: this.SERVICE_NAME,
method: "flushBuffers", method: "flushBuffer",
}); });
items = 0; items = 0;
} }
+184 -309
View File
@@ -1,23 +1,9 @@
import jmespath from "jmespath";
import https from "https";
import { GameDig } from "gamedig";
const SERVICE_NAME = "NetworkService"; 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 { class NetworkService {
static SERVICE_NAME = SERVICE_NAME; 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_PING = "ping";
this.TYPE_HTTP = "http"; this.TYPE_HTTP = "http";
this.TYPE_PAGESPEED = "pagespeed"; this.TYPE_PAGESPEED = "pagespeed";
@@ -29,6 +15,10 @@ class NetworkService {
this.NETWORK_ERROR = 5000; this.NETWORK_ERROR = 5000;
this.PING_ERROR = 5001; this.PING_ERROR = 5001;
this.axios = axios; this.axios = axios;
this.got = got;
this.https = https;
this.jmespath = jmespath;
this.GameDig = GameDig;
this.ping = ping; this.ping = ping;
this.logger = logger; this.logger = logger;
this.http = http; this.http = http;
@@ -38,151 +28,134 @@ class NetworkService {
this.settingsService = settingsService; this.settingsService = settingsService;
} }
get serviceName() { // Helper functions
return NetworkService.SERVICE_NAME;
}
/**
* Times the execution of an asynchronous operation.
*
* @param {Function} operation - The asynchronous operation to be timed.
* @returns {Promise<Object>} 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.
*/
async timeRequest(operation) { async timeRequest(operation) {
const startTime = Date.now(); const start = process.hrtime.bigint();
try { try {
const response = await operation(); const response = await operation();
const endTime = Date.now(); const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000);
const responseTime = endTime - startTime; return { response, responseTime: elapsedMs };
return { response, responseTime };
} catch (error) { } catch (error) {
const endTime = Date.now(); const elapsedMs = Math.round(Number(process.hrtime.bigint() - start) / 1_000_000);
const responseTime = endTime - startTime; return { response: null, responseTime: elapsedMs, error };
return { response: null, responseTime, 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<Object>} 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) { async requestPing(monitor) {
try { try {
const url = monitor.url; if (!monitor?.url) {
const { response, responseTime, error } = await this.timeRequest(() => this.ping.promise.probe(url)); throw new Error("Monitor URL is required");
}
const { response, responseTime, error } = await this.timeRequest(() => this.ping.promise.probe(monitor.url));
if (!response) {
throw new Error("Ping failed - no result returned");
}
const pingResponse = { const pingResponse = {
monitorId: monitor._id, monitorId: monitor._id,
type: "ping", type: "ping",
status: response.alive,
code: 200,
responseTime, responseTime,
message: "Success",
payload: response, payload: response,
}; };
if (error) { if (error) {
pingResponse.status = false; pingResponse.status = false;
pingResponse.code = this.PING_ERROR; pingResponse.code = 200;
pingResponse.message = "No response"; pingResponse.message = "Ping failed";
return pingResponse; return pingResponse;
} }
pingResponse.code = 200;
pingResponse.status = response.alive;
pingResponse.message = "Success";
return pingResponse; return pingResponse;
} catch (error) { } catch (err) {
error.service = this.SERVICE_NAME; err.service = this.SERVICE_NAME;
error.method = "requestPing"; err.method = "requestPing";
throw error; 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<Object>} 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) { async requestHttp(monitor) {
const { url, secret, _id, name, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor;
const httpResponse = {
monitorId: _id,
teamId: teamId,
type,
};
try { try {
const { url, secret, _id, name, teamId, type, ignoreTlsErrors, jsonPath, matchMethod, expectedValue } = monitor; if (!url) {
const config = {}; throw new Error("Monitor URL is required");
secret !== undefined && (config.headers = { Authorization: `Bearer ${secret}` });
if (ignoreTlsErrors === true) {
config.httpsAgent = new https.Agent({
rejectUnauthorized: false,
});
} }
const config = {
const { response, responseTime, error } = await this.timeRequest(() => this.axios.get(url, config)); headers: secret ? { Authorization: `Bearer ${secret}` } : undefined,
const httpResponse = {
monitorId: _id,
teamId,
type,
responseTime,
payload: response?.data,
}; };
if (error) { if (ignoreTlsErrors) {
const code = error.response?.status || this.NETWORK_ERROR; config.agent = {
httpResponse.code = code; https: new this.https.Agent({
httpResponse.status = false; rejectUnauthorized: false,
httpResponse.message = this.http.STATUS_CODES[code] || this.stringService.httpNetworkError; }),
return httpResponse; };
} }
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) { if (!expectedValue) {
// not configure expected value, return
httpResponse.status = true;
httpResponse.message = this.http.STATUS_CODES[response.status];
return httpResponse; return httpResponse;
} }
// validate if response data match expected value
let result = response?.data;
this.logger.info({ this.logger.info({
service: this.SERVICE_NAME, service: this.SERVICE_NAME,
method: "requestHttp", method: "requestHttp",
message: `Job: [${name}](${_id}) match result with expected value`, message: `Job: [${name}](${_id}) match result with expected value`,
details: { expectedValue, result, jsonPath, matchMethod }, details: { expectedValue, payload, jsonPath, matchMethod },
}); });
if (jsonPath) { if (jsonPath) {
@@ -196,106 +169,75 @@ class NetworkService {
} }
try { try {
result = jmespath.search(result, jsonPath); this.jmespath.search(payload, jsonPath);
} catch (error) { } catch {
httpResponse.status = false; httpResponse.status = false;
httpResponse.message = this.stringService.httpJsonPathError; httpResponse.message = this.stringService.httpJsonPathError;
return httpResponse; return httpResponse;
} }
} }
return httpResponse;
if (result === null || result === undefined) { } catch (err) {
if (err.name === "HTTPError" || err.name === "RequestError") {
httpResponse.code = err?.response?.statusCode || this.NETWORK_ERROR;
httpResponse.status = false; 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; return httpResponse;
} }
err.service = this.SERVICE_NAME;
let match; err.method = "requestHttp";
result = typeof result === "object" ? JSON.stringify(result) : result.toString(); throw err;
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;
} }
} }
/** async requestPageSpeed(monitor) {
* 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<Object|undefined>} 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) {
try { try {
const url = monitor.url; const url = monitor.url;
const updatedMonitor = { ...monitor }; if (!url) {
let pagespeedUrl = `https://pagespeedonline.googleapis.com/pagespeedonline/v5/runPagespeed?url=${url}&category=seo&category=accessibility&category=best-practices&category=performance`; 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(); const dbSettings = await this.settingsService.getDBSettings();
if (dbSettings?.pagespeedApiKey) { if (dbSettings?.pagespeedApiKey) {
pagespeedUrl += `&key=${dbSettings.pagespeedApiKey}`; pageSpeedUrl += `&key=${dbSettings.pagespeedApiKey}`;
} else { } else {
this.logger.warn({ 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, service: this.SERVICE_NAME,
method: "requestPagespeed", method: "requestPagespeed",
details: { url }, details: { url },
}); });
return;
} }
updatedMonitor.url = pagespeedUrl; return await this.requestHttp({
return await this.requestHttp(updatedMonitor); ...monitor,
} catch (error) { url: pageSpeedUrl,
error.service = this.SERVICE_NAME; });
error.method = "requestPagespeed"; } catch (err) {
throw error; err.service = this.SERVICE_NAME;
err.method = "requestPageSpeed";
throw err;
} }
} }
async requestHardware(monitor) { async requestHardware(monitor) {
try { try {
return await this.requestHttp(monitor); return await this.requestHttp(monitor);
} catch (error) { } catch (err) {
error.service = this.SERVICE_NAME; err.service = this.SERVICE_NAME;
error.method = "requestHardware"; err.method = "requestHardware";
throw error; 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<Object>} 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) { async requestDocker(monitor) {
try { try {
if (!monitor.url) {
throw new Error("Monitor URL is required");
}
const docker = new this.Docker({ const docker = new this.Docker({
socketPath: "/var/run/docker.sock", socketPath: "/var/run/docker.sock",
handleError: true, // Enable error handling handleError: true, // Enable error handling
@@ -306,14 +248,17 @@ class NetworkService {
if (!containerExists) { if (!containerExists) {
throw new Error(this.stringService.dockerNotFound); 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 { response, responseTime, error } = await this.timeRequest(() => container.inspect());
const dockerResponse = { const dockerResponse = {
monitorId: monitor._id, monitorId: monitor._id,
type: monitor.type, type: monitor.type,
responseTime, responseTime,
status: response?.State?.Status === "running" ? true : false,
code: 200,
message: "Docker container status fetched successfully",
}; };
if (error) { if (error) {
@@ -322,34 +267,15 @@ class NetworkService {
dockerResponse.message = error.reason || "Failed to fetch Docker container information"; dockerResponse.message = error.reason || "Failed to fetch Docker container information";
return dockerResponse; return dockerResponse;
} }
dockerResponse.status = response?.State?.Status === "running" ? true : false;
dockerResponse.code = 200;
dockerResponse.message = "Docker container status fetched successfully";
return dockerResponse; return dockerResponse;
} catch (error) { } catch (err) {
error.service = this.SERVICE_NAME; err.service = this.SERVICE_NAME;
error.method = "requestDocker"; err.method = "requestDocker";
throw error; 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<Object>} 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) { async requestPort(monitor) {
try { try {
const { url, port } = monitor; const { url, port } = monitor;
@@ -381,21 +307,21 @@ class NetworkService {
}); });
const portResponse = { const portResponse = {
code: 200,
status: response.success,
message: this.stringService.portSuccess,
monitorId: monitor._id, monitorId: monitor._id,
type: monitor.type, type: monitor.type,
responseTime, responseTime,
}; };
if (error) { if (error) {
portResponse.status = false;
portResponse.code = this.NETWORK_ERROR; portResponse.code = this.NETWORK_ERROR;
portResponse.status = false;
portResponse.message = this.stringService.portFail; portResponse.message = this.stringService.portFail;
return portResponse; return portResponse;
} }
portResponse.status = response.success;
portResponse.code = 200;
portResponse.message = this.stringService.portSuccess;
return portResponse; return portResponse;
} catch (error) { } catch (error) {
error.service = this.SERVICE_NAME; error.service = this.SERVICE_NAME;
@@ -404,19 +330,55 @@ class NetworkService {
} }
} }
/** async requestGame(monitor) {
* Handles unsupported job types by throwing an error with details. try {
* const { url, port, gameId } = monitor;
* @param {string} type - The unsupported job type that was provided
* @throws {Error} An error with service name, method name and unsupported type message const gameResponse = {
*/ code: 200,
handleUnsupportedType(type) { 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}`); const err = new Error(`Unsupported type: ${type}`);
err.service = this.SERVICE_NAME; err.service = this.SERVICE_NAME;
err.method = "getStatus"; err.method = "getStatus";
throw err; throw err;
} }
// Other network requests unrelated to monitoring:
async requestWebhook(type, url, body) { async requestWebhook(type, url, body) {
try { try {
const response = await this.axios.post(url, body, { const response = await this.axios.post(url, body, {
@@ -471,93 +433,6 @@ class NetworkService {
throw error; throw error;
} }
} }
/**
* Requests the status of a game monitor.
*
* @param {Object} monitor - The monitor object to request the status for.
* @returns {Promise<Object>} 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<Object>} 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; export default NetworkService;
@@ -1,17 +1,14 @@
import MonitorStats from "../../db/models/MonitorStats.js"; import MonitorStats from "../../db/models/MonitorStats.js";
import { safelyParseFloat } from "../../utils/dataUtils.js";
const SERVICE_NAME = "StatusService"; const SERVICE_NAME = "StatusService";
class StatusService { class StatusService {
static SERVICE_NAME = SERVICE_NAME; static SERVICE_NAME = SERVICE_NAME;
/** /**
* Creates an instance of StatusService. * @param {{
* * buffer: import("./bufferService.js").BufferService
* @param {Object} db - The database instance. * }}
* @param {Object} logger - The logger instance. */ constructor({ db, logger, buffer }) {
*/
constructor({ db, logger, buffer }) {
this.db = db; this.db = db;
this.logger = logger; this.logger = logger;
this.buffer = buffer; this.buffer = buffer;
@@ -24,7 +21,7 @@ class StatusService {
async updateRunningStats({ monitor, networkResponse }) { async updateRunningStats({ monitor, networkResponse }) {
try { try {
const monitorId = monitor._id; const monitorId = monitor._id;
const { responseTime, status, upt_burnt } = networkResponse; const { responseTime, status } = networkResponse;
// Get stats // Get stats
let stats = await MonitorStats.findOne({ monitorId }); let stats = await MonitorStats.findOne({ monitorId });
if (!stats) { if (!stats) {
@@ -36,8 +33,6 @@ class StatusService {
totalDownChecks: 0, totalDownChecks: 0,
uptimePercentage: 0, uptimePercentage: 0,
lastCheck: null, lastCheck: null,
timeSInceLastCheck: 0,
uptBurnt: 0,
}); });
} }
@@ -82,12 +77,6 @@ class StatusService {
// latest check // latest check
stats.lastCheckTimestamp = new Date().getTime(); 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(); await stats.save();
return true; return true;
} catch (error) { } catch (error) {
@@ -119,7 +108,8 @@ class StatusService {
* @returns {boolean} returnObject.prevStatus - The previous status of the monitor * @returns {boolean} returnObject.prevStatus - The previous status of the monitor
*/ */
updateStatus = async (networkResponse) => { updateStatus = async (networkResponse) => {
this.insertCheck(networkResponse); const check = this.buildCheck(networkResponse);
await this.insertCheck(check);
try { try {
const { monitorId, status, code } = networkResponse; const { monitorId, status, code } = networkResponse;
const monitor = await this.db.monitorModule.getMonitorById(monitorId); const monitor = await this.db.monitorModule.getMonitorById(monitorId);
@@ -127,31 +117,67 @@ class StatusService {
// Update running stats // Update running stats
this.updateRunningStats({ monitor, networkResponse }); this.updateRunningStats({ monitor, networkResponse });
// No change in monitor status, return early // If the status window size has changed, empty
if (monitor.status === status) 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 === undefined || monitor.status === null) {
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 { return {
monitor, monitor,
statusChanged: false, statusChanged: false,
prevStatus: monitor.status, prevStatus,
code, code,
timestamp: new Date().getTime(), timestamp: Date.now(),
}; };
}
// Monitor status changed, save prev status and update monitor // Check if threshold has been met
this.logger.info({ const failures = monitor.statusWindow.filter((s) => s === false).length;
service: this.SERVICE_NAME, const failureRate = (failures / monitor.statusWindow.length) * 100;
message: `${monitor.name} went from ${this.getStatusString(monitor.status)} to ${this.getStatusString(status)}`,
prevStatus: monitor.status,
newStatus: status,
});
const prevStatus = monitor.status; // If threshold has been met and the monitor is not already down, mark down:
monitor.status = status; 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(); await monitor.save();
return { return {
monitor, monitor,
statusChanged: true, statusChanged,
prevStatus, prevStatus,
code, code,
timestamp: new Date().getTime(), timestamp: new Date().getTime(),
@@ -192,14 +218,17 @@ class StatusService {
conn_took, conn_took,
connect_took, connect_took,
tls_took, tls_took,
timings,
} = networkResponse; } = networkResponse;
const check = { const check = {
monitorId, monitorId,
teamId, teamId,
type,
status, status,
statusCode: code, statusCode: code,
responseTime, responseTime,
timings: timings || {},
message, message,
first_byte_took, first_byte_took,
body_read_took, body_read_took,
@@ -262,25 +291,24 @@ class StatusService {
* @param {Object} networkResponse.payload - The payload of the response. * @param {Object} networkResponse.payload - The payload of the response.
* @returns {Promise<void>} A promise that resolves when the check is inserted. * @returns {Promise<void>} A promise that resolves when the check is inserted.
*/ */
insertCheck = async (networkResponse) => { insertCheck = async (check) => {
try { try {
const check = this.buildCheck(networkResponse);
if (typeof check === "undefined") { if (typeof check === "undefined") {
this.logger.warn({ this.logger.warn({
message: "Failed to build check", message: "Failed to build check",
service: this.SERVICE_NAME, service: this.SERVICE_NAME,
method: "insertCheck", method: "insertCheck",
details: networkResponse,
}); });
return; return false;
} }
this.buffer.addToBuffer({ check, type: networkResponse.type }); this.buffer.addToBuffer({ check });
return true;
} catch (error) { } catch (error) {
this.logger.error({ this.logger.error({
message: error.message, message: error.message,
service: error.service || this.SERVICE_NAME, service: error.service || this.SERVICE_NAME,
method: error.method || "insertCheck", 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, stack: error.stack,
}); });
} }
+4
View File
@@ -155,6 +155,8 @@ const createMonitorBodyValidation = joi.object({
name: joi.string().required(), name: joi.string().required(),
description: joi.string().required(), description: joi.string().required(),
type: 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(), url: joi.string().required(),
ignoreTlsErrors: joi.boolean().default(false), ignoreTlsErrors: joi.boolean().default(false),
port: joi.number(), port: joi.number(),
@@ -183,6 +185,8 @@ const createMonitorsBodyValidation = joi.array().items(
const editMonitorBodyValidation = joi.object({ const editMonitorBodyValidation = joi.object({
name: joi.string(), name: joi.string(),
statusWindowSize: joi.number().min(1).max(20).default(5),
statusWindowThreshold: joi.number().min(1).max(100).default(60),
description: joi.string(), description: joi.string(),
interval: joi.number(), interval: joi.number(),
notifications: joi.array().items(joi.string()), notifications: joi.array().items(joi.string()),