Compare commits

..
47 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
51 changed files with 1580 additions and 1716 deletions

No files matched your search

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